fix openrouter
This commit is contained in:
@@ -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
|
||||||
|
|||||||
@@ -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": {
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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]
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
Reference in New Issue
Block a user