diff --git a/electron/main.js b/electron/main.js index c82ceac149..56a00bd3ff 100644 --- a/electron/main.js +++ b/electron/main.js @@ -280,9 +280,14 @@ function installUpdate() { // ── Content Security Policy (#15) ────────────────────────── function setupContentSecurityPolicy() { session.defaultSession.webRequest.onHeadersReceived((details, callback) => { - const scriptSrc = isDev + // Determine if the request is for the Next.js development server + const isDevUrl = details.url.includes("localhost:20128") || details.url.includes("127.0.0.1:20128"); + + // React/Next.js requires unsafe-eval for source maps and Hot Module Replacement (HMR) during development. + const scriptSrc = isDevUrl ? "script-src 'self' 'unsafe-inline' 'unsafe-eval' blob:" : "script-src 'self' 'unsafe-inline' blob:"; + const csp = [ "default-src 'self'", "base-uri 'self'", @@ -291,6 +296,7 @@ function setupContentSecurityPolicy() { "frame-src 'none'", "child-src 'none'", "form-action 'self'", + `connect-src 'self' http://localhost:* ws://localhost:* wss://localhost:* https://*.omniroute.online https://*.omniroute.dev`, scriptSrc, "script-src-attr 'none'", "style-src 'self' 'unsafe-inline' https://fonts.googleapis.com", diff --git a/open-sse/config/providerRegistry.ts b/open-sse/config/providerRegistry.ts index c8a126c186..30dc62f580 100644 --- a/open-sse/config/providerRegistry.ts +++ b/open-sse/config/providerRegistry.ts @@ -704,8 +704,8 @@ export const REGISTRY: Record = { const action = stream ? "streamGenerateContent?alt=sse" : "generateContent"; return `${base}:${action}`; }, - authType: "oauth", - authHeader: "bearer", + authType: "apikey", + authHeader: "x-goog-api-key", defaultContextLength: 1048576, oauth: { clientIdEnv: "GEMINI_CLI_OAUTH_CLIENT_ID", @@ -714,6 +714,8 @@ export const REGISTRY: Record = { clientSecretDefault: resolvePublicCred("gemini_alt"), }, models: [ + { id: "gemini-1.5-pro", name: "Gemini 1.5 Pro" }, + { id: "gemini-1.5-flash", name: "Gemini 1.5 Flash" }, { id: "gemini-3.1-pro-preview", name: "Gemini 3.1 Pro Preview" }, { id: "gemini-3.1-pro-preview-customtools", name: "Gemini 3.1 Pro Preview Custom Tools" }, { id: "gemini-3-flash-preview", name: "Gemini 3 Flash Preview" }, diff --git a/open-sse/executors/gemini-cli.ts b/open-sse/executors/gemini-cli.ts index 1b77fc4b5b..ea82347db0 100644 --- a/open-sse/executors/gemini-cli.ts +++ b/open-sse/executors/gemini-cli.ts @@ -133,11 +133,24 @@ export class GeminiCLIExecutor extends BaseExecutor { model?: string ) { void clientHeaders; + + // Fallback to internal tracker if model not explicitly passed (matches older interface calls) + const activeModel = model || this._currentModel || "unknown"; + const raw = getGeminiCliHeaders( - normalizeGeminiModel(model || "unknown"), + normalizeGeminiModel(activeModel), credentials.accessToken, stream ? "*/*" : "application/json" ); + + if (credentials.apiKey) { + raw["x-goog-api-key"] = credentials.apiKey; + // getGeminiCliHeaders adds Authorization: Bearer undefined if accessToken is empty, so we clean it up + if (!credentials.accessToken) { + delete raw["Authorization"]; + } + } + return scrubProxyAndFingerprintHeaders(raw); } diff --git a/src/lib/modelAliasSeed.ts b/src/lib/modelAliasSeed.ts index c3848d1ed8..f4c77b1fae 100644 --- a/src/lib/modelAliasSeed.ts +++ b/src/lib/modelAliasSeed.ts @@ -1,12 +1,15 @@ import { getModelAliases, setModelAlias } from "@/lib/db/models"; export const DEFAULT_MODEL_ALIAS_SEED = Object.freeze({ - "gemini-3-pro-high": "antigravity/gemini-3-pro-preview", - "gemini-3-pro-low": "antigravity/gemini-3.1-pro-low", - "gemini-3-pro-preview": "antigravity/gemini-3-pro-preview", - "gemini-3.1-pro-preview": "antigravity/gemini-3-pro-preview", - "gemini-3.1-pro-preview-customtools": "antigravity/gemini-3-pro-preview", - "gemini-3-flash-preview": "antigravity/gemini-3-flash-preview", + "gemini-1.5-pro": "gemini-cli/gemini-1.5-pro", + "gemini-1.5-flash": "gemini-cli/gemini-1.5-flash", + "gemini-3-pro-high": "gemini-cli/gemini-3.1-pro-preview", + "gemini-3-pro-low": "gemini-cli/gemini-3.1-flash-lite-preview", + "gemini-3-pro-preview": "gemini-cli/gemini-3.1-pro-preview", + "gemini-3.1-pro-preview": "gemini-cli/gemini-3.1-pro-preview", + "gemini-3.1-pro-preview-customtools": "gemini-cli/gemini-3.1-pro-preview-customtools", + "gemini-3-flash-preview": "gemini-cli/gemini-3-flash-preview", + "gemini-3.1-flash-lite-preview": "gemini-cli/gemini-3.1-flash-lite-preview", }); type SeedLogger = { diff --git a/src/lib/providers/validation.ts b/src/lib/providers/validation.ts index 876eb3933d..9ecb7ea949 100644 --- a/src/lib/providers/validation.ts +++ b/src/lib/providers/validation.ts @@ -732,14 +732,31 @@ async function validateGeminiLikeProvider({ isLocal = false, }: any) { try { + if (!baseUrl) { + return { valid: false, error: "Missing base URL" }; + } + const requestUrl = typeof providerSpecificData?.modelsUrl === "string" && providerSpecificData.modelsUrl.trim() !== "" ? providerSpecificData.modelsUrl.trim() : `${baseUrl}/models`; + const urlWithKey = authType === "query" ? `${requestUrl}?key=${encodeURIComponent(apiKey)}` : requestUrl; - const headers = authType === "header" ? { "x-goog-api-key": apiKey } : {}; + + // Use the correct auth header based on provider config: + // - gemini / gemini-cli (API key): x-goog-api-key + // - gemini-cli (OAuth): Bearer token + const headers: Record = {}; + + if (authType === "header" || authType === "apikey") { + headers["x-goog-api-key"] = apiKey; + } else if (authType === "oauth" || apiKey.startsWith("ya29.")) { + headers["Authorization"] = `Bearer ${apiKey}`; + } + + applyCustomUserAgent(headers, providerSpecificData); const response = await validationRead( urlWithKey, @@ -749,10 +766,6 @@ async function validateGeminiLikeProvider({ isLocal ); - if (!baseUrl) { - return { valid: false, error: "Missing base URL" }; - } - if (response.ok) { return { valid: true, error: null }; } diff --git a/tests/unit/error-classification.test.ts b/tests/unit/error-classification.test.ts index 0f6be74c08..cad77e5793 100644 --- a/tests/unit/error-classification.test.ts +++ b/tests/unit/error-classification.test.ts @@ -18,11 +18,11 @@ test("getProviderCategory: OAuth providers return 'oauth'", () => { assert.equal(getProviderCategory("antigravity"), "oauth"); assert.equal(getProviderCategory("cursor"), "oauth"); assert.equal(getProviderCategory("kiro"), "oauth"); - assert.equal(getProviderCategory("gemini-cli"), "oauth"); assert.equal(getProviderCategory("cline"), "oauth"); }); test("getProviderCategory: API key providers return 'apikey'", () => { + assert.equal(getProviderCategory("gemini-cli"), "apikey"); assert.equal(getProviderCategory("groq"), "apikey"); assert.equal(getProviderCategory("fireworks"), "apikey"); assert.equal(getProviderCategory("cerebras"), "apikey"); diff --git a/tests/unit/executor-gemini-cli.test.ts b/tests/unit/executor-gemini-cli.test.ts index 34ca694920..3dbd1f7f09 100644 --- a/tests/unit/executor-gemini-cli.test.ts +++ b/tests/unit/executor-gemini-cli.test.ts @@ -40,6 +40,17 @@ test("GeminiCLIExecutor.buildUrl and buildHeaders match the native Gemini CLI fi ); assert.equal(headers.Authorization, "Bearer gcli-token"); assert.equal(headers.Accept, "*/*"); + + const apiKeyHeaders = executor.buildHeaders( + { apiKey: "gcli-api-key" }, + false, + undefined, + "models/gemini-2.5-flash" + ); + assert.equal(apiKeyHeaders["x-goog-api-key"], "gcli-api-key"); + assert.equal(apiKeyHeaders.Authorization, undefined); + assert.equal(apiKeyHeaders.Accept, "application/json"); + assert.match( headers["User-Agent"], new RegExp( @@ -108,6 +119,14 @@ test("GeminiCLIExecutor.refreshProject caches loadCodeAssist lookups and transfo assert.equal(second, "fresh-project-id"); assert.equal(calls, 1); assert.equal(transformed.project, "fresh-project-id"); + + const apiKeyTransformed = await executor.transformRequest( + "gemini-2.5-flash", + { request: { contents: [] } }, + true, + { apiKey: "gcli-api-key" } + ); + assert.equal(apiKeyTransformed.project, undefined); } finally { globalThis.fetch = originalFetch; } diff --git a/tests/unit/provider-validation-branches.test.ts b/tests/unit/provider-validation-branches.test.ts index 83737d9711..a54b8aaa5c 100644 --- a/tests/unit/provider-validation-branches.test.ts +++ b/tests/unit/provider-validation-branches.test.ts @@ -408,7 +408,7 @@ test("gemini validation rejects invalid keys via UNAUTHENTICATED status", async assert.equal(result.error, "Invalid API key"); }); -test("gemini-cli validation uses Bearer auth (OAuth)", async () => { +test("gemini-cli validation uses x-goog-api-key by default", async () => { const calls = []; globalThis.fetch = async (url, init) => { calls.push({ url: String(url), headers: init?.headers }); @@ -422,12 +422,34 @@ test("gemini-cli validation uses Bearer auth (OAuth)", async () => { const result = await validateProviderApiKey({ provider: "gemini-cli", - apiKey: "oauth-access-token", + apiKey: "gcli-api-key", }); assert.equal(result.valid, true); assert.equal(result.error, null); assert.equal(calls.length, 1); - assert.equal(calls[0].headers["Authorization"], "Bearer oauth-access-token"); - assert.equal(calls[0].headers["x-goog-api-key"], undefined); + assert.equal(calls[0].headers["x-goog-api-key"], "gcli-api-key"); + assert.equal(calls[0].headers["Authorization"], undefined); +}); + +test("gemini-cli validation supports Bearer auth if token starts with ya29.", async () => { + const calls = []; + globalThis.fetch = async (url, init) => { + calls.push({ url: String(url), headers: init?.headers }); + return new Response( + JSON.stringify({ + models: [{ name: "models/gemini-2.5-flash" }], + }), + { status: 200 } + ); + }; + + const result = await validateProviderApiKey({ + provider: "gemini-cli", + apiKey: "ya29.oauth-access-token", + }); + + assert.equal(result.valid, true); + assert.equal(calls.length, 1); + assert.equal(calls[0].headers["Authorization"], "Bearer ya29.oauth-access-token"); });