feat: auto load free openrouter model
Browse files
src/commands/login/login.tsx
CHANGED
|
@@ -109,7 +109,9 @@ export async function call(
|
|
| 109 |
// Increment authVersion to trigger re-fetching of auth-dependent data in hooks (e.g., MCP servers)
|
| 110 |
context.setAppState(prev => ({
|
| 111 |
...prev,
|
| 112 |
-
authVersion: prev.authVersion + 1 // 触发全局更新, 用 version 触发所有 hook 重新 fetch
|
|
|
|
|
|
|
| 113 |
}));
|
| 114 |
}
|
| 115 |
|
|
|
|
| 109 |
// Increment authVersion to trigger re-fetching of auth-dependent data in hooks (e.g., MCP servers)
|
| 110 |
context.setAppState(prev => ({
|
| 111 |
...prev,
|
| 112 |
+
authVersion: prev.authVersion + 1, // 触发全局更新, 用 version 触发所有 hook 重新 fetch
|
| 113 |
+
mainLoopModel: null, // 重置为 null,使用新 provider 的默认模型
|
| 114 |
+
mainLoopModelForSession: null, // 重置 session 模型
|
| 115 |
}));
|
| 116 |
}
|
| 117 |
|
src/components/OpenRouterLoginFlow.tsx
CHANGED
|
@@ -29,13 +29,15 @@ export function OpenRouterLoginFlow({
|
|
| 29 |
|
| 30 |
async function handleSubmit(value?: string): Promise<void> {
|
| 31 |
if (!value && !existingKey) {
|
|
|
|
| 32 |
return
|
| 33 |
}
|
| 34 |
-
|
| 35 |
const trimmed = value?.trim() || ''
|
| 36 |
-
const keyToSave = trimmed || existingKey
|
| 37 |
-
|
| 38 |
if (!keyToSave) {
|
|
|
|
| 39 |
return
|
| 40 |
}
|
| 41 |
|
|
|
|
| 29 |
|
| 30 |
async function handleSubmit(value?: string): Promise<void> {
|
| 31 |
if (!value && !existingKey) {
|
| 32 |
+
setStatus('Please enter an API key or press Esc to cancel')
|
| 33 |
return
|
| 34 |
}
|
| 35 |
+
|
| 36 |
const trimmed = value?.trim() || ''
|
| 37 |
+
const keyToSave = trimmed || existingKey || ''
|
| 38 |
+
|
| 39 |
if (!keyToSave) {
|
| 40 |
+
setStatus('Please enter an API key or press Esc to cancel')
|
| 41 |
return
|
| 42 |
}
|
| 43 |
|
src/components/SearchableModelPicker.tsx
CHANGED
|
@@ -104,9 +104,9 @@ export function SearchableModelPicker({
|
|
| 104 |
return filteredOptions
|
| 105 |
}, [filteredOptions, initial])
|
| 106 |
|
| 107 |
-
const initialFocusValue = Array.isArray(
|
| 108 |
? initialValue
|
| 109 |
-
: (Array.isArray(
|
| 110 |
|
| 111 |
// Helper functions
|
| 112 |
function resolveOptionModel(value?: string): string | undefined {
|
|
|
|
| 104 |
return filteredOptions
|
| 105 |
}, [filteredOptions, initial])
|
| 106 |
|
| 107 |
+
const initialFocusValue = Array.isArray(filteredOptions) && filteredOptions.some(opt => opt.value === initialValue)
|
| 108 |
? initialValue
|
| 109 |
+
: (Array.isArray(filteredOptions) && filteredOptions[0]?.value) ?? undefined
|
| 110 |
|
| 111 |
// Helper functions
|
| 112 |
function resolveOptionModel(value?: string): string | undefined {
|
src/utils/auth.ts
CHANGED
|
@@ -404,17 +404,26 @@ export async function saveOpenRouterApiKey(apiKey: string): Promise<void> {
|
|
| 404 |
'Invalid API key: API key must be a non-empty string.',
|
| 405 |
)
|
| 406 |
}
|
| 407 |
-
|
| 408 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 409 |
throw new Error(
|
| 410 |
'Invalid API key format. API key must contain only alphanumeric characters, dashes, and underscores.',
|
| 411 |
)
|
| 412 |
}
|
| 413 |
-
|
| 414 |
saveGlobalConfig(current => ({
|
| 415 |
...current,
|
| 416 |
authProvider: 'openrouter',
|
| 417 |
-
openRouterApiKey:
|
| 418 |
}))
|
| 419 |
// Clear provider cache so it will be re-read on next access
|
| 420 |
const { clearStoredProviderCache } = await import('./model/providers.js')
|
|
|
|
| 404 |
'Invalid API key: API key must be a non-empty string.',
|
| 405 |
)
|
| 406 |
}
|
| 407 |
+
|
| 408 |
+
// Trim the API key to remove any leading/trailing whitespace
|
| 409 |
+
const trimmedKey = apiKey.trim()
|
| 410 |
+
|
| 411 |
+
if (!trimmedKey) {
|
| 412 |
+
throw new Error(
|
| 413 |
+
'Invalid API key: API key cannot be empty or whitespace only.',
|
| 414 |
+
)
|
| 415 |
+
}
|
| 416 |
+
|
| 417 |
+
if (!isValidApiKey(trimmedKey)) {
|
| 418 |
throw new Error(
|
| 419 |
'Invalid API key format. API key must contain only alphanumeric characters, dashes, and underscores.',
|
| 420 |
)
|
| 421 |
}
|
| 422 |
+
|
| 423 |
saveGlobalConfig(current => ({
|
| 424 |
...current,
|
| 425 |
authProvider: 'openrouter',
|
| 426 |
+
openRouterApiKey: trimmedKey,
|
| 427 |
}))
|
| 428 |
// Clear provider cache so it will be re-read on next access
|
| 429 |
const { clearStoredProviderCache } = await import('./model/providers.js')
|
src/utils/model/model.ts
CHANGED
|
@@ -178,12 +178,50 @@ export function getRuntimeMainLoopModel(params: {
|
|
| 178 |
* @returns The default model setting to use
|
| 179 |
*/
|
| 180 |
export function getDefaultMainLoopModelSetting(): ModelName | ModelAlias {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 181 |
// Check if using Local provider
|
| 182 |
// Need to check both getAPIProvider() and direct file read because
|
| 183 |
// getAPIProvider() might return null if cache was cleared
|
| 184 |
-
const apiProvider = getAPIProvider()
|
| 185 |
let isLocalProvider = apiProvider === 'local'
|
| 186 |
-
|
| 187 |
// If getAPIProvider() returns null, check the config file directly
|
| 188 |
if (!isLocalProvider && apiProvider === null) {
|
| 189 |
try {
|
|
@@ -200,7 +238,7 @@ export function getDefaultMainLoopModelSetting(): ModelName | ModelAlias {
|
|
| 200 |
// Ignore errors, fall through to default behavior
|
| 201 |
}
|
| 202 |
}
|
| 203 |
-
|
| 204 |
if (isLocalProvider) {
|
| 205 |
const localModelName = getLocalModelName()
|
| 206 |
if (localModelName) {
|
|
|
|
| 178 |
* @returns The default model setting to use
|
| 179 |
*/
|
| 180 |
export function getDefaultMainLoopModelSetting(): ModelName | ModelAlias {
|
| 181 |
+
// Check if using OpenRouter provider
|
| 182 |
+
const apiProvider = getAPIProvider()
|
| 183 |
+
let isOpenRouterProvider = apiProvider === 'openrouter'
|
| 184 |
+
|
| 185 |
+
// If getAPIProvider() returns null, check the config file directly
|
| 186 |
+
if (!isOpenRouterProvider && apiProvider === null) {
|
| 187 |
+
try {
|
| 188 |
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
| 189 |
+
const { readFileSync } = require('fs') as typeof import('fs')
|
| 190 |
+
const { getGlobalClaudeFile } = require('./env.js') as typeof import('./env.js')
|
| 191 |
+
const raw = readFileSync(getGlobalClaudeFile(), 'utf8')
|
| 192 |
+
const config = JSON.parse(raw) as {
|
| 193 |
+
authProvider?: string
|
| 194 |
+
openRouterApiKey?: string
|
| 195 |
+
}
|
| 196 |
+
isOpenRouterProvider = config.authProvider === 'openrouter' && !!config.openRouterApiKey
|
| 197 |
+
} catch {
|
| 198 |
+
// Ignore errors, fall through to default behavior
|
| 199 |
+
}
|
| 200 |
+
}
|
| 201 |
+
|
| 202 |
+
if (isOpenRouterProvider) {
|
| 203 |
+
// Try to get the first free model from OpenRouter
|
| 204 |
+
try {
|
| 205 |
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
| 206 |
+
const openRouterModelsModule = require('./openRouterModels.js') as {
|
| 207 |
+
getFirstFreeModel: () => { value: string } | null
|
| 208 |
+
}
|
| 209 |
+
const firstFreeModel = openRouterModelsModule.getFirstFreeModel()
|
| 210 |
+
if (firstFreeModel) {
|
| 211 |
+
return firstFreeModel.value as ModelName
|
| 212 |
+
}
|
| 213 |
+
} catch {
|
| 214 |
+
// Module not yet loaded or error, fall through to default behavior
|
| 215 |
+
}
|
| 216 |
+
// Fallback to 'default' if no free model found
|
| 217 |
+
return 'default'
|
| 218 |
+
}
|
| 219 |
+
|
| 220 |
// Check if using Local provider
|
| 221 |
// Need to check both getAPIProvider() and direct file read because
|
| 222 |
// getAPIProvider() might return null if cache was cleared
|
|
|
|
| 223 |
let isLocalProvider = apiProvider === 'local'
|
| 224 |
+
|
| 225 |
// If getAPIProvider() returns null, check the config file directly
|
| 226 |
if (!isLocalProvider && apiProvider === null) {
|
| 227 |
try {
|
|
|
|
| 238 |
// Ignore errors, fall through to default behavior
|
| 239 |
}
|
| 240 |
}
|
| 241 |
+
|
| 242 |
if (isLocalProvider) {
|
| 243 |
const localModelName = getLocalModelName()
|
| 244 |
if (localModelName) {
|
src/utils/model/openRouterModels.ts
CHANGED
|
@@ -142,12 +142,13 @@ export function getFirstFreeModel(): ModelOption | null {
|
|
| 142 |
if (!openRouterModelsCache || openRouterModelsCache.length === 0) {
|
| 143 |
return null
|
| 144 |
}
|
| 145 |
-
|
| 146 |
return openRouterModelsCache.find(model => {
|
| 147 |
// Check if this model is free by looking at the model name
|
| 148 |
-
// Models with "(free)" in the name are free
|
| 149 |
-
const label = model.label || ''
|
| 150 |
-
|
|
|
|
| 151 |
}) || null
|
| 152 |
}
|
| 153 |
|
|
|
|
| 142 |
if (!openRouterModelsCache || openRouterModelsCache.length === 0) {
|
| 143 |
return null
|
| 144 |
}
|
| 145 |
+
|
| 146 |
return openRouterModelsCache.find(model => {
|
| 147 |
// Check if this model is free by looking at the model name
|
| 148 |
+
// Models with ":free" or "(free)" in the name are free
|
| 149 |
+
const label = (model.label || '').toLowerCase()
|
| 150 |
+
const value = (model.value || '').toLowerCase()
|
| 151 |
+
return label.includes(':free') || label.includes('(free)') || value.includes(':free') || value.includes('(free)')
|
| 152 |
}) || null
|
| 153 |
}
|
| 154 |
|