Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 12 additions & 2 deletions docs/openrouter.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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`
Expand All @@ -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
Expand All @@ -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.
Some Gemini models may have limited availability on OpenRouter. Check the [OpenRouter models page](https://openrouter.ai/models) for current availability.
3 changes: 0 additions & 3 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 0 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
}
49 changes: 45 additions & 4 deletions packages/core/src/core/contentGenerator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, string> = {
'gemini-2.5-pro': 'google/gemini-2.5-pro',
'gemini-2.5-flash': 'google/gemini-2.5-flash',
Expand All @@ -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;
}

/**
Expand Down Expand Up @@ -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;
Expand Down
65 changes: 59 additions & 6 deletions packages/core/src/core/openRouterContentGenerator.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -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');
});
});