From 6eb9d241f33462950aa39f2968d2ca2b48ffd454 Mon Sep 17 00:00:00 2001 From: 08mamba24 <864701928@qq.com> Date: Mon, 23 Mar 2026 23:55:08 +0800 Subject: [PATCH] fix: preserve OpenRouter model IDs for custom gateways --- docs/openrouter.md | 14 +++- package-lock.json | 3 - package.json | 3 - packages/core/src/core/contentGenerator.ts | 49 ++++++++++++-- .../core/openRouterContentGenerator.test.ts | 65 +++++++++++++++++-- 5 files changed, 116 insertions(+), 18 deletions(-) diff --git a/docs/openrouter.md b/docs/openrouter.md index a7d2559efe7..c493286a0f4 100644 --- a/docs/openrouter.md +++ b/docs/openrouter.md @@ -24,6 +24,12 @@ OpenRouter support allows you to use Gemini models through the OpenRouter API ga export OPENROUTER_BASE_URL="https://your-custom-endpoint.com/api/v1" ``` +4. **Set model explicitly for custom endpoints** + For OpenRouter-compatible gateways (not `openrouter.ai`), set the provider-native model ID: + ```bash + export GEMINI_MODEL="glm-4.7" + ``` + ## Usage ### Interactive Mode @@ -46,7 +52,8 @@ echo "What is the capital of France?" | gemini ## Supported Models -The following Gemini models are available through OpenRouter: +When `OPENROUTER_BASE_URL` is the official `openrouter.ai` endpoint, the CLI +maps these Gemini aliases automatically: - `gemini-2.5-pro` → `google/gemini-2.5-pro` - `gemini-2.5-flash` → `google/gemini-2.5-flash` @@ -59,6 +66,9 @@ The following Gemini models are available through OpenRouter: - `gemini-1.5-pro` → `google/gemini-pro-1.5` - `gemini-1.5-flash` → `google/gemini-flash-1.5` +For custom OpenRouter-compatible endpoints, the CLI sends the model as-is. Use +the exact model ID expected by your provider. + ## Features - ✅ Text generation @@ -83,4 +93,4 @@ echo $OPENROUTER_API_KEY OpenRouter has rate limits based on your account tier. If you encounter rate limit errors, consider upgrading your OpenRouter account or reducing request frequency. ### Model not available -Some Gemini models may have limited availability on OpenRouter. Check the [OpenRouter models page](https://openrouter.ai/models) for current availability. \ No newline at end of file +Some Gemini models may have limited availability on OpenRouter. Check the [OpenRouter models page](https://openrouter.ai/models) for current availability. diff --git a/package-lock.json b/package-lock.json index 05e43bbd80a..90cb42cf12d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,9 +10,6 @@ "workspaces": [ "packages/*" ], - "dependencies": { - "@google/gemini-cli": "^0.1.1" - }, "bin": { "gemini": "bundle/gemini.js" }, diff --git a/package.json b/package.json index e3f670bbf93..bab77ef03c4 100644 --- a/package.json +++ b/package.json @@ -83,8 +83,5 @@ "react-devtools-core": "^4.28.5", "typescript-eslint": "^8.30.1", "yargs": "^17.7.2" - }, - "dependencies": { - "@google/gemini-cli": "^0.1.1" } } diff --git a/packages/core/src/core/contentGenerator.ts b/packages/core/src/core/contentGenerator.ts index e972c3f337c..784d7cffba1 100644 --- a/packages/core/src/core/contentGenerator.ts +++ b/packages/core/src/core/contentGenerator.ts @@ -17,10 +17,40 @@ import { createCodeAssistContentGenerator } from '../code_assist/codeAssist.js'; import { DEFAULT_GEMINI_MODEL } from '../config/models.js'; import { getEffectiveModel } from './modelCheck.js'; +const OPENROUTER_DEFAULT_BASE_URL = 'https://openrouter.ai/api/v1'; + +function isOfficialOpenRouterBaseUrl(baseUrl: string): boolean { + try { + const parsed = new URL(baseUrl); + return ( + parsed.hostname === 'openrouter.ai' || + parsed.hostname.endsWith('.openrouter.ai') + ); + } catch { + return /(^|\/\/)([^/]*\.)?openrouter\.ai(?=\/|$)/i.test(baseUrl); + } +} + +function isGeminiAliasModel(model: string): boolean { + return ( + model === 'gemini-pro' || + model === 'gemini-pro-vision' || + model.startsWith('gemini-') + ); +} + /** * Maps Gemini model names to OpenRouter model IDs */ -function mapGeminiModelToOpenRouter(model: string): string { +function mapGeminiModelToOpenRouter(model: string, baseUrl: string): string { + if (model.includes('/')) { + return model; + } + + if (!isOfficialOpenRouterBaseUrl(baseUrl)) { + return model; + } + const modelMap: Record = { 'gemini-2.5-pro': 'google/gemini-2.5-pro', 'gemini-2.5-flash': 'google/gemini-2.5-flash', @@ -35,7 +65,16 @@ function mapGeminiModelToOpenRouter(model: string): string { 'gemini-1.5-flash': 'google/gemini-flash-1.5', }; - return modelMap[model] || `google/${model}`; + const mappedModel = modelMap[model]; + if (mappedModel) { + return mappedModel; + } + + if (isGeminiAliasModel(model)) { + return `google/${model}`; + } + + return model; } /** @@ -123,12 +162,14 @@ export async function createContentGeneratorConfig( } if (authType === AuthType.USE_OPENROUTER && openRouterApiKey) { + const resolvedOpenRouterBaseUrl = + openRouterBaseUrl || OPENROUTER_DEFAULT_BASE_URL; contentGeneratorConfig.apiKey = openRouterApiKey; - contentGeneratorConfig.openRouterBaseUrl = - openRouterBaseUrl || 'https://openrouter.ai/api/v1'; + contentGeneratorConfig.openRouterBaseUrl = resolvedOpenRouterBaseUrl; // Map Gemini model names to OpenRouter format contentGeneratorConfig.model = mapGeminiModelToOpenRouter( contentGeneratorConfig.model, + resolvedOpenRouterBaseUrl, ); return contentGeneratorConfig; diff --git a/packages/core/src/core/openRouterContentGenerator.test.ts b/packages/core/src/core/openRouterContentGenerator.test.ts index 6395ee317b9..59c681e4172 100644 --- a/packages/core/src/core/openRouterContentGenerator.test.ts +++ b/packages/core/src/core/openRouterContentGenerator.test.ts @@ -4,7 +4,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { describe, it, expect } from 'vitest'; +import { describe, it, expect, beforeEach, afterEach } from 'vitest'; import { createOpenRouterContentGenerator } from './openRouterContentGenerator.js'; import { ContentGeneratorConfig, AuthType } from './contentGenerator.js'; @@ -89,22 +89,75 @@ describe('OpenRouter Content Generator', () => { }); describe('Model mapping', () => { - it('should map Gemini models to OpenRouter format', async () => { - // Set the environment variable for the test + const originalApiKey = process.env.OPENROUTER_API_KEY; + const originalBaseUrl = process.env.OPENROUTER_BASE_URL; + + beforeEach(() => { process.env.OPENROUTER_API_KEY = 'test-key'; + delete process.env.OPENROUTER_BASE_URL; + }); + afterEach(() => { + if (originalApiKey === undefined) { + delete process.env.OPENROUTER_API_KEY; + } else { + process.env.OPENROUTER_API_KEY = originalApiKey; + } + + if (originalBaseUrl === undefined) { + delete process.env.OPENROUTER_BASE_URL; + } else { + process.env.OPENROUTER_BASE_URL = originalBaseUrl; + } + }); + + it('maps Gemini aliases to OpenRouter format for the official endpoint', async () => { const { createContentGeneratorConfig } = await import( './contentGenerator.js' ); - const config = await createContentGeneratorConfig( 'gemini-2.5-flash', AuthType.USE_OPENROUTER, ); expect(config.model).toBe('google/gemini-2.5-flash'); + }); + + it('keeps fully-qualified model IDs unchanged', async () => { + const { createContentGeneratorConfig } = await import( + './contentGenerator.js' + ); + const config = await createContentGeneratorConfig( + 'google/gemini-2.5-flash', + AuthType.USE_OPENROUTER, + ); + + expect(config.model).toBe('google/gemini-2.5-flash'); + }); + + it('does not force non-Gemini models onto google/ for the official endpoint', async () => { + const { createContentGeneratorConfig } = await import( + './contentGenerator.js' + ); + const config = await createContentGeneratorConfig( + 'glm-4.7', + AuthType.USE_OPENROUTER, + ); + + expect(config.model).toBe('glm-4.7'); + }); + + it('passes model IDs through unchanged for custom OpenRouter-compatible endpoints', async () => { + process.env.OPENROUTER_BASE_URL = 'https://open.bigmodel.cn/api/paas/v4'; + + const { createContentGeneratorConfig } = await import( + './contentGenerator.js' + ); + const config = await createContentGeneratorConfig( + 'glm-4.7', + AuthType.USE_OPENROUTER, + ); - // Clean up - delete process.env.OPENROUTER_API_KEY; + expect(config.model).toBe('glm-4.7'); }); });