From b2f41739ef97ac1fa35e8c5502d42f62fb172465 Mon Sep 17 00:00:00 2001 From: Peter Dave Hello Date: Tue, 14 Jul 2026 00:02:35 +0800 Subject: [PATCH] Add Google Gemini as an OpenAI-compatible API provider Register Google as a built-in provider using the Gemini OpenAI-compatible endpoint. Add curated Gemini model presets, provider credential mapping, and background routing through the shared API path. Reuse supported Bearer authorization so API keys stay out of request URLs. Limit legacy resolution to configured Google preset keys so raw model IDs are not truncated by custom model parsing. --- src/background/index.mjs | 2 + src/config/index.mjs | 43 +++++++++++++++++++ src/config/openai-provider-mappings.mjs | 2 + src/services/apis/provider-registry.mjs | 9 ++++ .../services/apis/provider-registry.test.mjs | 10 +++++ 5 files changed, 66 insertions(+) diff --git a/src/background/index.mjs b/src/background/index.mjs index aa41ecd4f..73059d31d 100644 --- a/src/background/index.mjs +++ b/src/background/index.mjs @@ -30,6 +30,7 @@ import { isUsingOpenRouterApiModel, isUsingAimlApiModel, isUsingDeepSeekApiModel, + isUsingGoogleApiModel, } from '../config/index.mjs' import '../_locales/i18n' import { openUrl } from '../utils/open-url' @@ -345,6 +346,7 @@ function isUsingOpenAICompatibleApiSession(session) { isUsingOllamaApiModel(session) || isUsingOpenRouterApiModel(session) || isUsingAimlApiModel(session) || + isUsingGoogleApiModel(session) || isUsingGptCompletionApiModel(session) ) } diff --git a/src/config/index.mjs b/src/config/index.mjs index b7cf428af..e1a22f822 100644 --- a/src/config/index.mjs +++ b/src/config/index.mjs @@ -179,6 +179,14 @@ export const aimlApiModelKeys = [ 'aiml_deepseek_v4_pro', 'aiml_deepseek_v4_flash', ] +export const googleApiModelKeys = [ + 'googleGemini3_1Pro', + 'googleGemini3_5Flash', + 'googleGemini3Flash', + 'googleGemini2_5Pro', + 'googleGemini2_5Flash', + 'googleGemini2_5FlashLite', +] export const AlwaysCustomGroups = [ 'ollamaApiModelKeys', @@ -253,6 +261,10 @@ export const ModelGroups = { value: aimlApiModelKeys, desc: 'AI/ML (API)', }, + googleApiModelKeys: { + value: googleApiModelKeys, + desc: 'Google (API)', + }, customApiModelKeys: { value: customApiModelKeys, desc: 'Custom Model', @@ -605,6 +617,30 @@ export const Models = { value: 'deepseek/deepseek-v4-flash', desc: 'AIML (DeepSeek V4 Flash)', }, + googleGemini3_1Pro: { + value: 'gemini-3.1-pro-preview', + desc: 'Google (Gemini 3.1 Pro Preview)', + }, + googleGemini3_5Flash: { + value: 'gemini-3.5-flash', + desc: 'Google (Gemini 3.5 Flash)', + }, + googleGemini3Flash: { + value: 'gemini-3-flash-preview', + desc: 'Google (Gemini 3 Flash Preview)', + }, + googleGemini2_5Pro: { + value: 'gemini-2.5-pro', + desc: 'Google (Gemini 2.5 Pro)', + }, + googleGemini2_5Flash: { + value: 'gemini-2.5-flash', + desc: 'Google (Gemini 2.5 Flash)', + }, + googleGemini2_5FlashLite: { + value: 'gemini-2.5-flash-lite', + desc: 'Google (Gemini 2.5 Flash-Lite)', + }, } for (const modelName in Models) { @@ -673,6 +709,7 @@ export const defaultConfig = { openRouterApiKey: '', aimlApiKey: '', + googleApiKey: '', // advanced @@ -711,6 +748,8 @@ export const defaultConfig = { 'claudeOpus48Api', 'claudeSonnet5Api', 'claudeHaiku45Api', + 'googleGemini3_1Pro', + 'googleGemini3_5Flash', 'openRouter_auto', 'openRouter_free', ], @@ -868,6 +907,10 @@ export function isUsingAimlApiModel(configOrSession) { return isInApiModeGroup(aimlApiModelKeys, configOrSession) } +export function isUsingGoogleApiModel(configOrSession) { + return isInApiModeGroup(googleApiModelKeys, configOrSession) +} + export function isUsingChatGLMApiModel(configOrSession) { return isInApiModeGroup(chatglmApiModelKeys, configOrSession) } diff --git a/src/config/openai-provider-mappings.mjs b/src/config/openai-provider-mappings.mjs index b7a534875..57c0485a3 100644 --- a/src/config/openai-provider-mappings.mjs +++ b/src/config/openai-provider-mappings.mjs @@ -6,6 +6,7 @@ export const LEGACY_API_KEY_FIELD_BY_PROVIDER_ID = { aiml: 'aimlApiKey', chatglm: 'chatglmApiKey', ollama: 'ollamaApiKey', + google: 'googleApiKey', 'legacy-custom-default': 'customApiKey', } @@ -26,5 +27,6 @@ export const OPENAI_COMPATIBLE_GROUP_TO_PROVIDER_ID = { aimlApiModelKeys: 'aiml', chatglmApiModelKeys: 'chatglm', ollamaApiModelKeys: 'ollama', + googleApiModelKeys: 'google', customApiModelKeys: 'legacy-custom-default', } diff --git a/src/services/apis/provider-registry.mjs b/src/services/apis/provider-registry.mjs index 249e59ae8..72aa47662 100644 --- a/src/services/apis/provider-registry.mjs +++ b/src/services/apis/provider-registry.mjs @@ -70,6 +70,14 @@ const BUILTIN_PROVIDER_TEMPLATE = [ builtin: true, enabled: true, }, + { + id: 'google', + name: 'Google', + baseUrl: 'https://generativelanguage.googleapis.com/v1beta/openai', + chatCompletionsPath: '/chat/completions', + builtin: true, + enabled: true, + }, { id: 'legacy-custom-default', name: 'Custom Model (Legacy)', @@ -111,6 +119,7 @@ function resolveProviderIdFromLegacyModelName(modelName) { return 'ollama' } if (preset.startsWith('chatglm') || preset === 'chatglmApiModelKeys') return 'chatglm' + if (preset.startsWith('googleGemini') || preset === 'googleApiModelKeys') return 'google' if (preset === 'customApiModelKeys') return 'legacy-custom-default' return null diff --git a/tests/unit/services/apis/provider-registry.test.mjs b/tests/unit/services/apis/provider-registry.test.mjs index 858f3e5a8..29d949814 100644 --- a/tests/unit/services/apis/provider-registry.test.mjs +++ b/tests/unit/services/apis/provider-registry.test.mjs @@ -6,6 +6,7 @@ import { getProviderById, resolveEndpointTypeForSession, resolveOpenAICompatibleRequest, + resolveProviderIdForSession, } from '../../../../src/services/apis/provider-registry.mjs' test('resolveEndpointTypeForSession prefers apiMode when present', () => { @@ -40,6 +41,15 @@ test('resolveEndpointTypeForSession falls back to legacy modelName when apiMode assert.equal(resolveEndpointTypeForSession(session), 'completion') }) +test('resolveProviderIdForSession resolves legacy Google preset keys', () => { + assert.equal(resolveProviderIdForSession({ modelName: 'googleGemini2_5Flash' }), 'google') + assert.equal(resolveProviderIdForSession({ modelName: 'googleApiModelKeys-custom' }), 'google') +}) + +test('resolveProviderIdForSession does not treat raw Gemini model IDs as legacy presets', () => { + assert.equal(resolveProviderIdForSession({ modelName: 'gemini-2.5-flash' }), null) +}) + test('resolveOpenAICompatibleRequest resolves custom provider from normalized session provider id', () => { const config = { customOpenAIProviders: [