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
3 changes: 3 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@ PRODUCTION_TEST_MODE=true
TEST_GPT5_API_KEY=your_gpt5_api_key_here
TEST_GPT5_BASE_URL=http://127.0.0.1:3000/openai

# OpenRouter Provider Configuration
OPENROUTER_API_KEY=your_openrouter_api_key_here

# MiniMax Codex Test Configuration
TEST_MINIMAX_API_KEY=your_minimax_api_key_here
TEST_MINIMAX_BASE_URL=https://api.minimaxi.com/v1
Expand Down
34 changes: 34 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -322,6 +322,38 @@ You can use the onboarding to set up the model, or `/model`.
If you don't see the models you want on the list, you can manually set them in `/config`
As long as you have an openai-like endpoint, it should work.

### OpenRouter setup

Kode includes OpenRouter as an OpenAI-compatible provider. Create an API key in [OpenRouter](https://openrouter.ai/settings/keys), then choose OpenRouter from `/model` and select or enter any OpenRouter model ID, for example:

```bash
export OPENROUTER_API_KEY=sk-or-v1-...
```

```yaml
version: 1
profiles:
- name: OpenRouter Claude Sonnet
provider: openrouter
modelName: anthropic/claude-sonnet-4.5
baseURL: https://openrouter.ai/api/v1
maxTokens: 8192
contextLength: 200000
apiKey:
fromEnv: OPENROUTER_API_KEY
pointers:
main: anthropic/claude-sonnet-4.5
task: anthropic/claude-sonnet-4.5
compact: anthropic/claude-sonnet-4.5
quick: anthropic/claude-sonnet-4.5
```

Import the profile with:

```bash
kode models import kode-openrouter.yaml
```

### Commands

- `/help` - Show available commands
Expand Down Expand Up @@ -516,6 +548,8 @@ pointers:
quick: gpt-4o
```

For OpenRouter, use `provider: openrouter`, `baseURL: https://openrouter.ai/api/v1`, and `apiKey.fromEnv: OPENROUTER_API_KEY`.

#### 2. **TaskTool Intelligent Task Distribution**
Our specially designed `TaskTool` (Architect tool) implements:
- **Subagent Mechanism**: Can launch multiple sub-agents to process tasks in parallel
Expand Down
25 changes: 25 additions & 0 deletions docs/develop/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,7 @@ kode config list -g
```bash
# API Keys
OPENAI_API_KEY=sk-...
OPENROUTER_API_KEY=sk-or-v1-...

# Model Selection
CLAUDE_MODEL=claude-3-5-sonnet-20241022
Expand Down Expand Up @@ -361,6 +362,30 @@ Temporary for current session:

### Custom Model Providers

#### OpenRouter

OpenRouter is available as an OpenAI-compatible provider. Use the `/model` selector and choose OpenRouter, or import a model profile:

```yaml
version: 1
profiles:
- name: OpenRouter Claude Sonnet
provider: openrouter
modelName: anthropic/claude-sonnet-4.5
baseURL: https://openrouter.ai/api/v1
maxTokens: 8192
contextLength: 200000
apiKey:
fromEnv: OPENROUTER_API_KEY
pointers:
main: anthropic/claude-sonnet-4.5
task: anthropic/claude-sonnet-4.5
compact: anthropic/claude-sonnet-4.5
quick: anthropic/claude-sonnet-4.5
```

OpenRouter model IDs use the `provider/model` format shown in the [OpenRouter model list](https://openrouter.ai/models).

```json
{
"modelProfiles": {
Expand Down
1 change: 1 addition & 0 deletions src/core/config/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,7 @@ export type ProviderType =
| 'opendev'
| 'xai'
| 'groq'
| 'openrouter'
| 'gemini'
| 'ollama'
| 'azure'
Expand Down
11 changes: 7 additions & 4 deletions src/core/config/validator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,22 +95,26 @@ export function validateAndRepairGPT5Profile(
if (
profile.provider !== 'openai' &&
profile.provider !== 'custom-openai' &&
profile.provider !== 'openrouter' &&
profile.provider !== 'azure'
) {
debugLogger.warn('GPT5_CONFIG_UNEXPECTED_PROVIDER', {
model: profile.modelName,
provider: profile.provider,
expectedProviders: ['openai', 'custom-openai', 'azure'],
expectedProviders: ['openai', 'custom-openai', 'openrouter', 'azure'],
})
}

if (profile.modelName.includes('gpt-5') && !profile.baseURL) {
repairedProfile.baseURL = 'https://api.openai.com/v1'
repairedProfile.baseURL =
profile.provider === 'openrouter'
? 'https://openrouter.ai/api/v1'
: 'https://api.openai.com/v1'
wasRepaired = true
debugLogger.state('GPT5_CONFIG_AUTO_REPAIR', {
model: profile.modelName,
field: 'baseURL',
value: 'https://api.openai.com/v1',
value: repairedProfile.baseURL,
})
}
}
Expand Down Expand Up @@ -210,4 +214,3 @@ export function createGPT5ModelProfile(

return profile
}

2 changes: 2 additions & 0 deletions src/services/ai/openai.ts
Original file line number Diff line number Diff line change
Expand Up @@ -559,6 +559,7 @@ export async function getCompletionWithProfile(
'mistral',
'xai',
'groq',
'openrouter',
'custom-openai',
].includes(provider)

Expand Down Expand Up @@ -716,6 +717,7 @@ export async function getCompletionWithProfile(
'mistral',
'xai',
'groq',
'openrouter',
'custom-openai',
].includes(provider)

Expand Down
2 changes: 2 additions & 0 deletions src/ui/components/model-selector/ModelSelector.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -603,6 +603,7 @@ export function ModelSelector({
'glm',
'minimax',
'baidu-qianfan',
'openrouter',
'custom-openai',
].includes(selectedProvider)

Expand Down Expand Up @@ -938,6 +939,7 @@ export function ModelSelector({
'mistral',
'xai',
'groq',
'openrouter',
'custom-openai',
].includes(selectedProvider)

Expand Down
2 changes: 2 additions & 0 deletions src/utils/model/modelConfigYaml.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,8 @@ function suggestedApiKeyEnvForProvider(provider: string): string | undefined {
case 'openai':
case 'custom-openai':
return 'OPENAI_API_KEY'
case 'openrouter':
return 'OPENROUTER_API_KEY'
case 'azure':
return 'AZURE_OPENAI_API_KEY'
case 'gemini':
Expand Down
20 changes: 20 additions & 0 deletions tests/unit/config-validator-openrouter.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { describe, expect, test } from 'bun:test'
import { validateAndRepairGPT5Profile } from '../../src/core/config/validator'

describe('OpenRouter GPT-5 config validation', () => {
test('repairs missing GPT-5 baseURL to OpenRouter for OpenRouter profiles', () => {
const repaired = validateAndRepairGPT5Profile({
name: 'OpenRouter GPT-5',
provider: 'openrouter',
modelName: 'openai/gpt-5',
apiKey: 'test-key',
maxTokens: 8192,
contextLength: 128000,
isActive: true,
createdAt: 1,
})

expect(repaired.baseURL).toBe('https://openrouter.ai/api/v1')
expect(repaired.validationStatus).toBe('auto_repaired')
})
})
31 changes: 31 additions & 0 deletions tests/unit/model-config-yaml.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,37 @@ describe('modelConfigYaml', () => {
expect(yamlText).toContain('fromEnv')
})

test('export uses OPENROUTER_API_KEY for OpenRouter profiles', () => {
const config: any = {
modelProfiles: [
{
name: 'OpenRouter Main',
provider: 'openrouter',
modelName: 'anthropic/claude-sonnet-4.5',
baseURL: 'https://openrouter.ai/api/v1',
apiKey: 'SECRET_KEY_SHOULD_NOT_APPEAR',
maxTokens: 8192,
contextLength: 200000,
isActive: true,
createdAt: 1,
},
],
modelPointers: {
main: 'anthropic/claude-sonnet-4.5',
task: 'anthropic/claude-sonnet-4.5',
compact: 'anthropic/claude-sonnet-4.5',
quick: 'anthropic/claude-sonnet-4.5',
},
}

const yamlText = formatModelConfigYamlForSharing(config)

expect(yamlText).toContain('provider: openrouter')
expect(yamlText).toContain('baseURL: https://openrouter.ai/api/v1')
expect(yamlText).toContain('fromEnv: OPENROUTER_API_KEY')
expect(yamlText).not.toContain('SECRET_KEY_SHOULD_NOT_APPEAR')
})

test('import resolves apiKey from env and applies pointers', () => {
process.env.TEST_OPENAI_KEY = 'resolved-from-env'

Expand Down