chenbhao commited on
Commit
85a51a2
Β·
1 Parent(s): 4cb291d

feat: provider model list in desktop and fix bun run tauri dev can't keep

Browse files
desktop/src/components/controls/ModelSelector.tsx CHANGED
@@ -1,9 +1,8 @@
1
  import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react'
2
  import { createPortal } from 'react-dom'
3
- import { OFFICIAL_DEFAULT_MODEL_ID, OFFICIAL_MODELS } from '../../constants/modelCatalog'
4
  import {
5
  OPENAI_OFFICIAL_DEFAULT_MODEL_ID,
6
- OPENAI_OFFICIAL_MODELS,
7
  OPENAI_OFFICIAL_PROVIDER_ID,
8
  } from '../../constants/openaiOfficialProvider'
9
  import { useTranslation } from '../../i18n'
@@ -42,88 +41,59 @@ type DropdownPosition = {
42
  maxHeight: number
43
  }
44
 
 
 
 
 
 
 
 
45
  const DROPDOWN_WIDTH = 360
46
  const DROPDOWN_GAP = 8
47
  const VIEWPORT_MARGIN = 16
48
  const DROPDOWN_MAX_HEIGHT = 420
49
  const DROPDOWN_MIN_HEIGHT = 180
50
 
51
- function officialChoices(
52
- providerId: string | null,
53
- models: ModelInfo[],
54
- isDefault: boolean,
55
- officialName: string,
56
- ): ProviderChoice {
57
- return {
58
- providerId,
59
- providerName: officialName,
60
- isDefault,
61
- models,
62
- }
63
- }
 
 
 
 
 
64
 
65
- function buildProviderModels(
66
- provider: SavedProvider,
67
- labels: Record<'main' | 'haiku' | 'sonnet' | 'opus', string>,
68
- ): ModelInfo[] {
69
- const entries: Array<{ id: string; label: string }> = [
70
- { id: provider.models.main.trim(), label: labels.main },
71
- { id: provider.models.haiku.trim(), label: labels.haiku },
72
- { id: provider.models.sonnet.trim(), label: labels.sonnet },
73
- { id: provider.models.opus.trim(), label: labels.opus },
74
- ]
75
 
76
- const byId = new Map<string, { id: string; labels: string[] }>()
77
- for (const entry of entries) {
78
- if (!entry.id) continue
79
- const existing = byId.get(entry.id)
80
- if (existing) {
81
- if (!existing.labels.includes(entry.label)) {
82
- existing.labels.push(entry.label)
83
- }
84
- continue
85
  }
86
- byId.set(entry.id, { id: entry.id, labels: [entry.label] })
87
  }
88
 
89
- return [...byId.values()].map((entry) => ({
90
- id: entry.id,
91
- name: entry.id,
92
- description: entry.labels.join(' Β· '),
93
- context: '',
94
- }))
95
- }
96
-
97
- function buildProviderChoices(
98
- providers: SavedProvider[],
99
- activeId: string | null,
100
- availableModels: ModelInfo[],
101
- officialName: string,
102
- openAIOfficialName: string,
103
- labels: Record<'main' | 'haiku' | 'sonnet' | 'opus', string>,
104
- ): ProviderChoice[] {
105
- const claudeOfficialModels = activeId === null && availableModels.length > 0
106
- ? availableModels
107
- : OFFICIAL_MODELS
108
- const openAIOfficialModels = activeId === OPENAI_OFFICIAL_PROVIDER_ID && availableModels.length > 0
109
- ? availableModels
110
- : OPENAI_OFFICIAL_MODELS
111
-
112
- return [
113
- officialChoices(null, claudeOfficialModels, activeId === null, officialName),
114
- officialChoices(
115
- OPENAI_OFFICIAL_PROVIDER_ID,
116
- openAIOfficialModels,
117
- activeId === OPENAI_OFFICIAL_PROVIDER_ID,
118
- openAIOfficialName,
119
- ),
120
- ...providers.map((provider) => ({
121
- providerId: provider.id,
122
- providerName: provider.name,
123
- isDefault: activeId === provider.id,
124
- models: buildProviderModels(provider, labels),
125
- })),
126
- ]
127
  }
128
 
129
  function resolveDefaultRuntimeSelection(
@@ -163,6 +133,7 @@ export function ModelSelector({
163
  currentModel: storeModel,
164
  availableModels,
165
  effortLevel,
 
166
  activeProviderName,
167
  setModel,
168
  setEffort,
@@ -272,26 +243,13 @@ export function ModelSelector({
272
  }
273
  }, [open, updateDropdownPosition])
274
 
275
- const roleLabels = useMemo(
276
- () => ({
277
- main: t('settings.providers.mainModel'),
278
- haiku: t('settings.providers.haikuModel'),
279
- sonnet: t('settings.providers.sonnetModel'),
280
- opus: t('settings.providers.opusModel'),
281
- }),
282
- [t],
283
- )
284
-
285
  const providerChoices = useMemo(
286
  () => buildProviderChoices(
287
  providers,
288
- activeId,
289
  availableModels,
290
- t('settings.providers.officialName'),
291
- t('settings.providers.openaiOfficialName'),
292
- roleLabels,
293
  ),
294
- [activeId, availableModels, providers, roleLabels, t],
295
  )
296
 
297
  const selectedModel = isControlled
 
1
  import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react'
2
  import { createPortal } from 'react-dom'
3
+ import { OFFICIAL_DEFAULT_MODEL_ID } from '../../constants/modelCatalog'
4
  import {
5
  OPENAI_OFFICIAL_DEFAULT_MODEL_ID,
 
6
  OPENAI_OFFICIAL_PROVIDER_ID,
7
  } from '../../constants/openaiOfficialProvider'
8
  import { useTranslation } from '../../i18n'
 
41
  maxHeight: number
42
  }
43
 
44
+ const CLI_PROVIDER_NAMES: Record<string, string> = {
45
+ nvidia: 'NVIDIA',
46
+ openrouter: 'OpenRouter',
47
+ opencode: 'OpenCode Zen',
48
+ local: 'Local',
49
+ }
50
+
51
  const DROPDOWN_WIDTH = 360
52
  const DROPDOWN_GAP = 8
53
  const VIEWPORT_MARGIN = 16
54
  const DROPDOWN_MAX_HEIGHT = 420
55
  const DROPDOWN_MIN_HEIGHT = 180
56
 
57
+ function buildProviderChoices(
58
+ providers: SavedProvider[],
59
+ availableModels: ModelInfo[],
60
+ activeProviderId: string | null,
61
+ ): ProviderChoice[] {
62
+ if (activeProviderId && availableModels.length > 0) {
63
+ const isCli = activeProviderId.startsWith('cli-')
64
+
65
+ if (isCli) {
66
+ const cliProviderKey = activeProviderId.replace('cli-', '')
67
+ const cliProviderName = CLI_PROVIDER_NAMES[cliProviderKey] || activeProviderId
68
+ return [{
69
+ providerId: activeProviderId,
70
+ providerName: cliProviderName,
71
+ isDefault: true,
72
+ models: availableModels,
73
+ }]
74
+ }
75
 
76
+ if (activeProviderId === OPENAI_OFFICIAL_PROVIDER_ID) {
77
+ return [{
78
+ providerId: OPENAI_OFFICIAL_PROVIDER_ID,
79
+ providerName: 'OpenAI',
80
+ isDefault: true,
81
+ models: availableModels,
82
+ }]
83
+ }
 
 
84
 
85
+ const provider = providers.find(p => p.id === activeProviderId)
86
+ if (provider) {
87
+ return [{
88
+ providerId: provider.id,
89
+ providerName: provider.name,
90
+ isDefault: true,
91
+ models: availableModels,
92
+ }]
 
93
  }
 
94
  }
95
 
96
+ return []
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
97
  }
98
 
99
  function resolveDefaultRuntimeSelection(
 
133
  currentModel: storeModel,
134
  availableModels,
135
  effortLevel,
136
+ activeProviderId,
137
  activeProviderName,
138
  setModel,
139
  setEffort,
 
243
  }
244
  }, [open, updateDropdownPosition])
245
 
 
 
 
 
 
 
 
 
 
 
246
  const providerChoices = useMemo(
247
  () => buildProviderChoices(
248
  providers,
 
249
  availableModels,
250
+ activeProviderId,
 
 
251
  ),
252
+ [activeProviderId, availableModels, providers],
253
  )
254
 
255
  const selectedModel = isControlled
desktop/src/stores/settingsStore.ts CHANGED
@@ -53,6 +53,7 @@ type SettingsStore = {
53
  effortLevel: EffortLevel
54
  thinkingEnabled: boolean
55
  availableModels: ModelInfo[]
 
56
  activeProviderName: string | null
57
  locale: Locale
58
  theme: ThemeMode
@@ -134,6 +135,7 @@ export const useSettingsStore = create<SettingsStore>((set, get) => ({
134
  effortLevel: 'medium',
135
  thinkingEnabled: true,
136
  availableModels: [],
 
137
  activeProviderName: null,
138
  locale: getStoredLocale(),
139
  theme: useUIStore.getState().theme,
@@ -181,6 +183,7 @@ export const useSettingsStore = create<SettingsStore>((set, get) => ({
181
  set({
182
  permissionMode: mode,
183
  availableModels: modelsRes.models,
 
184
  activeProviderName: modelsRes.provider?.name ?? null,
185
  currentModel: model,
186
  effortLevel: level,
 
53
  effortLevel: EffortLevel
54
  thinkingEnabled: boolean
55
  availableModels: ModelInfo[]
56
+ activeProviderId: string | null
57
  activeProviderName: string | null
58
  locale: Locale
59
  theme: ThemeMode
 
135
  effortLevel: 'medium',
136
  thinkingEnabled: true,
137
  availableModels: [],
138
+ activeProviderId: null,
139
  activeProviderName: null,
140
  locale: getStoredLocale(),
141
  theme: useUIStore.getState().theme,
 
183
  set({
184
  permissionMode: mode,
185
  availableModels: modelsRes.models,
186
+ activeProviderId: modelsRes.provider?.id ?? null,
187
  activeProviderName: modelsRes.provider?.name ?? null,
188
  currentModel: model,
189
  effortLevel: level,
src/server/api/models.ts CHANGED
@@ -247,6 +247,32 @@ async function fetchCliProviderModels(): Promise<ApiModelInfo[]> {
247
  // fall through
248
  }
249
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
250
  } catch {
251
  // fall through
252
  }
@@ -314,53 +340,49 @@ const CLI_PROVIDER_NAMES: Record<string, string> = {
314
  }
315
 
316
  async function handleModelsList(): Promise<Response> {
317
- // First check if CLI has an authProvider configured β€” this takes precedence
318
- // over cc-haha providers since CLI's /login command manages it
319
  const cliConfig = await readCliAuthProvider()
320
  const cliAuthProvider = cliConfig?.authProvider
321
 
322
- if (cliAuthProvider && cliAuthProvider !== 'anthropic' && cliAuthProvider !== 'openai') {
323
- const cliModels = await fetchCliProviderModels()
324
- if (cliModels.length > 0) {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
325
  return Response.json({
326
- models: cliModels,
327
- provider: {
328
- id: `cli-${cliAuthProvider}`,
329
- name: CLI_PROVIDER_NAMES[cliAuthProvider] || cliAuthProvider,
330
- },
331
  })
332
  }
333
- // If no models fetched yet but authProvider is set, still return provider info
334
- // The model list will be populated once available
335
- return Response.json({
336
- models: [],
337
- provider: {
338
- id: `cli-${cliAuthProvider}`,
339
- name: CLI_PROVIDER_NAMES[cliAuthProvider] || cliAuthProvider,
340
- },
341
- })
342
  }
343
 
344
- const { providers, activeId } = await providerService.listProviders()
345
- if (isOpenAIOfficialProviderId(activeId)) {
346
  return Response.json({
347
- models: buildOpenAIModelList(),
348
  provider: {
349
- id: OPENAI_OFFICIAL_PROVIDER_ID,
350
- name: OPENAI_OFFICIAL_PROVIDER_NAME,
351
  },
352
  })
353
  }
354
 
355
- const activeProvider = activeId ? providers.find((p) => p.id === activeId) : null
356
- if (activeProvider) {
357
- const modelList = buildProviderModelList(activeProvider.models)
358
- return Response.json({
359
- models: modelList,
360
- provider: { id: activeProvider.id, name: activeProvider.name },
361
- })
362
- }
363
-
364
  return Response.json({ models: getStandaloneModelList(), provider: null })
365
  }
366
 
 
247
  // fall through
248
  }
249
  }
250
+
251
+ // ── NVIDIA NIM ────────────────────────────────────────────
252
+ if (authProvider === 'nvidia' && config.nvidiaApiKey) {
253
+ try {
254
+ const res = await fetch('https://integrate.api.nvidia.com/v1/models', {
255
+ headers: { Authorization: `Bearer ${config.nvidiaApiKey}` },
256
+ })
257
+ if (res.ok) {
258
+ const data = await res.json() as any
259
+ const models: ApiModelInfo[] = (data.data || []).map((m: any) => ({
260
+ id: m.id,
261
+ name: m.id,
262
+ description: m.owned_by || '',
263
+ context: '',
264
+ }))
265
+
266
+ if (models.length > 0) {
267
+ cliProviderModelCache = models
268
+ cliProviderModelCacheTime = Date.now()
269
+ return models
270
+ }
271
+ }
272
+ } catch {
273
+ // fall through
274
+ }
275
+ }
276
  } catch {
277
  // fall through
278
  }
 
340
  }
341
 
342
  async function handleModelsList(): Promise<Response> {
 
 
343
  const cliConfig = await readCliAuthProvider()
344
  const cliAuthProvider = cliConfig?.authProvider
345
 
346
+ const { providers, activeId } = await providerService.listProviders()
347
+
348
+ if (activeId) {
349
+ const activeProvider = providers.find((p) => p.id === activeId)
350
+ if (activeProvider) {
351
+ if (activeProvider.presetId === 'tui-nvidia' && cliAuthProvider === 'nvidia') {
352
+ const cliModels = await fetchCliProviderModels()
353
+ if (cliModels.length > 0) {
354
+ return Response.json({
355
+ models: cliModels,
356
+ provider: { id: activeProvider.id, name: activeProvider.name },
357
+ })
358
+ }
359
+ }
360
+
361
+ if (isOpenAIOfficialProviderId(activeId)) {
362
+ return Response.json({
363
+ models: buildOpenAIModelList(),
364
+ provider: { id: OPENAI_OFFICIAL_PROVIDER_ID, name: OPENAI_OFFICIAL_PROVIDER_NAME },
365
+ })
366
+ }
367
+
368
  return Response.json({
369
+ models: buildProviderModelList(activeProvider.models),
370
+ provider: { id: activeProvider.id, name: activeProvider.name },
 
 
 
371
  })
372
  }
 
 
 
 
 
 
 
 
 
373
  }
374
 
375
+ if (cliAuthProvider && cliAuthProvider !== 'anthropic' && cliAuthProvider !== 'openai') {
376
+ const cliModels = await fetchCliProviderModels()
377
  return Response.json({
378
+ models: cliModels,
379
  provider: {
380
+ id: `cli-${cliAuthProvider}`,
381
+ name: CLI_PROVIDER_NAMES[cliAuthProvider] || cliAuthProvider,
382
  },
383
  })
384
  }
385
 
 
 
 
 
 
 
 
 
 
386
  return Response.json({ models: getStandaloneModelList(), provider: null })
387
  }
388