fix: desktop model list uses sidecar proxy (no CORS) and correct provider names
Browse files- Replace fetchProviderModels() (browser CORS block) with modelsApi.list()
(server-side sidecar proxy) to fetch NVIDIA/OpenRouter models
- Derive provider names from ~/.claude.json authProvider field, avoiding
sidecar's stale/corrupted names like "TUI: NVIDIA NIM"
- Fall back to buildDefaultCliModels() when sidecar returns empty
- Remove clearProviderModelCache() since we no longer use providerModels.ts
in settings/provider stores
- Fix ApiFormat type comparison ('openai' -> 'openai_chat'/'openai_responses')
Co-Authored-By: Claude Big Pickle <noreply@anthropic.com>
desktop/src/api/providerModels.ts
CHANGED
|
@@ -156,7 +156,7 @@ let nvCacheTime = 0
|
|
| 156 |
|
| 157 |
async function fetchNvidiaModels(apiKey?: string | null, baseUrl?: string): Promise<ModelInfo[]> {
|
| 158 |
if (nvCache && Date.now() - nvCacheTime < OR_CACHE_TTL) {
|
| 159 |
-
return nvCache
|
| 160 |
}
|
| 161 |
|
| 162 |
if (!apiKey) {
|
|
|
|
| 156 |
|
| 157 |
async function fetchNvidiaModels(apiKey?: string | null, baseUrl?: string): Promise<ModelInfo[]> {
|
| 158 |
if (nvCache && Date.now() - nvCacheTime < OR_CACHE_TTL) {
|
| 159 |
+
return nvCache.length > 0 ? nvCache : []
|
| 160 |
}
|
| 161 |
|
| 162 |
if (!apiKey) {
|
desktop/src/stores/providerStore.ts
CHANGED
|
@@ -6,12 +6,7 @@ import { useChatStore } from './chatStore'
|
|
| 6 |
import { useSessionRuntimeStore } from './sessionRuntimeStore'
|
| 7 |
import { useSettingsStore } from './settingsStore'
|
| 8 |
import { OFFICIAL_DEFAULT_MODEL_ID } from '../constants/modelCatalog'
|
| 9 |
-
import {
|
| 10 |
-
OPENAI_OFFICIAL_DEFAULT_MODEL_ID,
|
| 11 |
-
OPENAI_OFFICIAL_PROVIDER_ID,
|
| 12 |
-
} from '../constants/openaiOfficialProvider'
|
| 13 |
import { saveTuiConfigPatch, clearConfigCache } from '../api/config'
|
| 14 |
-
import { clearProviderModelCache } from '../api/providerModels'
|
| 15 |
import type {
|
| 16 |
SavedProvider,
|
| 17 |
CreateProviderInput,
|
|
@@ -58,8 +53,9 @@ function mapSidecarToTuiProvider(provider: SavedProvider): TuiProviderMapping {
|
|
| 58 |
const name = (provider.name || '').toLowerCase()
|
| 59 |
|
| 60 |
// Detect local provider (Ollama, LM Studio, vLLM, etc.)
|
|
|
|
| 61 |
if (
|
| 62 |
-
|
| 63 |
(baseUrl.includes('localhost') || baseUrl.includes('127.0.0.1') || baseUrl.includes('0.0.0.0') ||
|
| 64 |
name.includes('local') || name.includes('ollama'))
|
| 65 |
) {
|
|
@@ -74,7 +70,8 @@ function mapSidecarToTuiProvider(provider: SavedProvider): TuiProviderMapping {
|
|
| 74 |
switch (apiFormat) {
|
| 75 |
case 'anthropic':
|
| 76 |
return { authProvider: 'anthropic', apiKeyField: 'anthropicApiKey', apiKey: provider.apiKey }
|
| 77 |
-
case '
|
|
|
|
| 78 |
if (baseUrl.includes('nvidia') || name.includes('nvidia')) {
|
| 79 |
return { authProvider: 'nvidia', apiKeyField: 'nvidiaApiKey', apiKey: provider.apiKey }
|
| 80 |
}
|
|
@@ -228,11 +225,8 @@ export const useProviderStore = create<ProviderStore>((set, get) => ({
|
|
| 228 |
patch.model = provider.models.main
|
| 229 |
|
| 230 |
await saveTuiConfigPatch(patch)
|
| 231 |
-
clearProviderModelCache()
|
| 232 |
clearConfigCache()
|
| 233 |
-
|
| 234 |
-
await settings.setModel(provider.models.main)
|
| 235 |
-
await settings.fetchAll()
|
| 236 |
},
|
| 237 |
|
| 238 |
activateOfficial: async () => {
|
|
@@ -240,7 +234,6 @@ export const useProviderStore = create<ProviderStore>((set, get) => ({
|
|
| 240 |
await get().fetchProviders()
|
| 241 |
// 同步回 Anthropic first-party
|
| 242 |
await saveTuiConfigPatch({ authProvider: 'anthropic' })
|
| 243 |
-
clearProviderModelCache()
|
| 244 |
clearConfigCache()
|
| 245 |
const settings = useSettingsStore.getState()
|
| 246 |
await settings.setModel(OFFICIAL_DEFAULT_MODEL_ID)
|
|
|
|
| 6 |
import { useSessionRuntimeStore } from './sessionRuntimeStore'
|
| 7 |
import { useSettingsStore } from './settingsStore'
|
| 8 |
import { OFFICIAL_DEFAULT_MODEL_ID } from '../constants/modelCatalog'
|
|
|
|
|
|
|
|
|
|
|
|
|
| 9 |
import { saveTuiConfigPatch, clearConfigCache } from '../api/config'
|
|
|
|
| 10 |
import type {
|
| 11 |
SavedProvider,
|
| 12 |
CreateProviderInput,
|
|
|
|
| 53 |
const name = (provider.name || '').toLowerCase()
|
| 54 |
|
| 55 |
// Detect local provider (Ollama, LM Studio, vLLM, etc.)
|
| 56 |
+
const isOpenAiFormat = apiFormat === 'openai_chat' || apiFormat === 'openai_responses'
|
| 57 |
if (
|
| 58 |
+
isOpenAiFormat &&
|
| 59 |
(baseUrl.includes('localhost') || baseUrl.includes('127.0.0.1') || baseUrl.includes('0.0.0.0') ||
|
| 60 |
name.includes('local') || name.includes('ollama'))
|
| 61 |
) {
|
|
|
|
| 70 |
switch (apiFormat) {
|
| 71 |
case 'anthropic':
|
| 72 |
return { authProvider: 'anthropic', apiKeyField: 'anthropicApiKey', apiKey: provider.apiKey }
|
| 73 |
+
case 'openai_chat':
|
| 74 |
+
case 'openai_responses': {
|
| 75 |
if (baseUrl.includes('nvidia') || name.includes('nvidia')) {
|
| 76 |
return { authProvider: 'nvidia', apiKeyField: 'nvidiaApiKey', apiKey: provider.apiKey }
|
| 77 |
}
|
|
|
|
| 225 |
patch.model = provider.models.main
|
| 226 |
|
| 227 |
await saveTuiConfigPatch(patch)
|
|
|
|
| 228 |
clearConfigCache()
|
| 229 |
+
await settings.syncFromConfig()
|
|
|
|
|
|
|
| 230 |
},
|
| 231 |
|
| 232 |
activateOfficial: async () => {
|
|
|
|
| 234 |
await get().fetchProviders()
|
| 235 |
// 同步回 Anthropic first-party
|
| 236 |
await saveTuiConfigPatch({ authProvider: 'anthropic' })
|
|
|
|
| 237 |
clearConfigCache()
|
| 238 |
const settings = useSettingsStore.getState()
|
| 239 |
await settings.setModel(OFFICIAL_DEFAULT_MODEL_ID)
|
desktop/src/stores/settingsStore.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
| 1 |
import { create } from 'zustand'
|
| 2 |
import { ApiError } from '../api/client'
|
| 3 |
import { settingsApi } from '../api/settings'
|
|
|
|
| 4 |
import { h5AccessApi } from '../api/h5Access'
|
| 5 |
-
import { fetchProviderModels, clearProviderModelCache } from '../api/providerModels'
|
| 6 |
import { getTuiConfig, saveTuiConfigPatch, clearConfigCache } from '../api/config'
|
| 7 |
import {
|
| 8 |
isThemeMode,
|
|
@@ -40,6 +40,30 @@ export const UI_ZOOM_STEP = APP_ZOOM_CONTROL_STEP
|
|
| 40 |
export const UI_ZOOM_DEFAULT = DEFAULT_APP_ZOOM
|
| 41 |
let desktopNotificationsSaveQueue: Promise<void> = Promise.resolve()
|
| 42 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 43 |
function getStoredLocale(): Locale {
|
| 44 |
try {
|
| 45 |
const stored = localStorage.getItem(LOCALE_STORAGE_KEY)
|
|
@@ -173,47 +197,46 @@ export const useSettingsStore = create<SettingsStore>((set, get) => ({
|
|
| 173 |
set({ isLoading: true, error: null })
|
| 174 |
try {
|
| 175 |
const previousH5Access = get().h5Access
|
| 176 |
-
const [
|
| 177 |
-
// Fetch models
|
| 178 |
-
|
| 179 |
settingsApi.getPermissionMode(),
|
| 180 |
-
// Read model + effort from ~/.claude.json (TUI-style)
|
| 181 |
getTuiConfig(),
|
| 182 |
settingsApi.getUser(),
|
| 183 |
loadH5AccessSettings(previousH5Access),
|
| 184 |
])
|
| 185 |
|
| 186 |
-
//
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 187 |
let availableModels: ModelInfo[]
|
| 188 |
let activeProviderId: string | null
|
| 189 |
let activeProviderName: string | null
|
| 190 |
|
| 191 |
-
if (
|
| 192 |
-
|
| 193 |
-
|
| 194 |
-
|
| 195 |
-
|
| 196 |
-
|
| 197 |
-
// Never use sidecar's provider name: it may be stale and corrupted
|
| 198 |
-
// (e.g. "TUI: NVIDIA NIM" instead of "NVIDIA").
|
| 199 |
-
const authProvider = config.authProvider as string | undefined
|
| 200 |
-
if (authProvider) {
|
| 201 |
-
const CLI_PROVIDER_NAMES: Record<string, string> = {
|
| 202 |
-
nvidia: 'NVIDIA',
|
| 203 |
-
openrouter: 'OpenRouter',
|
| 204 |
-
opencode: 'OpenCode Zen',
|
| 205 |
-
openai: 'OpenAI',
|
| 206 |
-
local: 'Local',
|
| 207 |
-
anthropic: 'Anthropic',
|
| 208 |
-
}
|
| 209 |
-
availableModels = []
|
| 210 |
-
activeProviderId = `cli-${authProvider}`
|
| 211 |
-
activeProviderName = CLI_PROVIDER_NAMES[authProvider] ?? authProvider
|
| 212 |
} else {
|
| 213 |
-
availableModels =
|
| 214 |
-
activeProviderId = null
|
| 215 |
-
activeProviderName = null
|
| 216 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 217 |
}
|
| 218 |
|
| 219 |
// Derive currentModel from config's model field, matched against availableModels
|
|
@@ -301,10 +324,9 @@ export const useSettingsStore = create<SettingsStore>((set, get) => ({
|
|
| 301 |
syncFromConfig: async () => {
|
| 302 |
// Re-read ~/.claude.json and re-fetch the model list for the new provider.
|
| 303 |
// This keeps settingsStore in sync when cliAuthStore writes to the config file.
|
| 304 |
-
clearProviderModelCache()
|
| 305 |
clearConfigCache()
|
| 306 |
-
const config = await getTuiConfig().catch(() => ({}))
|
| 307 |
-
const authProvider = config?.authProvider as string |
|
| 308 |
if (!authProvider) {
|
| 309 |
set({ activeProviderId: null, activeProviderName: null, availableModels: [], currentModel: null })
|
| 310 |
return
|
|
@@ -320,13 +342,20 @@ export const useSettingsStore = create<SettingsStore>((set, get) => ({
|
|
| 320 |
const cliProviderId = `cli-${authProvider}`
|
| 321 |
const cliProviderName = CLI_NAMES[authProvider] ?? authProvider
|
| 322 |
|
| 323 |
-
// Fetch
|
| 324 |
-
const
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 325 |
set({
|
| 326 |
activeProviderId: cliProviderId,
|
| 327 |
activeProviderName: cliProviderName,
|
| 328 |
-
availableModels
|
| 329 |
-
currentModel:
|
| 330 |
})
|
| 331 |
},
|
| 332 |
|
|
|
|
| 1 |
import { create } from 'zustand'
|
| 2 |
import { ApiError } from '../api/client'
|
| 3 |
import { settingsApi } from '../api/settings'
|
| 4 |
+
import { modelsApi } from '../api/models'
|
| 5 |
import { h5AccessApi } from '../api/h5Access'
|
|
|
|
| 6 |
import { getTuiConfig, saveTuiConfigPatch, clearConfigCache } from '../api/config'
|
| 7 |
import {
|
| 8 |
isThemeMode,
|
|
|
|
| 40 |
export const UI_ZOOM_DEFAULT = DEFAULT_APP_ZOOM
|
| 41 |
let desktopNotificationsSaveQueue: Promise<void> = Promise.resolve()
|
| 42 |
|
| 43 |
+
function buildDefaultCliModels(providerKey: string): ModelInfo[] {
|
| 44 |
+
switch (providerKey) {
|
| 45 |
+
case 'nvidia':
|
| 46 |
+
return [
|
| 47 |
+
{ id: 'nvidia/llama-3.1-nemotron-70b-instruct', name: 'Nemotron 70B', description: 'Default NVIDIA model', context: '' },
|
| 48 |
+
]
|
| 49 |
+
case 'openai':
|
| 50 |
+
return [
|
| 51 |
+
{ id: 'gpt-5.4-codex', name: 'GPT-5.4 Codex', description: 'Default OpenAI model', context: '' },
|
| 52 |
+
{ id: 'gpt-5.4-mini', name: 'GPT-5.4 Mini', description: 'Fast OpenAI option', context: '' },
|
| 53 |
+
]
|
| 54 |
+
case 'openrouter':
|
| 55 |
+
return [
|
| 56 |
+
{ id: 'openrouter/default', name: 'OpenRouter Default', description: 'Set via OpenRouter dashboard', context: '' },
|
| 57 |
+
]
|
| 58 |
+
case 'local':
|
| 59 |
+
return [
|
| 60 |
+
{ id: 'local/default', name: 'Local Default', description: 'Check your local model server', context: '' },
|
| 61 |
+
]
|
| 62 |
+
default:
|
| 63 |
+
return []
|
| 64 |
+
}
|
| 65 |
+
}
|
| 66 |
+
|
| 67 |
function getStoredLocale(): Locale {
|
| 68 |
try {
|
| 69 |
const stored = localStorage.getItem(LOCALE_STORAGE_KEY)
|
|
|
|
| 197 |
set({ isLoading: true, error: null })
|
| 198 |
try {
|
| 199 |
const previousH5Access = get().h5Access
|
| 200 |
+
const [sidecarResult, { mode }, config, userSettings, h5AccessResult] = await Promise.all([
|
| 201 |
+
// Fetch models from sidecar proxy (server-side, bypasses CORS)
|
| 202 |
+
modelsApi.list().catch(() => ({ models: [] as ModelInfo[], provider: null as { id: string; name: string } | null })),
|
| 203 |
settingsApi.getPermissionMode(),
|
| 204 |
+
// Read model + effort from ~/.claude.json (TUI-style)
|
| 205 |
getTuiConfig(),
|
| 206 |
settingsApi.getUser(),
|
| 207 |
loadH5AccessSettings(previousH5Access),
|
| 208 |
])
|
| 209 |
|
| 210 |
+
// Derive provider info from ~/.claude.json authProvider (correct names).
|
| 211 |
+
// Sidecar's provider name may be stale/corrupted (e.g. "TUI: NVIDIA NIM").
|
| 212 |
+
const CLI_NAMES: Record<string, string> = {
|
| 213 |
+
nvidia: 'NVIDIA',
|
| 214 |
+
openrouter: 'OpenRouter',
|
| 215 |
+
opencode: 'OpenCode Zen',
|
| 216 |
+
openai: 'OpenAI',
|
| 217 |
+
local: 'Local',
|
| 218 |
+
anthropic: 'Anthropic',
|
| 219 |
+
}
|
| 220 |
+
const authProvider = config.authProvider as string | undefined
|
| 221 |
let availableModels: ModelInfo[]
|
| 222 |
let activeProviderId: string | null
|
| 223 |
let activeProviderName: string | null
|
| 224 |
|
| 225 |
+
if (authProvider) {
|
| 226 |
+
activeProviderId = `cli-${authProvider}`
|
| 227 |
+
activeProviderName = CLI_NAMES[authProvider] ?? authProvider
|
| 228 |
+
// Use sidecar models if available (from proxy), else build defaults
|
| 229 |
+
if (sidecarResult.models.length > 0) {
|
| 230 |
+
availableModels = sidecarResult.models
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 231 |
} else {
|
| 232 |
+
availableModels = buildDefaultCliModels(authProvider)
|
|
|
|
|
|
|
| 233 |
}
|
| 234 |
+
} else {
|
| 235 |
+
activeProviderId = sidecarResult.provider?.id ?? null
|
| 236 |
+
activeProviderName = sidecarResult.provider?.name ?? null
|
| 237 |
+
availableModels = sidecarResult.models.length > 0
|
| 238 |
+
? sidecarResult.models
|
| 239 |
+
: []
|
| 240 |
}
|
| 241 |
|
| 242 |
// Derive currentModel from config's model field, matched against availableModels
|
|
|
|
| 324 |
syncFromConfig: async () => {
|
| 325 |
// Re-read ~/.claude.json and re-fetch the model list for the new provider.
|
| 326 |
// This keeps settingsStore in sync when cliAuthStore writes to the config file.
|
|
|
|
| 327 |
clearConfigCache()
|
| 328 |
+
const config = await getTuiConfig().catch(() => ({ authProvider: null }))
|
| 329 |
+
const authProvider = (config?.authProvider ?? null) as string | null
|
| 330 |
if (!authProvider) {
|
| 331 |
set({ activeProviderId: null, activeProviderName: null, availableModels: [], currentModel: null })
|
| 332 |
return
|
|
|
|
| 342 |
const cliProviderId = `cli-${authProvider}`
|
| 343 |
const cliProviderName = CLI_NAMES[authProvider] ?? authProvider
|
| 344 |
|
| 345 |
+
// Fetch models from sidecar proxy (server-side, bypasses CORS)
|
| 346 |
+
const sidecarResult = await modelsApi.list().catch(() => ({
|
| 347 |
+
models: [] as ModelInfo[],
|
| 348 |
+
provider: null as { id: string; name: string } | null,
|
| 349 |
+
}))
|
| 350 |
+
|
| 351 |
+
// Fallback chain: sidecar models → default CLI models
|
| 352 |
+
const defaults = buildDefaultCliModels(authProvider)
|
| 353 |
+
const availableModels = sidecarResult.models.length > 0 ? sidecarResult.models : defaults
|
| 354 |
set({
|
| 355 |
activeProviderId: cliProviderId,
|
| 356 |
activeProviderName: cliProviderName,
|
| 357 |
+
availableModels,
|
| 358 |
+
currentModel: availableModels[0] ?? null,
|
| 359 |
})
|
| 360 |
},
|
| 361 |
|