From 8bd28b4e65965497f6f3aaaddf11007d94c6a4d3 Mon Sep 17 00:00:00 2001 From: Pedro Ivo Date: Sat, 8 Aug 2026 16:36:58 -0300 Subject: [PATCH] fix(cli): show the active model provider --- .../PromptInput/PromptInputFooter.tsx | 7 +- src/components/StartupScreen.test.ts | 37 +++++++++++ src/components/StartupScreen.ts | 54 ++++++++++------ src/utils/model/activeModelIdentity.test.ts | 24 +++++++ src/utils/model/activeModelIdentity.ts | 64 +++++++++++++++++++ 5 files changed, 166 insertions(+), 20 deletions(-) create mode 100644 src/utils/model/activeModelIdentity.test.ts create mode 100644 src/utils/model/activeModelIdentity.ts diff --git a/src/components/PromptInput/PromptInputFooter.tsx b/src/components/PromptInput/PromptInputFooter.tsx index 8e365768c6..dca57b787f 100644 --- a/src/components/PromptInput/PromptInputFooter.tsx +++ b/src/components/PromptInput/PromptInputFooter.tsx @@ -21,6 +21,7 @@ import { getContextWindowForModel } from '../../utils/context.js'; import { formatNumber } from '../../utils/format.js'; import { isFullscreenEnvEnabled } from '../../utils/fullscreen.js'; import { getRuntimeMainLoopModel } from '../../utils/model/model.js'; +import { getActiveModelIdentity } from '../../utils/model/activeModelIdentity.js'; import type { PermissionMode } from '../../utils/permissions/PermissionMode.js'; import { doesMostRecentAssistantMessageExceed200k, tokenCountWithEstimation } from '../../utils/tokens.js'; import { isUndercover } from '../../utils/undercover.js'; @@ -43,6 +44,7 @@ function ContextWindowDisplayInner({ messages, permissionMode }: { const mainLoopModel = useMainLoopModel(); const exceeds200k = useMemo(() => doesMostRecentAssistantMessageExceed200k(messages), [messages]); const runtimeModel = getRuntimeMainLoopModel({ permissionMode, mainLoopModel, exceeds200kTokens: exceeds200k }); + const activeModel = getActiveModelIdentity(runtimeModel); const windowSize = getContextWindowForModel(runtimeModel, getSdkBetas()); const { avgRate10s, isGenerating } = useTokenRateDetailed(messages); @@ -56,7 +58,10 @@ function ContextWindowDisplayInner({ messages, permissionMode }: { const windowK = formatNumber(windowSize); return ( - + + {activeModel.provider} + {activeModel.model} + ยท context {pct}% ยท diff --git a/src/components/StartupScreen.test.ts b/src/components/StartupScreen.test.ts index af00c2ace3..a57800058d 100644 --- a/src/components/StartupScreen.test.ts +++ b/src/components/StartupScreen.test.ts @@ -41,6 +41,10 @@ const originalWrite = process.stdout.write async function importStartupScreenWithModels( models: Array<{ id: string }> = [{ id: 'early-adopters/qwen3.6-27b' }], settingsModel?: string, + optionalModels: { + codex?: Array<{ id: string }> + claude?: Array<{ id: string }> + } = {}, ) { mock.restore() setSessionSettingsCache({ @@ -57,6 +61,12 @@ async function importStartupScreenWithModels( getVerbooModelMeta: (modelId: string) => models.find(model => model.id === modelId), })) + mock.module('../services/api/codexModels.js', () => ({ + getCachedCodexModels: () => optionalModels.codex ?? [], + })) + mock.module('../services/api/claudeNativeModels.js', () => ({ + getCachedClaudeNativeModels: () => optionalModels.claude ?? [], + })) const nonce = `${Date.now()}-${Math.random()}` return import(`./StartupScreen.js?ts=${nonce}`) } @@ -151,6 +161,32 @@ describe('detectProvider โ€” Verboo isolation', () => { expect(result.model).toBe('early-adopters/qwen3.6-27b') }) + test('labels an active Codex model from the Codex catalog', async () => { + const { detectProvider } = await importStartupScreenWithModels( + [{ id: 'early-adopters/qwen3.6-27b' }], + undefined, + { codex: [{ id: 'gpt-5.5' }] }, + ) + + expect(detectProvider('gpt-5.5')).toMatchObject({ + name: 'Codex', + model: 'gpt-5.5', + }) + }) + + test('labels an active Claude model from the Claude catalog', async () => { + const { detectProvider } = await importStartupScreenWithModels( + [{ id: 'early-adopters/qwen3.6-27b' }], + undefined, + { claude: [{ id: 'claude-opus-4-6' }] }, + ) + + expect(detectProvider('claude-opus-4-6')).toMatchObject({ + name: 'Claude', + model: 'claude-opus-4-6', + }) + }) + test('uses persisted Verboo model when no CLI override is provided', async () => { const { detectProvider } = await importStartupScreenWithModels( [ @@ -200,6 +236,7 @@ describe('renderStartupScreen', () => { expect(plainOutput).toContain('๐Ÿ‘ป') expect(plainOutput).toContain('Verboo Code') + expect(plainOutput).toContain('Verboo ยท early-adopters/qwen3.6-27b') expect(plainOutput).not.toContain('โ–„โ–€โ–€โ–€โ–€โ–€โ–€โ–€โ–„') expect(plainOutput).not.toContain('Tokens ilimitados') expect(output).toContain('\x1b[0m') diff --git a/src/components/StartupScreen.ts b/src/components/StartupScreen.ts index e2ef1e152a..68a81bd759 100644 --- a/src/components/StartupScreen.ts +++ b/src/components/StartupScreen.ts @@ -4,10 +4,14 @@ */ import { VERBOO_ROUTER_URL, isVerbooMode } from '../constants/oauth.js' -import { isLocalProviderUrl, resolveProviderRequest } from '../services/api/providerConfig.js' +import { getCachedClaudeNativeModels } from '../services/api/claudeNativeModels.js' +import { CLAUDE_NATIVE_API_BASE_URL } from '../services/api/claudeNativeConfig.js' +import { getCachedCodexModels } from '../services/api/codexModels.js' +import { DEFAULT_CODEX_BASE_URL, isLocalProviderUrl, resolveProviderRequest } from '../services/api/providerConfig.js' import { getCachedVerbooModels } from '../services/api/verbooModels.js' import { getLocalOpenAICompatibleProviderLabel } from '../utils/providerDiscovery.js' -import { getDefaultVerbooModel, getUserSpecifiedModelSetting, isClaudeModelLike, parseUserSpecifiedModel } from '../utils/model/model.js' +import { getActiveModelIdentity } from '../utils/model/activeModelIdentity.js' +import { getDefaultMainLoopModel, getDefaultVerbooModel, getUserSpecifiedModelSetting, parseUserSpecifiedModel } from '../utils/model/model.js' import { containsExactZaiGlmModelId, isZaiBaseUrl } from '../utils/zaiProvider.js' declare const MACRO: { VERSION: string; DISPLAY_VERSION?: string } @@ -25,17 +29,24 @@ const STARTUP_DEFAULT_COLUMNS = 80 // โ”€โ”€โ”€ Provider detection โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ function resolveVerbooStartupModel(modelOverride?: string): string { - const cachedModels = getCachedVerbooModels() + const verbooModels = getCachedVerbooModels() + const codexModels = getCachedCodexModels() + const claudeModels = getCachedClaudeNativeModels() + const availableModels = [ + ...(verbooModels ?? []), + ...(codexModels ?? []), + ...(claudeModels ?? []), + ] + const catalogsLoaded = + verbooModels !== null || codexModels !== null || claudeModels !== null const resolveIfAvailable = (model: unknown): string | undefined => { if (typeof model !== 'string') return undefined const trimmed = model.trim() - if (!trimmed || isClaudeModelLike(trimmed)) return undefined + if (!trimmed) return undefined const resolved = parseUserSpecifiedModel(trimmed) - if (isClaudeModelLike(resolved)) return undefined - - if (cachedModels !== null) { - return cachedModels.some(m => m.id === resolved && !isClaudeModelLike(m.id)) + if (catalogsLoaded) { + return availableModels.some(model => model.id === resolved) ? resolved : undefined } @@ -46,20 +57,25 @@ function resolveVerbooStartupModel(modelOverride?: string): string { return ( resolveIfAvailable(modelOverride) ?? resolveIfAvailable(getUserSpecifiedModelSetting()) ?? - cachedModels?.find(model => !isClaudeModelLike(model.id))?.id ?? getDefaultVerbooModel() ) } export function detectProvider(modelOverride?: string): { name: string; model: string; baseUrl: string; isLocal: boolean } { if (isVerbooMode()) { - const baseUrl = VERBOO_ROUTER_URL - const isLocal = isLocalProviderUrl(baseUrl) + const model = resolveVerbooStartupModel(modelOverride) + const identity = getActiveModelIdentity(model) + const baseUrl = + identity.provider === 'Codex' + ? DEFAULT_CODEX_BASE_URL + : identity.provider === 'Claude' + ? CLAUDE_NATIVE_API_BASE_URL + : VERBOO_ROUTER_URL return { - name: 'Verboo', - model: resolveVerbooStartupModel(modelOverride), + name: identity.provider, + model: identity.model, baseUrl, - isLocal, + isLocal: false, } } @@ -143,12 +159,12 @@ export function detectProvider(modelOverride?: string): { name: string; model: s return { name, model: displayModel, baseUrl, isLocal } } - // VERBOO-BRAND: default provider รฉ Verboo. API LLM via router em code.verboo.ai/router. - const modelSetting = modelOverride || getDefaultVerbooModel() + const modelSetting = + modelOverride || getUserSpecifiedModelSetting() || getDefaultMainLoopModel() const resolvedModel = parseUserSpecifiedModel(modelSetting) - const baseUrl = VERBOO_ROUTER_URL + const baseUrl = process.env.ANTHROPIC_BASE_URL || CLAUDE_NATIVE_API_BASE_URL const isLocal = isLocalProviderUrl(baseUrl) - return { name: 'Verboo', model: resolvedModel, baseUrl, isLocal } + return { name: 'Claude', model: resolvedModel, baseUrl, isLocal } } // โ”€โ”€โ”€ Main โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ @@ -172,7 +188,7 @@ export function renderStartupScreen( const DIMP = `${DIM}${rgb(...DIMCOL)}` const STATUS_C = p.isLocal ? rgb(130, 200, 140) : PURPLE const statusLabel = p.isLocal ? 'local' : 'cloud' - const providerAndModel = p.name === 'Verboo' ? p.model : `${p.name} ยท ${p.model}` + const providerAndModel = `${p.name} ยท ${p.model}` const shownVersion = truncateStartupText(version, Math.max(1, columns - 22)) const model = truncateStartupText( providerAndModel, diff --git a/src/utils/model/activeModelIdentity.test.ts b/src/utils/model/activeModelIdentity.test.ts new file mode 100644 index 0000000000..9d31eb872a --- /dev/null +++ b/src/utils/model/activeModelIdentity.test.ts @@ -0,0 +1,24 @@ +import { describe, expect, test } from 'bun:test' +import { classifyActiveModelProvider } from './activeModelIdentity.js' + +describe('classifyActiveModelProvider', () => { + test('mirrors runtime routing priority across the three catalogs', () => { + const catalogs = { + verboo: [{ id: 'shared-model' }, { id: 'verboo-ultra' }], + codex: [{ id: 'shared-model' }, { id: 'gpt-5.5' }], + claude: [{ id: 'shared-model' }, { id: 'claude-opus-4-6' }], + } + + expect(classifyActiveModelProvider('shared-model', catalogs)).toBe('Verboo') + expect(classifyActiveModelProvider('gpt-5.5', catalogs)).toBe('Codex') + expect(classifyActiveModelProvider('claude-opus-4-6', catalogs)).toBe('Claude') + }) + + test('recognizes provider-shaped IDs when catalogs are unavailable', () => { + const emptyCatalogs = { verboo: null, codex: null, claude: null } + + expect(classifyActiveModelProvider('gpt-5.3-codex', emptyCatalogs)).toBe('Codex') + expect(classifyActiveModelProvider('claude-sonnet-4-6[1m]', emptyCatalogs)).toBe('Claude') + expect(classifyActiveModelProvider('ultra/minimax-m3', emptyCatalogs)).toBe('Verboo') + }) +}) diff --git a/src/utils/model/activeModelIdentity.ts b/src/utils/model/activeModelIdentity.ts new file mode 100644 index 0000000000..83ac6c4a23 --- /dev/null +++ b/src/utils/model/activeModelIdentity.ts @@ -0,0 +1,64 @@ +import { getCachedClaudeNativeModels } from '../../services/api/claudeNativeModels.js' +import { getCachedCodexModels } from '../../services/api/codexModels.js' +import { getCachedVerbooModels } from '../../services/api/verbooModels.js' + +export type ActiveModelProvider = 'Verboo' | 'Codex' | 'Claude' + +type ModelReference = { id: string } + +export type ActiveModelCatalogs = { + verboo: readonly ModelReference[] | null + codex: readonly ModelReference[] | null + claude: readonly ModelReference[] | null +} + +function comparableModelId(modelId: string): string { + return modelId.trim().replace(/\[1m\]$/i, '').trim() +} + +function catalogContains( + catalog: readonly ModelReference[] | null, + modelId: string, +): boolean { + return catalog?.some(model => model.id === modelId) === true +} + +export function classifyActiveModelProvider( + modelId: string, + catalogs: ActiveModelCatalogs, +): ActiveModelProvider { + const comparable = comparableModelId(modelId) + + // Match the routing order in services/api/client.ts. An ID exposed by the + // Verboo catalog always routes through Verboo even if another catalog also + // happens to expose the same ID. + if (catalogContains(catalogs.verboo, comparable)) return 'Verboo' + if (catalogContains(catalogs.codex, comparable)) return 'Codex' + if (catalogContains(catalogs.claude, comparable)) return 'Claude' + + // Catalogs are normally primed before the interactive UI appears. These + // fallbacks keep early/degraded startup labels useful without changing the + // routing decision itself. + if (/(?:^|[-_/])(claude|sonnet|opus|haiku)(?:$|[-_/])/i.test(comparable)) { + return 'Claude' + } + if (/^(?:gpt-|o\d)|(?:^|[-_/])codex(?:$|[-_/])/i.test(comparable)) { + return 'Codex' + } + return 'Verboo' +} + +export function getActiveModelIdentity(modelId: string): { + provider: ActiveModelProvider + model: string +} { + const model = modelId.trim() + return { + provider: classifyActiveModelProvider(model, { + verboo: getCachedVerbooModels(), + codex: getCachedCodexModels(), + claude: getCachedClaudeNativeModels(), + }), + model, + } +}