From 450412a7eb5c9392bee98542654c81aaca86477a Mon Sep 17 00:00:00 2001 From: ProfSynapse Date: Tue, 19 May 2026 16:13:17 -0400 Subject: [PATCH 1/5] Add Gemini 3.5 Flash to Google and OpenRouter model registries - google/GoogleModels.ts: gemini-3.5-flash (1M context, 65K output, $1.50/$9.00 per 1M) - openrouter/OpenRouterModels.ts: google/gemini-3.5-flash with same specs - Capabilities: JSON, Images, Functions, Streaming, Thinking - No default model change; strictly additive Sources: openrouter.ai/google/gemini-3.5-flash, ai.google.dev (released 2026-05-19) --- .../llm/adapters/google/GoogleModels.ts | 18 ++++++++++++++++++ .../adapters/openrouter/OpenRouterModels.ts | 16 ++++++++++++++++ 2 files changed, 34 insertions(+) diff --git a/src/services/llm/adapters/google/GoogleModels.ts b/src/services/llm/adapters/google/GoogleModels.ts index 72bc20952..4f22ece20 100644 --- a/src/services/llm/adapters/google/GoogleModels.ts +++ b/src/services/llm/adapters/google/GoogleModels.ts @@ -40,6 +40,24 @@ export const GOOGLE_MODELS: ModelSpec[] = [ } }, + // Gemini 3.5 models + { + provider: 'google', + name: 'Gemini 3.5 Flash', + apiName: 'gemini-3.5-flash', + contextWindow: 1048576, + maxTokens: 65536, + inputCostPerMillion: 1.50, + outputCostPerMillion: 9.00, + capabilities: { + supportsJSON: true, + supportsImages: true, + supportsFunctions: true, + supportsStreaming: true, + supportsThinking: true + } + }, + // Gemini 3.0 models { provider: 'google', diff --git a/src/services/llm/adapters/openrouter/OpenRouterModels.ts b/src/services/llm/adapters/openrouter/OpenRouterModels.ts index 0ea0fd1ee..cceb93dec 100644 --- a/src/services/llm/adapters/openrouter/OpenRouterModels.ts +++ b/src/services/llm/adapters/openrouter/OpenRouterModels.ts @@ -281,6 +281,22 @@ export const OPENROUTER_MODELS: ModelSpec[] = [ supportsThinking: true } }, + { + provider: 'openrouter', + name: 'Gemini 3.5 Flash', + apiName: 'google/gemini-3.5-flash', + contextWindow: 1048576, + maxTokens: 65536, + inputCostPerMillion: 1.50, + outputCostPerMillion: 9.00, + capabilities: { + supportsJSON: true, + supportsImages: true, + supportsFunctions: true, + supportsStreaming: true, + supportsThinking: true + } + }, { provider: 'openrouter', name: 'GPT-5', From 7da78efd45bf30647b0d206ebbadd7cb6742fb23 Mon Sep 17 00:00:00 2001 From: ProfSynapse Date: Tue, 19 May 2026 16:28:58 -0400 Subject: [PATCH 2/5] Extend provider smoke harness for Google adapter - Adds 'google' to SmokeProvider union, PROVIDERS list, normalizeModelForProvider - createAdapter() handles GoogleAdapter with GEMINI_API_KEY - readDotEnv() falls back to main repo .env when run from a worktree - Live smoke verified: gemini-3.5-flash (Google direct) + google/gemini-3.5-flash (OpenRouter) --- tests/debug/provider-model-live-smoke.test.ts | 35 ++++++++++++++++--- 1 file changed, 30 insertions(+), 5 deletions(-) diff --git a/tests/debug/provider-model-live-smoke.test.ts b/tests/debug/provider-model-live-smoke.test.ts index 8540a4e31..ffdb1a0f3 100644 --- a/tests/debug/provider-model-live-smoke.test.ts +++ b/tests/debug/provider-model-live-smoke.test.ts @@ -30,11 +30,12 @@ import { DEFAULT_MODELS } from '../../src/services/llm/adapters/ModelRegistry'; import { OpenAIAdapter } from '../../src/services/llm/adapters/openai/OpenAIAdapter'; import { OpenRouterAdapter } from '../../src/services/llm/adapters/openrouter/OpenRouterAdapter'; import { OpenAICodexAdapter, type CodexOAuthTokens } from '../../src/services/llm/adapters/openai-codex/OpenAICodexAdapter'; +import { GoogleAdapter } from '../../src/services/llm/adapters/google/GoogleAdapter'; import type { GenerateOptions, LLMResponse } from '../../src/services/llm/adapters/types'; jest.setTimeout(240_000); -type SmokeProvider = 'openai' | 'openrouter' | 'openai-codex'; +type SmokeProvider = 'openai' | 'openrouter' | 'openai-codex' | 'google'; interface ProviderSettingsShape { llmProviders?: { @@ -58,13 +59,24 @@ interface SmokeTarget { } const RUN_LIVE = process.env.RUN_MODEL_SMOKE === '1'; -const PROVIDERS: SmokeProvider[] = ['openai', 'openrouter', 'openai-codex']; +const PROVIDERS: SmokeProvider[] = ['openai', 'openrouter', 'openai-codex', 'google']; + +function findDotEnv(): string | null { + const candidates = [ + path.join(process.cwd(), '.env'), + '/Users/jrosenbaum/Documents/Code/.obsidian/plugins/claudesidian-mcp/.env', + ]; + for (const c of candidates) { + if (fs.existsSync(c)) return c; + } + return null; +} function readDotEnv(): Map { - const envPath = path.join(process.cwd(), '.env'); + const envPath = findDotEnv(); const values = new Map(); - if (!fs.existsSync(envPath)) { + if (!envPath) { return values; } @@ -123,6 +135,10 @@ function normalizeModelForProvider(provider: SmokeProvider, model: string): stri return model.slice('openai/'.length); } + if (provider === 'google' && model.startsWith('google/')) { + return model.slice('google/'.length); + } + return model; } @@ -132,6 +148,7 @@ function getProviderModel(provider: SmokeProvider): string { openai: getEnv('OPENAI_SMOKE_MODEL'), openrouter: getEnv('OPENROUTER_SMOKE_MODEL'), 'openai-codex': getEnv('CODEX_SMOKE_MODEL'), + google: getEnv('GOOGLE_SMOKE_MODEL'), }[provider]; const model = providerOverride || sharedOverride || DEFAULT_MODELS[provider]; @@ -184,7 +201,7 @@ function setRequestUrlToRealFetch(): void { }); } -function createAdapter(provider: SmokeProvider): OpenAIAdapter | OpenRouterAdapter | OpenAICodexAdapter { +function createAdapter(provider: SmokeProvider): OpenAIAdapter | OpenRouterAdapter | OpenAICodexAdapter | GoogleAdapter { if (provider === 'openai') { const apiKey = getEnv('OPENAI_API_KEY'); if (!apiKey) { @@ -201,6 +218,14 @@ function createAdapter(provider: SmokeProvider): OpenAIAdapter | OpenRouterAdapt return new OpenRouterAdapter(apiKey); } + if (provider === 'google') { + const apiKey = getEnv('GEMINI_API_KEY'); + if (!apiKey) { + throw new Error('GEMINI_API_KEY is required for Google smoke tests'); + } + return new GoogleAdapter(apiKey); + } + const tokens = loadCodexTokensFromLocalDataJson(); if (!tokens) { throw new Error('Codex OAuth tokens are required in data.json for Codex smoke tests'); From fb8a0636bd86b2e97dab67ce31fec315fb47484c Mon Sep 17 00:00:00 2001 From: ProfSynapse Date: Tue, 19 May 2026 16:34:09 -0400 Subject: [PATCH 3/5] Revert findDotEnv walk-up in smoke harness readDotEnv() back to its original single-path shape (path.join(process.cwd(), '.env')). Worktree obtains .env via a symlink to the main repo, so harness needs no fallback. --- tests/debug/provider-model-live-smoke.test.ts | 15 ++------------- 1 file changed, 2 insertions(+), 13 deletions(-) diff --git a/tests/debug/provider-model-live-smoke.test.ts b/tests/debug/provider-model-live-smoke.test.ts index ffdb1a0f3..24f67d66c 100644 --- a/tests/debug/provider-model-live-smoke.test.ts +++ b/tests/debug/provider-model-live-smoke.test.ts @@ -61,22 +61,11 @@ interface SmokeTarget { const RUN_LIVE = process.env.RUN_MODEL_SMOKE === '1'; const PROVIDERS: SmokeProvider[] = ['openai', 'openrouter', 'openai-codex', 'google']; -function findDotEnv(): string | null { - const candidates = [ - path.join(process.cwd(), '.env'), - '/Users/jrosenbaum/Documents/Code/.obsidian/plugins/claudesidian-mcp/.env', - ]; - for (const c of candidates) { - if (fs.existsSync(c)) return c; - } - return null; -} - function readDotEnv(): Map { - const envPath = findDotEnv(); + const envPath = path.join(process.cwd(), '.env'); const values = new Map(); - if (!envPath) { + if (!fs.existsSync(envPath)) { return values; } From 968c7f75ba2ecc30229d6b9e62a1969fb6c62fe6 Mon Sep 17 00:00:00 2001 From: ProfSynapse Date: Tue, 19 May 2026 17:17:24 -0400 Subject: [PATCH 4/5] Add gemini-3.5-flash eval config Live-mode OpenRouter config for the LLM eval harness targeting google/gemini-3.5-flash. Mirrors live.yaml shape with the single model variant. Co-Authored-By: Claude Opus 4.7 (1M context) --- tests/eval/configs/gemini-3.5-flash.yaml | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 tests/eval/configs/gemini-3.5-flash.yaml diff --git a/tests/eval/configs/gemini-3.5-flash.yaml b/tests/eval/configs/gemini-3.5-flash.yaml new file mode 100644 index 000000000..b1f6f0acf --- /dev/null +++ b/tests/eval/configs/gemini-3.5-flash.yaml @@ -0,0 +1,23 @@ +mode: live +testVaultPath: tests/eval/test-vault/ + +providers: + openrouter: + apiKeyEnv: OPENROUTER_API_KEY + models: + - google/gemini-3.5-flash + enabled: true + +defaults: + temperature: 0 + maxRetries: 1 + retryDelayMs: 2000 + timeout: 120000 + systemPrompt: default + +capture: + enabled: true + dumpOnFailure: true + artifactsDir: test-artifacts/ + +scenarios: tests/eval/scenarios/**/*.eval.yaml From 85e3a0622261104706de066308d6193ad87b97c4 Mon Sep 17 00:00:00 2001 From: ProfSynapse Date: Tue, 19 May 2026 20:34:37 -0400 Subject: [PATCH 5/5] test(models): cover Gemini 3.5 Flash registry entries --- .../llm/adapters/google/GoogleModels.ts | 6 ++--- tests/debug/provider-model-live-smoke.test.ts | 2 +- tests/unit/ModelRegistry.test.ts | 22 +++++++++++++++++++ 3 files changed, 26 insertions(+), 4 deletions(-) diff --git a/src/services/llm/adapters/google/GoogleModels.ts b/src/services/llm/adapters/google/GoogleModels.ts index 4f22ece20..de085997c 100644 --- a/src/services/llm/adapters/google/GoogleModels.ts +++ b/src/services/llm/adapters/google/GoogleModels.ts @@ -1,12 +1,12 @@ /** * Google Model Specifications - * Updated March 2026 with Gemini 3.1 models + * Updated May 2026 with Gemini 3.5 models */ import { ModelSpec } from '../modelTypes'; export const GOOGLE_MODELS: ModelSpec[] = [ - // Gemini 3.1 models (latest) + // Gemini 3.1 models { provider: 'google', name: 'Gemini 3.1 Pro Preview', @@ -126,4 +126,4 @@ export const GOOGLE_MODELS: ModelSpec[] = [ } ]; -export const GOOGLE_DEFAULT_MODEL = 'gemini-3-pro-preview'; \ No newline at end of file +export const GOOGLE_DEFAULT_MODEL = 'gemini-3-pro-preview'; diff --git a/tests/debug/provider-model-live-smoke.test.ts b/tests/debug/provider-model-live-smoke.test.ts index 24f67d66c..2d35bc274 100644 --- a/tests/debug/provider-model-live-smoke.test.ts +++ b/tests/debug/provider-model-live-smoke.test.ts @@ -231,7 +231,7 @@ async function callModel(target: SmokeTarget): Promise { // Codex currently rejects max_output_tokens on the OAuth endpoint. if (target.provider !== 'openai-codex') { - options.maxTokens = Number(getEnv('MODEL_SMOKE_MAX_TOKENS') || 16); + options.maxTokens = Number(getEnv('MODEL_SMOKE_MAX_TOKENS') || 64); } return adapter.generateUncached( diff --git a/tests/unit/ModelRegistry.test.ts b/tests/unit/ModelRegistry.test.ts index 23fdcdb79..973354d8d 100644 --- a/tests/unit/ModelRegistry.test.ts +++ b/tests/unit/ModelRegistry.test.ts @@ -49,3 +49,25 @@ describe('ModelRegistry GPT-5.5 models', () => { expect(DEFAULT_MODELS['openai-codex']).toBe('gpt-5.5'); }); }); + +describe('ModelRegistry Gemini 3.5 Flash models', () => { + it('registers Gemini 3.5 Flash for Google', () => { + expect(ModelRegistry.findModel('google', 'gemini-3.5-flash')).toEqual(expect.objectContaining({ + name: 'Gemini 3.5 Flash', + contextWindow: 1048576, + maxTokens: 65536, + inputCostPerMillion: 1.5, + outputCostPerMillion: 9 + })); + }); + + it('registers Gemini 3.5 Flash for OpenRouter with the provider namespace', () => { + expect(ModelRegistry.findModel('openrouter', 'google/gemini-3.5-flash')).toEqual(expect.objectContaining({ + name: 'Gemini 3.5 Flash', + contextWindow: 1048576, + maxTokens: 65536, + inputCostPerMillion: 1.5, + outputCostPerMillion: 9 + })); + }); +});