Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion src/components/PromptInput/PromptInputFooter.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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);

Expand All @@ -56,7 +58,10 @@ function ContextWindowDisplayInner({ messages, permissionMode }: {
const windowK = formatNumber(windowSize);

return (
<Box flexDirection="row" gap={1}>
<Box flexDirection="row" gap={1} flexShrink={1}>
<Text color="claude">{activeModel.provider}</Text>
<Text dimColor wrap="truncate">{activeModel.model}</Text>
<Text dimColor>·</Text>
<Text dimColor>context</Text>
<Text color={contextColor} dimColor={contextColor === undefined}>{pct}%</Text>
<Text dimColor>·</Text>
Expand Down
37 changes: 37 additions & 0 deletions src/components/StartupScreen.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand All @@ -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}`)
}
Expand Down Expand Up @@ -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(
[
Expand Down Expand Up @@ -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')
Expand Down
54 changes: 35 additions & 19 deletions src/components/StartupScreen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
Expand All @@ -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
}
Expand All @@ -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,
}
}

Expand Down Expand Up @@ -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 ─────────────────────────────────────────────────────────────────────
Expand All @@ -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,
Expand Down
24 changes: 24 additions & 0 deletions src/utils/model/activeModelIdentity.test.ts
Original file line number Diff line number Diff line change
@@ -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')
})
})
64 changes: 64 additions & 0 deletions src/utils/model/activeModelIdentity.ts
Original file line number Diff line number Diff line change
@@ -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,
}
}