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
|
| 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
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 64 |
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
{ id: provider.models.opus.trim(), label: labels.opus },
|
| 74 |
-
]
|
| 75 |
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
}
|
| 84 |
-
continue
|
| 85 |
}
|
| 86 |
-
byId.set(entry.id, { id: entry.id, labels: [entry.label] })
|
| 87 |
}
|
| 88 |
|
| 89 |
-
return [
|
| 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 |
-
|
| 291 |
-
t('settings.providers.openaiOfficialName'),
|
| 292 |
-
roleLabels,
|
| 293 |
),
|
| 294 |
-
[
|
| 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 |
-
|
| 323 |
-
|
| 324 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 325 |
return Response.json({
|
| 326 |
-
models:
|
| 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 |
-
|
| 345 |
-
|
| 346 |
return Response.json({
|
| 347 |
-
models:
|
| 348 |
provider: {
|
| 349 |
-
id:
|
| 350 |
-
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 |
|