chenbhao Claude Big Pickle commited on
Commit
0892211
·
1 Parent(s): e40073c

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
- apiFormat === 'openai' &&
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 'openai': {
 
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 [directModels, { mode }, config, userSettings, h5AccessResult] = await Promise.all([
177
- // Fetch models directly from provider APIs (TUI-style)
178
- fetchProviderModels().catch(() => ({ models: [] as ModelInfo[], provider: null as { id: string; name: string } | null })),
179
  settingsApi.getPermissionMode(),
180
- // Read model + effort from ~/.claude.json (TUI-style), bypassing sidecar
181
  getTuiConfig(),
182
  settingsApi.getUser(),
183
  loadH5AccessSettings(previousH5Access),
184
  ])
185
 
186
- // Use direct models if available (TUI-style), otherwise fall back to sidecar
 
 
 
 
 
 
 
 
 
 
187
  let availableModels: ModelInfo[]
188
  let activeProviderId: string | null
189
  let activeProviderName: string | null
190
 
191
- if (directModels.models.length > 0) {
192
- availableModels = directModels.models
193
- activeProviderId = directModels.provider?.id ?? null
194
- activeProviderName = directModels.provider?.name ?? null
195
- } else {
196
- // No direct models — always derive from ~/.claude.json authProvider.
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 | undefined
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 fresh models for the new provider
324
- const { models } = await fetchProviderModels().catch(() => ({ models: [] as ModelInfo[], provider: null }))
 
 
 
 
 
 
 
325
  set({
326
  activeProviderId: cliProviderId,
327
  activeProviderName: cliProviderName,
328
- availableModels: models,
329
- currentModel: models.length > 0 ? models[0] : null,
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