Selaa lähdekoodia

feat: 引擎信息显示模型的实际版本

- 引擎状态增加 modelLabels:Claude 用 SDK 的 supportedModels 把别名解析为实际版本
  (如 sonnet → Sonnet 5.5 · claude-sonnet-5-5,不调用模型,缓存 10 分钟),Codex 取 model/list 的显示名
- 概览页的默认模型与可选模型、Playground 的模型选择、Profile 的模型输入都显示实际版本

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Jerry Liang 1 viikko sitten
vanhempi
commit
716304c57c

+ 7 - 2
apps/admin/src/views/OverviewView.vue

@@ -199,8 +199,13 @@ onMounted(load)
             </div>
             <span class="text-fg-muted">账户</span><span>{{ p.account ?? '—' }}</span>
             <span class="text-fg-muted">CLI</span><span class="font-mono break-all">{{ p.cliPath }} <span class="text-fg-muted">{{ p.version }}</span></span>
-            <span class="text-fg-muted">默认模型</span><span>{{ p.defaultModel || '—' }}</span>
-            <span class="text-fg-muted">可选模型</span><span>{{ p.models.join('、') || '—' }}</span>
+            <span class="text-fg-muted">默认模型</span>
+            <span>{{ p.defaultModel || '—' }}<span v-if="p.modelLabels?.[p.defaultModel]" class="text-fg-muted">({{ p.modelLabels[p.defaultModel] }})</span></span>
+            <span class="text-fg-muted">可选模型</span>
+            <span v-if="!p.models.length">—</span>
+            <span v-else class="flex flex-col">
+              <span v-for="m in p.models" :key="m">{{ m }}<span v-if="p.modelLabels?.[m]" class="text-fg-muted"> · {{ p.modelLabels[m] }}</span></span>
+            </span>
             <template v-if="p.message"><span class="text-fg-muted">说明</span><span class="text-warning">{{ p.message }}</span></template>
             <template v-if="usage">
               <span class="text-fg-muted">本服务用量</span>

+ 2 - 1
apps/admin/src/views/PlaygroundView.vue

@@ -50,6 +50,7 @@ async function saveAsProfile() {
 const started = computed(() => (chat.value?.state.items.length ?? 0) > 0)
 const idle = computed(() => chat.value?.state.runState === 'idle')
 const models = computed(() => providers.value.find(p => p.id === detail.value?.provider)?.models ?? [])
+const modelLabels = computed(() => providers.value.find(p => p.id === detail.value?.provider)?.modelLabels ?? {})
 
 async function loadSessions() {
   try {
@@ -230,7 +231,7 @@ onBeforeUnmount(() => {
                 <label class="pill" title="模型">
                   <span class="text-fg-muted">{{ detail.provider }}</span>
                   <select class="pill-select" :value="detail.model" :disabled="!idle" aria-label="模型" @change="update({ model: ($event.target as HTMLSelectElement).value })">
-                    <option v-for="m in (models.includes(detail.model) ? models : [detail.model, ...models])" :key="m" :value="m">{{ m }}</option>
+                    <option v-for="m in (models.includes(detail.model) ? models : [detail.model, ...models])" :key="m" :value="m">{{ m }}{{ modelLabels[m] ? ` · ${modelLabels[m]}` : '' }}</option>
                   </select>
                 </label>
                 <label class="pill" title="权限模式">

+ 3 - 1
apps/admin/src/views/ProfilesView.vue

@@ -21,6 +21,7 @@ const id = computed(() => (route.params.id as string | undefined) || null)
 const isNew = computed(() => id.value === 'new')
 const current = computed(() => profiles.value.find(p => p.id === id.value) ?? null)
 const models = computed(() => providers.value.find(p => p.id === draft.value?.provider)?.models ?? [])
+const modelLabels = computed(() => providers.value.find(p => p.id === draft.value?.provider)?.modelLabels ?? {})
 
 const blank = (): ProfileInput => ({
   name: '', description: '', provider: 'claude', model: providers.value.find(p => p.id === 'claude')?.defaultModel ?? 'sonnet',
@@ -187,7 +188,8 @@ onMounted(() => load().catch(err => toast(errorMessage(err), 'error')))
             <div>
               <label class="label" for="p-model">模型</label>
               <input id="p-model" v-model="draft.model" class="field" list="p-models">
-              <datalist id="p-models"><option v-for="m in models" :key="m" :value="m" /></datalist>
+              <datalist id="p-models"><option v-for="m in models" :key="m" :value="m" :label="modelLabels[m]" /></datalist>
+              <div v-if="modelLabels[draft.model]" class="hint mt-1">{{ modelLabels[draft.model] }}</div>
             </div>
             <div>
               <label class="label" for="p-mode">权限模式</label>

+ 30 - 0
apps/server/src/providers/claude/adapter.ts

@@ -27,6 +27,35 @@ export class ClaudeProvider implements AgentProvider {
   /** 对话中引擎报告的额度(rate_limit_event),查询额度失败时作为后备 */
   private lastRateLimits = new Map<string, QuotaWindowDto>()
   private quotaCache: { at: number; value: ProviderQuotaDto } | null = null
+  private labelsCache: { at: number; value: Record<string, string> } | null = null
+
+  /**
+   * 模型别名对应的实际版本(如 sonnet → Sonnet 5.5 · claude-sonnet-5-5):用 SDK 的 supportedModels 查询,
+   * 不发送消息、不调用模型;缓存 10 分钟,失败时不显示
+   */
+  private async modelLabels(settings: AppSettings): Promise<Record<string, string>> {
+    if (this.labelsCache && Date.now() - this.labelsCache.at < 600_000) return this.labelsCache.value
+    const ac = new AbortController()
+    const timer = setTimeout(() => ac.abort(), 30_000)
+    try {
+      const q = query({
+        prompt: idleInput(ac.signal),
+        options: { abortController: ac, settingSources: [], cwd: settings.paths.workspaceRoot, pathToClaudeCodeExecutable: settings.providers.claude.cliPath },
+      })
+      const value: Record<string, string> = {}
+      for (const m of await q.supportedModels()) {
+        if (m.value === 'default') continue
+        value[m.value] = m.resolvedModel && m.resolvedModel !== m.value ? `${m.displayName} · ${m.resolvedModel}` : m.displayName
+      }
+      this.labelsCache = { at: Date.now(), value }
+      return value
+    } catch {
+      return {}
+    } finally {
+      clearTimeout(timer)
+      ac.abort()
+    }
+  }
 
   async run(ctx: RunContext): Promise<RunResult> {
     try {
@@ -227,6 +256,7 @@ export class ClaudeProvider implements AgentProvider {
       const info = JSON.parse(auth.stdout) as { loggedIn?: boolean; subscriptionType?: string; email?: string }
       return {
         ...base,
+        modelLabels: info.loggedIn ? await this.modelLabels(settings) : undefined,
         version: ver.stdout.trim().split(' ')[0] || null,
         loggedIn: !!info.loggedIn,
         available: s.enabled && !!info.loggedIn,

+ 3 - 1
apps/server/src/providers/codex/adapter.ts

@@ -271,18 +271,20 @@ export class CodexProvider implements AgentProvider {
       const acc = await client.request<GetAccountResponse>('account/read', {}, 30_000)
       const loggedIn = !!acc.account
       let models = s.models
+      const modelLabels: Record<string, string> = {}
       if (loggedIn) {
         const list = await client.request<ModelListResponse>('model/list', {}, 30_000).catch(() => null)
         if (list) {
           this.models = list.data.filter(m => !m.hidden).map(m => m.model)
           if (!models.length) models = this.models
+          for (const m of list.data) if (m.displayName && m.displayName !== m.model) modelLabels[m.model] = m.displayName
         }
       }
       const account = acc.account?.type === 'chatgpt'
         ? [acc.account.email, `(${acc.account.planType})`].filter(Boolean).join(' ')
         : acc.account?.type ?? null
       return {
-        ...base, version: this.version, loggedIn, account, models,
+        ...base, version: this.version, loggedIn, account, models, modelLabels,
         defaultModel: s.defaultModel || models[0] || '',
         available: s.enabled && loggedIn,
         message: loggedIn ? (s.enabled ? null : '已登录,尚未在"设置 → 引擎"中启用') : '未登录:点击"登录"按钮,用 ChatGPT 账号完成设备码登录',

+ 2 - 0
packages/shared/src/api.ts

@@ -136,6 +136,8 @@ export interface ProviderStatusDto {
   version: string | null
   models: string[]
   defaultModel: string
+  /** 模型的显示名与实际版本,如 sonnet → "Sonnet 5.5 · claude-sonnet-5-5"(引擎能提供时) */
+  modelLabels?: Record<string, string>
   message: string | null
 }