fix: /login provider change
Browse files- src/components/OpenAILoginFlow.tsx +6 -1
- src/components/OpenRouterLoginFlow.tsx +6 -1
- src/services/api/errors.ts +2 -1
- src/utils/auth.ts +22 -5
- src/utils/model/model.ts +23 -1
- src/utils/model/modelOptions.ts +40 -0
- src/utils/model/providers.ts +2 -2
- src/utils/swarm/teammateModel.ts +3 -1
src/components/OpenAILoginFlow.tsx
CHANGED
|
@@ -115,7 +115,11 @@ export function OpenAILoginFlow({
|
|
| 115 |
}
|
| 116 |
}
|
| 117 |
|
| 118 |
-
async function handleSubmit(value: string): Promise<void> {
|
|
|
|
|
|
|
|
|
|
|
|
|
| 119 |
const trimmed = value.trim()
|
| 120 |
if (!trimmed) {
|
| 121 |
return
|
|
@@ -180,6 +184,7 @@ export function OpenAILoginFlow({
|
|
| 180 |
onChangeCursorOffset={setCursorOffset}
|
| 181 |
columns={72}
|
| 182 |
mask="*"
|
|
|
|
| 183 |
/>
|
| 184 |
</Box>
|
| 185 |
{status ? <Text color="error">{status}</Text> : null}
|
|
|
|
| 115 |
}
|
| 116 |
}
|
| 117 |
|
| 118 |
+
async function handleSubmit(value?: string): Promise<void> {
|
| 119 |
+
if (!value) {
|
| 120 |
+
return
|
| 121 |
+
}
|
| 122 |
+
|
| 123 |
const trimmed = value.trim()
|
| 124 |
if (!trimmed) {
|
| 125 |
return
|
|
|
|
| 184 |
onChangeCursorOffset={setCursorOffset}
|
| 185 |
columns={72}
|
| 186 |
mask="*"
|
| 187 |
+
focus={true}
|
| 188 |
/>
|
| 189 |
</Box>
|
| 190 |
{status ? <Text color="error">{status}</Text> : null}
|
src/components/OpenRouterLoginFlow.tsx
CHANGED
|
@@ -19,7 +19,11 @@ export function OpenRouterLoginFlow({
|
|
| 19 |
const [inputValue, setInputValue] = useState('')
|
| 20 |
const [cursorOffset, setCursorOffset] = useState(0)
|
| 21 |
|
| 22 |
-
async function handleSubmit(value: string): Promise<void> {
|
|
|
|
|
|
|
|
|
|
|
|
|
| 23 |
const trimmed = value.trim()
|
| 24 |
if (!trimmed) {
|
| 25 |
return
|
|
@@ -76,6 +80,7 @@ export function OpenRouterLoginFlow({
|
|
| 76 |
onChangeCursorOffset={setCursorOffset}
|
| 77 |
columns={72}
|
| 78 |
mask="*"
|
|
|
|
| 79 |
/>
|
| 80 |
</Box>
|
| 81 |
{status ? <Text color="error">{status}</Text> : null}
|
|
|
|
| 19 |
const [inputValue, setInputValue] = useState('')
|
| 20 |
const [cursorOffset, setCursorOffset] = useState(0)
|
| 21 |
|
| 22 |
+
async function handleSubmit(value?: string): Promise<void> {
|
| 23 |
+
if (!value) {
|
| 24 |
+
return
|
| 25 |
+
}
|
| 26 |
+
|
| 27 |
const trimmed = value.trim()
|
| 28 |
if (!trimmed) {
|
| 29 |
return
|
|
|
|
| 80 |
onChangeCursorOffset={setCursorOffset}
|
| 81 |
columns={72}
|
| 82 |
mask="*"
|
| 83 |
+
focus={true}
|
| 84 |
/>
|
| 85 |
</Box>
|
| 86 |
{status ? <Text color="error">{status}</Text> : null}
|
src/services/api/errors.ts
CHANGED
|
@@ -905,9 +905,10 @@ export function getAssistantMessageFromError(
|
|
| 905 |
if (error instanceof APIError && error.status === 404) {
|
| 906 |
const switchCmd = getIsNonInteractiveSession() ? '--model' : '/model'
|
| 907 |
const fallbackSuggestion = get3PModelFallbackSuggestion(model)
|
|
|
|
| 908 |
return createAssistantAPIErrorMessage({
|
| 909 |
content: fallbackSuggestion
|
| 910 |
-
? `The model ${model} is not available on your ${
|
| 911 |
: `There's an issue with the selected model (${model}). It may not exist or you may not have access to it. Run ${switchCmd} to pick a different model.`,
|
| 912 |
error: 'invalid_request',
|
| 913 |
})
|
|
|
|
| 905 |
if (error instanceof APIError && error.status === 404) {
|
| 906 |
const switchCmd = getIsNonInteractiveSession() ? '--model' : '/model'
|
| 907 |
const fallbackSuggestion = get3PModelFallbackSuggestion(model)
|
| 908 |
+
const provider = getAPIProvider()
|
| 909 |
return createAssistantAPIErrorMessage({
|
| 910 |
content: fallbackSuggestion
|
| 911 |
+
? `The model ${model} is not available on your ${provider ?? 'configured'} deployment. Try ${switchCmd} to switch to ${fallbackSuggestion}, or ask your admin to enable this model.`
|
| 912 |
: `There's an issue with the selected model (${model}). It may not exist or you may not have access to it. Run ${switchCmd} to pick a different model.`,
|
| 913 |
error: 'invalid_request',
|
| 914 |
})
|
src/utils/auth.ts
CHANGED
|
@@ -388,11 +388,18 @@ export async function saveOpenAIApiKey(apiKey: string): Promise<void> {
|
|
| 388 |
}
|
| 389 |
|
| 390 |
export async function saveOpenRouterApiKey(apiKey: string): Promise<void> {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 391 |
if (!isValidApiKey(apiKey)) {
|
| 392 |
throw new Error(
|
| 393 |
'Invalid API key format. API key must contain only alphanumeric characters, dashes, and underscores.',
|
| 394 |
)
|
| 395 |
}
|
|
|
|
| 396 |
saveGlobalConfig(current => ({
|
| 397 |
...current,
|
| 398 |
authProvider: 'openrouter',
|
|
@@ -402,12 +409,16 @@ export async function saveOpenRouterApiKey(apiKey: string): Promise<void> {
|
|
| 402 |
const { clearStoredProviderCache } = await import('./model/providers.js')
|
| 403 |
clearStoredProviderCache()
|
| 404 |
// Clear OpenRouter models cache so it will be refetched with new API key
|
| 405 |
-
|
| 406 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 407 |
}
|
| 408 |
|
| 409 |
export async function saveLocalModelConfig(baseUrl: string, modelName: string): Promise<void> {
|
| 410 |
-
console.log('[saveLocalModelConfig] Saving local model config:', baseUrl, modelName)
|
| 411 |
if (!baseUrl.trim()) {
|
| 412 |
throw new Error('Base URL cannot be empty')
|
| 413 |
}
|
|
@@ -426,14 +437,20 @@ export async function saveLocalModelConfig(baseUrl: string, modelName: string):
|
|
| 426 |
localBaseUrl: baseUrl,
|
| 427 |
localModelName: modelName,
|
| 428 |
}
|
| 429 |
-
console.log('[saveLocalModelConfig] New config to save:', newConfig)
|
| 430 |
return newConfig
|
| 431 |
})
|
| 432 |
|
| 433 |
// Clear provider cache so it will be re-read on next access
|
| 434 |
const { clearStoredProviderCache } = await import('./model/providers.js')
|
| 435 |
clearStoredProviderCache()
|
| 436 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 437 |
}
|
| 438 |
|
| 439 |
export function getLocalModelName(): string | null {
|
|
|
|
| 388 |
}
|
| 389 |
|
| 390 |
export async function saveOpenRouterApiKey(apiKey: string): Promise<void> {
|
| 391 |
+
if (!apiKey || typeof apiKey !== 'string') {
|
| 392 |
+
throw new Error(
|
| 393 |
+
'Invalid API key: API key must be a non-empty string.',
|
| 394 |
+
)
|
| 395 |
+
}
|
| 396 |
+
|
| 397 |
if (!isValidApiKey(apiKey)) {
|
| 398 |
throw new Error(
|
| 399 |
'Invalid API key format. API key must contain only alphanumeric characters, dashes, and underscores.',
|
| 400 |
)
|
| 401 |
}
|
| 402 |
+
|
| 403 |
saveGlobalConfig(current => ({
|
| 404 |
...current,
|
| 405 |
authProvider: 'openrouter',
|
|
|
|
| 409 |
const { clearStoredProviderCache } = await import('./model/providers.js')
|
| 410 |
clearStoredProviderCache()
|
| 411 |
// Clear OpenRouter models cache so it will be refetched with new API key
|
| 412 |
+
try {
|
| 413 |
+
const { clearOpenRouterModelsCache } = await import('./model/openRouterModels.js')
|
| 414 |
+
clearOpenRouterModelsCache()
|
| 415 |
+
} catch (error) {
|
| 416 |
+
// Ignore errors from clearing cache
|
| 417 |
+
console.error('Error clearing OpenRouter models cache:', error)
|
| 418 |
+
}
|
| 419 |
}
|
| 420 |
|
| 421 |
export async function saveLocalModelConfig(baseUrl: string, modelName: string): Promise<void> {
|
|
|
|
| 422 |
if (!baseUrl.trim()) {
|
| 423 |
throw new Error('Base URL cannot be empty')
|
| 424 |
}
|
|
|
|
| 437 |
localBaseUrl: baseUrl,
|
| 438 |
localModelName: modelName,
|
| 439 |
}
|
|
|
|
| 440 |
return newConfig
|
| 441 |
})
|
| 442 |
|
| 443 |
// Clear provider cache so it will be re-read on next access
|
| 444 |
const { clearStoredProviderCache } = await import('./model/providers.js')
|
| 445 |
clearStoredProviderCache()
|
| 446 |
+
|
| 447 |
+
// Clear OpenRouter models cache so it will be refetched with new API key
|
| 448 |
+
try {
|
| 449 |
+
const { clearOpenRouterModelsCache } = await import('./model/openRouterModels.js')
|
| 450 |
+
clearOpenRouterModelsCache()
|
| 451 |
+
} catch (error) {
|
| 452 |
+
// Ignore errors from clearing cache
|
| 453 |
+
}
|
| 454 |
}
|
| 455 |
|
| 456 |
export function getLocalModelName(): string | null {
|
src/utils/model/model.ts
CHANGED
|
@@ -179,7 +179,29 @@ export function getRuntimeMainLoopModel(params: {
|
|
| 179 |
*/
|
| 180 |
export function getDefaultMainLoopModelSetting(): ModelName | ModelAlias {
|
| 181 |
// Check if using Local provider
|
| 182 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 183 |
const localModelName = getLocalModelName()
|
| 184 |
if (localModelName) {
|
| 185 |
return localModelName
|
|
|
|
| 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 {
|
| 190 |
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
| 191 |
+
const { readFileSync } = require('fs') as typeof import('fs')
|
| 192 |
+
const { getGlobalClaudeFile } = require('./env.js') as typeof import('./env.js')
|
| 193 |
+
const raw = readFileSync(getGlobalClaudeFile(), 'utf8')
|
| 194 |
+
const config = JSON.parse(raw) as {
|
| 195 |
+
authProvider?: string
|
| 196 |
+
localBaseUrl?: string
|
| 197 |
+
}
|
| 198 |
+
isLocalProvider = config.authProvider === 'local' && !!config.localBaseUrl
|
| 199 |
+
} catch {
|
| 200 |
+
// Ignore errors, fall through to default behavior
|
| 201 |
+
}
|
| 202 |
+
}
|
| 203 |
+
|
| 204 |
+
if (isLocalProvider) {
|
| 205 |
const localModelName = getLocalModelName()
|
| 206 |
if (localModelName) {
|
| 207 |
return localModelName
|
src/utils/model/modelOptions.ts
CHANGED
|
@@ -67,7 +67,40 @@ export function getDefaultOptionForUser(fastMode = false): ModelOption {
|
|
| 67 |
// PAYG
|
| 68 |
const provider = getAPIProvider()
|
| 69 |
const isOpenAI = provider === 'openai'
|
|
|
|
| 70 |
const is3P = provider !== 'firstParty'
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 71 |
return {
|
| 72 |
value: null,
|
| 73 |
label: 'Default (recommended)',
|
|
@@ -333,6 +366,13 @@ function getOpusPlanOption(): ModelOption {
|
|
| 333 |
// @[MODEL LAUNCH]: Update the model picker lists below to include/reorder options for the new model.
|
| 334 |
// Each user tier (ant, Max/Team Premium, Pro/Team Standard/Enterprise, PAYG 1P, PAYG 3P) has its own list.
|
| 335 |
function getModelOptionsBase(fastMode = false): ModelOption[] {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 336 |
if (process.env.USER_TYPE === 'ant') {
|
| 337 |
// Build options from antModels config
|
| 338 |
const antModelOptions: ModelOption[] = getAntModels().map(m => ({
|
|
|
|
| 67 |
// PAYG
|
| 68 |
const provider = getAPIProvider()
|
| 69 |
const isOpenAI = provider === 'openai'
|
| 70 |
+
const isLocal = provider === 'local'
|
| 71 |
const is3P = provider !== 'firstParty'
|
| 72 |
+
|
| 73 |
+
// Check if we should use local model
|
| 74 |
+
let localModelName: string | null = null
|
| 75 |
+
if (isLocal || provider === null) {
|
| 76 |
+
// Try to get local model name from file
|
| 77 |
+
try {
|
| 78 |
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
| 79 |
+
const { readFileSync } = require('fs') as typeof import('fs')
|
| 80 |
+
const { getGlobalClaudeFile } = require('../env.js') as typeof import('../env.js')
|
| 81 |
+
const raw = readFileSync(getGlobalClaudeFile(), 'utf8')
|
| 82 |
+
const config = JSON.parse(raw) as {
|
| 83 |
+
authProvider?: string
|
| 84 |
+
localBaseUrl?: string
|
| 85 |
+
localModelName?: string
|
| 86 |
+
}
|
| 87 |
+
if (config.authProvider === 'local' && config.localBaseUrl && config.localModelName) {
|
| 88 |
+
localModelName = config.localModelName
|
| 89 |
+
}
|
| 90 |
+
} catch {
|
| 91 |
+
// Ignore errors
|
| 92 |
+
}
|
| 93 |
+
}
|
| 94 |
+
|
| 95 |
+
if (localModelName) {
|
| 96 |
+
return {
|
| 97 |
+
value: null,
|
| 98 |
+
label: 'Default (recommended)',
|
| 99 |
+
description: `Use the default local model (currently ${localModelName})`,
|
| 100 |
+
descriptionForModel: `Default local model (currently ${localModelName})`,
|
| 101 |
+
}
|
| 102 |
+
}
|
| 103 |
+
|
| 104 |
return {
|
| 105 |
value: null,
|
| 106 |
label: 'Default (recommended)',
|
|
|
|
| 366 |
// @[MODEL LAUNCH]: Update the model picker lists below to include/reorder options for the new model.
|
| 367 |
// Each user tier (ant, Max/Team Premium, Pro/Team Standard/Enterprise, PAYG 1P, PAYG 3P) has its own list.
|
| 368 |
function getModelOptionsBase(fastMode = false): ModelOption[] {
|
| 369 |
+
const provider = getAPIProvider()
|
| 370 |
+
|
| 371 |
+
// If no provider is configured, only return Default option
|
| 372 |
+
if (!provider) {
|
| 373 |
+
return [getDefaultOptionForUser(fastMode)]
|
| 374 |
+
}
|
| 375 |
+
|
| 376 |
if (process.env.USER_TYPE === 'ant') {
|
| 377 |
// Build options from antModels config
|
| 378 |
const antModelOptions: ModelOption[] = getAntModels().map(m => ({
|
src/utils/model/providers.ts
CHANGED
|
@@ -164,7 +164,7 @@ export function getOpenAIBaseUrl(): string {
|
|
| 164 |
return process.env.OPENAI_BASE_URL ?? 'https://api.openai.com/v1'
|
| 165 |
}
|
| 166 |
|
| 167 |
-
export function getAPIProvider(): APIProvider {
|
| 168 |
const explicitProvider = getExplicitProviderOverride()
|
| 169 |
if (explicitProvider) {
|
| 170 |
return explicitProvider
|
|
@@ -180,7 +180,7 @@ export function getAPIProvider(): APIProvider {
|
|
| 180 |
? 'openai'
|
| 181 |
: isOpenRouterConfigured()
|
| 182 |
? 'openrouter'
|
| 183 |
-
: getStoredProviderPreference()
|
| 184 |
}
|
| 185 |
|
| 186 |
export function getAPIProviderForStatsig(): AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS {
|
|
|
|
| 164 |
return process.env.OPENAI_BASE_URL ?? 'https://api.openai.com/v1'
|
| 165 |
}
|
| 166 |
|
| 167 |
+
export function getAPIProvider(): APIProvider | null {
|
| 168 |
const explicitProvider = getExplicitProviderOverride()
|
| 169 |
if (explicitProvider) {
|
| 170 |
return explicitProvider
|
|
|
|
| 180 |
? 'openai'
|
| 181 |
: isOpenRouterConfigured()
|
| 182 |
? 'openrouter'
|
| 183 |
+
: getStoredProviderPreference()
|
| 184 |
}
|
| 185 |
|
| 186 |
export function getAPIProviderForStatsig(): AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS {
|
src/utils/swarm/teammateModel.ts
CHANGED
|
@@ -6,5 +6,7 @@ import { getAPIProvider } from '../model/providers.js'
|
|
| 6 |
// use Opus 4.6. Must be provider-aware so Bedrock/Vertex/Foundry customers get
|
| 7 |
// the correct model ID.
|
| 8 |
export function getHardcodedTeammateModelFallback(): string {
|
| 9 |
-
|
|
|
|
|
|
|
| 10 |
}
|
|
|
|
| 6 |
// use Opus 4.6. Must be provider-aware so Bedrock/Vertex/Foundry customers get
|
| 7 |
// the correct model ID.
|
| 8 |
export function getHardcodedTeammateModelFallback(): string {
|
| 9 |
+
const provider = getAPIProvider()
|
| 10 |
+
// Default to firstParty if no provider is configured
|
| 11 |
+
return CLAUDE_OPUS_4_6_CONFIG[provider ?? 'firstParty']
|
| 12 |
}
|