fix openrouter

This commit is contained in:
x1xhlol
2026-04-01 18:32:03 +02:00
Unverified
parent b181c9925d
commit 237fcb0f35
7 changed files with 94 additions and 23 deletions

View File

@@ -64,7 +64,7 @@ Default endpoints:
- OpenAI base URL: `https://api.openai.com/v1` - OpenAI base URL: `https://api.openai.com/v1`
- OpenAI websocket mode endpoint: `wss://api.openai.com/v1/responses` - OpenAI websocket mode endpoint: `wss://api.openai.com/v1/responses`
- OpenRouter base URL: `https://openrouter.ai/api/v1` - OpenRouter Anthropic-compatible base URL: `https://openrouter.ai/api`
- OpenRouter Responses API: `https://openrouter.ai/api/v1/responses` - OpenRouter Responses API: `https://openrouter.ai/api/v1/responses`
## Quick Start ## Quick Start
@@ -91,7 +91,7 @@ OpenRouter:
```bash ```bash
BETTER_CLAWD_API_PROVIDER=openrouter BETTER_CLAWD_API_PROVIDER=openrouter
OPENROUTER_API_KEY=your_key_here OPENROUTER_API_KEY=your_key_here
OPENROUTER_BASE_URL=https://openrouter.ai/api/v1 OPENROUTER_BASE_URL=https://openrouter.ai/api
``` ```
## What You Get ## What You Get

View File

@@ -1,6 +1,6 @@
{ {
"name": "better-clawd", "name": "better-clawd",
"version": "0.1.4", "version": "0.1.5",
"description": "Claude Code, but better.", "description": "Claude Code, but better.",
"type": "module", "type": "module",
"bin": { "bin": {

View File

@@ -45,8 +45,8 @@ export function OpenRouterLoginFlow({
<Text>Configuring OpenRouter login for Better-Clawd...</Text> <Text>Configuring OpenRouter login for Better-Clawd...</Text>
</Box> </Box>
<Text dimColor={true}> <Text dimColor={true}>
OpenRouter support uses your OpenRouter API key with the Responses API OpenRouter support uses your OpenRouter API key with the
endpoint. Anthropic-compatible Messages API endpoint.
</Text> </Text>
</Box> </Box>
) )
@@ -59,8 +59,8 @@ export function OpenRouterLoginFlow({
'Better-Clawd can use OpenRouter with your OpenRouter API key.'} 'Better-Clawd can use OpenRouter with your OpenRouter API key.'}
</Text> </Text>
<Text dimColor={true}> <Text dimColor={true}>
Paste your OpenRouter key to use `https://openrouter.ai/api/v1` and the Paste your OpenRouter key to use the Anthropic-compatible OpenRouter base
Responses API compatibility layer. URL at `https://openrouter.ai/api`.
</Text> </Text>
<Box> <Box>
<Text>Paste your OpenRouter API key:</Text> <Text>Paste your OpenRouter API key:</Text>

View File

@@ -9,6 +9,7 @@ import {
truncateToWidth, truncateToWidth,
truncateToWidthNoEllipsis, truncateToWidthNoEllipsis,
} from './format.js' } from './format.js'
import { getAPIProvider } from './model/providers.js'
import { getStoredChangelogFromMemory, parseChangelog } from './releaseNotes.js' import { getStoredChangelogFromMemory, parseChangelog } from './releaseNotes.js'
import { gt } from './semver.js' import { gt } from './semver.js'
import { loadMessageLogs } from './sessionStorage.js' import { loadMessageLogs } from './sessionStorage.js'
@@ -253,9 +254,20 @@ export function getLogoDisplayData(): {
const cwd = serverUrl const cwd = serverUrl
? `${displayPath} in ${serverUrl.replace(/^https?:\/\//, '')}` ? `${displayPath} in ${serverUrl.replace(/^https?:\/\//, '')}`
: displayPath : displayPath
const apiProvider = getAPIProvider()
const billingType = isClaudeAISubscriber() const billingType = isClaudeAISubscriber()
? getSubscriptionName() ? getSubscriptionName()
: 'API Usage Billing' : apiProvider === 'openrouter'
? 'OpenRouter'
: apiProvider === 'openai'
? 'OpenAI'
: apiProvider === 'bedrock'
? 'AWS Bedrock'
: apiProvider === 'vertex'
? 'Google Vertex AI'
: apiProvider === 'foundry'
? 'Microsoft Foundry'
: 'API Usage Billing'
const agentName = getInitialSettings().agent const agentName = getInitialSettings().agent
return { return {

View File

@@ -25,19 +25,20 @@ const MODEL_KEYS = Object.keys(ALL_MODEL_CONFIGS) as ModelKey[]
function getBuiltinModelStrings(provider: APIProvider): ModelStrings { function getBuiltinModelStrings(provider: APIProvider): ModelStrings {
if (provider === 'openai') { if (provider === 'openai') {
const out = getBuiltinModelStrings('firstParty') as Record<string, string> const out = getBuiltinModelStrings('firstParty') as Record<string, string>
out.haiku35 = process.env.OPENAI_HAIKU_MODEL || 'gpt-5.4-mini'
out.haiku45 = process.env.OPENAI_HAIKU_MODEL || 'gpt-5.4-mini' out.haiku45 = process.env.OPENAI_HAIKU_MODEL || 'gpt-5.4-mini'
out.sonnet37 = process.env.OPENAI_SONNET_MODEL || 'gpt-5.4'
out.sonnet40 = process.env.OPENAI_SONNET_MODEL || 'gpt-5.4'
out.sonnet45 = process.env.OPENAI_SONNET_MODEL || 'gpt-5.4'
out.sonnet46 = process.env.OPENAI_SONNET_MODEL || 'gpt-5.4' out.sonnet46 = process.env.OPENAI_SONNET_MODEL || 'gpt-5.4'
out.opus40 = process.env.OPENAI_OPUS_MODEL || 'gpt-5.4'
out.opus41 = process.env.OPENAI_OPUS_MODEL || 'gpt-5.4'
out.opus45 = process.env.OPENAI_OPUS_MODEL || 'gpt-5.4'
out.opus46 = process.env.OPENAI_OPUS_MODEL || 'gpt-5.4' out.opus46 = process.env.OPENAI_OPUS_MODEL || 'gpt-5.4'
return out as ModelStrings return out as ModelStrings
} }
if (provider === 'openrouter') {
const out = getBuiltinModelStrings('firstParty') as Record<string, string>
out.sonnet46 =
process.env.OPENROUTER_SONNET_MODEL || 'anthropic/claude-sonnet-4.6'
process.env.OPENROUTER_OPUS_MODEL || 'anthropic/claude-opus-4.6'
return out as ModelStrings
}
const out = {} as ModelStrings const out = {} as ModelStrings
for (const key of MODEL_KEYS) { for (const key of MODEL_KEYS) {
out[key] = ALL_MODEL_CONFIGS[key][provider] out[key] = ALL_MODEL_CONFIGS[key][provider]

View File

@@ -9,6 +9,40 @@ export type APIProvider =
| 'vertex' | 'vertex'
| 'foundry' | 'foundry'
function getStoredProviderPreference(): APIProvider | null {
try {
// Read the global config file directly so provider selection works even
// before the guarded config loader is enabled during startup.
// eslint-disable-next-line @typescript-eslint/no-require-imports
const { readFileSync } = require('fs') as typeof import('fs')
// eslint-disable-next-line @typescript-eslint/no-require-imports
const { getGlobalClaudeFile } =
require('../env.js') as typeof import('../env.js')
const raw = readFileSync(getGlobalClaudeFile(), 'utf8')
const config = JSON.parse(raw) as {
authProvider?: 'anthropic' | 'openrouter' | 'openai'
openRouterApiKey?: string
openAiApiKey?: string
openAiAccessToken?: string
}
switch (config.authProvider) {
case 'openrouter':
return config.openRouterApiKey ? 'openrouter' : null
case 'openai':
return config.openAiApiKey || config.openAiAccessToken
? 'openai'
: null
case 'anthropic':
return 'firstParty'
default:
return null
}
} catch {
return null
}
}
function getExplicitProviderOverride(): APIProvider | null { function getExplicitProviderOverride(): APIProvider | null {
const rawProvider = const rawProvider =
process.env.BETTER_CLAWD_API_PROVIDER ?? process.env.BETTER_CLAWD_API_PROVIDER ??
@@ -65,7 +99,30 @@ export function isOpenAIConfigured(): boolean {
} }
export function getOpenRouterBaseUrl(): string { export function getOpenRouterBaseUrl(): string {
return process.env.OPENROUTER_BASE_URL ?? 'https://openrouter.ai/api/v1' const configuredBaseUrl = process.env.OPENROUTER_BASE_URL
const fallbackBaseUrl = 'https://openrouter.ai/api'
if (!configuredBaseUrl) {
return fallbackBaseUrl
}
try {
const url = new URL(configuredBaseUrl)
if (url.host === 'openrouter.ai') {
const normalizedPath = url.pathname.replace(/\/+$/, '')
if (normalizedPath === '' || normalizedPath === '/') {
url.pathname = '/api'
} else if (normalizedPath === '/api/v1') {
// Anthropic SDK appends /v1/messages itself, so OpenRouter's SDK base
// must stop at /api rather than /api/v1.
url.pathname = '/api'
}
}
return url.toString().replace(/\/$/, '')
} catch {
return configuredBaseUrl
}
} }
export function getOpenAIBaseUrl(): string { export function getOpenAIBaseUrl(): string {
@@ -88,7 +145,7 @@ export function getAPIProvider(): APIProvider {
? 'openai' ? 'openai'
: isOpenRouterConfigured() : isOpenRouterConfigured()
? 'openrouter' ? 'openrouter'
: 'firstParty' : getStoredProviderPreference() ?? 'firstParty'
} }
export function getAPIProviderForStatsig(): AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS { export function getAPIProviderForStatsig(): AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS {

View File

@@ -11,7 +11,11 @@ import { getDisplayPath } from './file.js';
import { formatNumber } from './format.js'; import { formatNumber } from './format.js';
import { getIdeClientName, type IDEExtensionInstallationStatus, isJetBrainsIde, toIDEDisplayName } from './ide.js'; import { getIdeClientName, type IDEExtensionInstallationStatus, isJetBrainsIde, toIDEDisplayName } from './ide.js';
import { getClaudeAiUserDefaultModelDescription, modelDisplayString } from './model/model.js'; import { getClaudeAiUserDefaultModelDescription, modelDisplayString } from './model/model.js';
import { getAPIProvider } from './model/providers.js'; import {
getAPIProvider,
getOpenAIBaseUrl,
getOpenRouterBaseUrl,
} from './model/providers.js';
import { getMTLSConfig } from './mtls.js'; import { getMTLSConfig } from './mtls.js';
import { checkInstall } from './nativeInstaller/index.js'; import { checkInstall } from './nativeInstaller/index.js';
import { getProxyUrl } from './proxy.js'; import { getProxyUrl } from './proxy.js';
@@ -264,15 +268,12 @@ export function buildAPIProviderProperties(): Property[] {
} else if (apiProvider === 'openrouter') { } else if (apiProvider === 'openrouter') {
properties.push({ properties.push({
label: 'OpenRouter base URL', label: 'OpenRouter base URL',
value: value: getOpenRouterBaseUrl()
process.env.OPENROUTER_BASE_URL ||
process.env.ANTHROPIC_BASE_URL ||
'https://openrouter.ai/api/v1'
}); });
} else if (apiProvider === 'openai') { } else if (apiProvider === 'openai') {
properties.push({ properties.push({
label: 'OpenAI base URL', label: 'OpenAI base URL',
value: process.env.OPENAI_BASE_URL || 'https://api.openai.com/v1' value: getOpenAIBaseUrl()
}); });
} else if (apiProvider === 'bedrock') { } else if (apiProvider === 'bedrock') {
const bedrockBaseUrl = process.env.BEDROCK_BASE_URL; const bedrockBaseUrl = process.env.BEDROCK_BASE_URL;