diff --git a/src/cliproxy/model-catalog.ts b/src/cliproxy/model-catalog.ts index 557cf828..0fccc017 100644 --- a/src/cliproxy/model-catalog.ts +++ b/src/cliproxy/model-catalog.ts @@ -15,8 +15,10 @@ export interface ModelEntry { id: string; /** Human-readable name for display */ name: string; - /** Access tier indicator */ + /** Access tier indicator - 'paid' means requires paid API tier (not model pricing) */ tier?: 'free' | 'paid'; + /** Optional description for the model */ + description?: string; } /** @@ -40,10 +42,27 @@ export const MODEL_CATALOG: Partial> = displayName: 'Antigravity', defaultModel: 'gemini-3-pro-preview', models: [ - { id: 'gemini-claude-opus-4-5-thinking', name: 'Claude Opus 4.5 Thinking' }, - { id: 'gemini-claude-sonnet-4-5-thinking', name: 'Claude Sonnet 4.5 Thinking' }, - { id: 'gemini-claude-sonnet-4-5', name: 'Claude Sonnet 4.5' }, - { id: 'gemini-3-pro-preview', name: 'Gemini 3 Pro', tier: 'paid' }, + { + id: 'gemini-claude-opus-4-5-thinking', + name: 'Claude Opus 4.5 Thinking', + description: 'Most capable, extended thinking', + }, + { + id: 'gemini-claude-sonnet-4-5-thinking', + name: 'Claude Sonnet 4.5 Thinking', + description: 'Balanced with extended thinking', + }, + { + id: 'gemini-claude-sonnet-4-5', + name: 'Claude Sonnet 4.5', + description: 'Fast and capable', + }, + { + id: 'gemini-3-pro-preview', + name: 'Gemini 3 Pro', + tier: 'paid', + description: 'Google latest, requires Paid API tier', + }, ], }, gemini: { @@ -51,8 +70,17 @@ export const MODEL_CATALOG: Partial> = displayName: 'Gemini', defaultModel: 'gemini-2.5-pro', models: [ - { id: 'gemini-3-pro-preview', name: 'Gemini 3 Pro', tier: 'paid' }, - { id: 'gemini-2.5-pro', name: 'Gemini 2.5 Pro' }, + { + id: 'gemini-3-pro-preview', + name: 'Gemini 3 Pro', + tier: 'paid', + description: 'Latest model, requires Paid API tier', + }, + { + id: 'gemini-2.5-pro', + name: 'Gemini 2.5 Pro', + description: 'Stable release, works with free tier', + }, ], }, }; diff --git a/src/cliproxy/model-config.ts b/src/cliproxy/model-config.ts index 1ff67b97..d58c6c11 100644 --- a/src/cliproxy/model-config.ts +++ b/src/cliproxy/model-config.ts @@ -11,6 +11,7 @@ import { InteractivePrompt } from '../utils/prompt'; import { getProviderCatalog, supportsModelConfig, ModelEntry } from './model-catalog'; import { getProviderSettingsPath, getClaudeEnvVars } from './config-generator'; import { CLIProxyProvider } from './types'; +import { initUI, color, bold, dim, ok, info, header } from '../utils/ui'; /** CCS directory */ const CCS_DIR = path.join(process.env.HOME || process.env.USERPROFILE || '', '.ccs'); @@ -39,11 +40,23 @@ export function getCurrentModel(provider: CLIProxyProvider): string | undefined } /** - * Format model entry for display + * Format model entry for display in selection list */ function formatModelOption(model: ModelEntry): string { - const tierLabel = model.tier === 'paid' ? ' [paid]' : ''; - return `${model.name} (${model.id})${tierLabel}`; + // Tier badge: clarify that "paid" means API tier, not model pricing + const tierBadge = model.tier === 'paid' ? color(' [Paid API]', 'warning') : ''; + return `${model.name}${tierBadge}`; +} + +/** + * Format model entry for detailed display (with description) + */ +function formatModelDetailed(model: ModelEntry, isCurrent: boolean): string { + const marker = isCurrent ? color('>', 'success') : ' '; + const name = isCurrent ? bold(model.name) : model.name; + const tierBadge = model.tier === 'paid' ? color(' [Paid API]', 'warning') : ''; + const desc = model.description ? dim(` - ${model.description}`) : ''; + return ` ${marker} ${name}${tierBadge}${desc}`; } /** @@ -72,6 +85,9 @@ export async function configureProviderModel( return false; } + // Initialize UI for colors/gradient + await initUI(); + // Build options list const options = catalog.models.map((m) => ({ id: m.id, @@ -82,9 +98,13 @@ export async function configureProviderModel( const defaultIdx = catalog.models.findIndex((m) => m.id === catalog.defaultModel); const safeDefaultIdx = defaultIdx >= 0 ? defaultIdx : 0; - // Show header + // Show header with context (gradient like ccs doctor) + console.error(''); + console.error(header(`Configure ${catalog.displayName} Model`)); + console.error(''); + console.error(dim(' Select which model to use for this provider.')); + console.error(dim(' Models marked [Paid API] require a paid Google AI Studio API key.')); console.error(''); - console.error(`[i] Configure ${catalog.displayName} model`); // Interactive selection const selectedModel = await InteractivePrompt.selectFromList('Select model:', options, { @@ -118,8 +138,8 @@ export async function configureProviderModel( const displayName = selectedEntry?.name || selectedModel; console.error(''); - console.error(`[OK] Model set to: ${displayName} (${selectedModel})`); - console.error(`[i] Saved to: ${settingsPath}`); + console.error(ok(`Model set to: ${bold(displayName)}`)); + console.error(dim(` Config saved: ${settingsPath}`)); console.error(''); return true; @@ -128,41 +148,47 @@ export async function configureProviderModel( /** * Show current model configuration */ -export function showCurrentConfig(provider: CLIProxyProvider): void { +export async function showCurrentConfig(provider: CLIProxyProvider): Promise { if (!supportsModelConfig(provider)) { - console.error(`[i] Provider ${provider} does not support model configuration`); + console.error(info(`Provider ${provider} does not support model configuration`)); return; } const catalog = getProviderCatalog(provider); if (!catalog) return; + // Initialize UI for colors/gradient + await initUI(); + const currentModel = getCurrentModel(provider); const settingsPath = getProviderSettingsPath(provider); console.error(''); - console.error(`[i] ${catalog.displayName} Model Configuration`); + console.error(header(`${catalog.displayName} Model Configuration`)); console.error(''); if (currentModel) { const entry = catalog.models.find((m) => m.id === currentModel); const displayName = entry?.name || 'Unknown'; - console.error(` Current: ${displayName} (${currentModel})`); - console.error(` Config: ${settingsPath}`); + console.error( + ` ${bold('Current:')} ${color(displayName, 'success')} ${dim(`(${currentModel})`)}` + ); + console.error(` ${bold('Config:')} ${dim(settingsPath)}`); } else { - console.error(` Current: (using defaults)`); - console.error(` Default: ${catalog.defaultModel}`); + console.error(` ${bold('Current:')} ${dim('(using defaults)')}`); + console.error(` ${bold('Default:')} ${catalog.defaultModel}`); } console.error(''); - console.error('Available models:'); + console.error(bold('Available models:')); + console.error(dim(' [Paid API] = Requires paid Google AI Studio API key')); + console.error(''); catalog.models.forEach((m) => { const isCurrent = m.id === currentModel; - const marker = isCurrent ? '>' : ' '; - console.error(` ${marker} ${formatModelOption(m)}`); + console.error(formatModelDetailed(m, isCurrent)); }); console.error(''); - console.error(`Run "ccs ${provider} --config" to change`); + console.error(dim(`Run "ccs ${provider} --config" to change`)); console.error(''); } diff --git a/tests/unit/cliproxy/model-config.test.js b/tests/unit/cliproxy/model-config.test.js index 24118bd7..807dad9b 100644 --- a/tests/unit/cliproxy/model-config.test.js +++ b/tests/unit/cliproxy/model-config.test.js @@ -55,20 +55,20 @@ describe('Model Config', () => { }); describe('showCurrentConfig', () => { - it('does not throw for agy provider', () => { + it('does not throw for agy provider', async () => { const { showCurrentConfig } = modelConfig; - // Just verify it doesn't throw - assert.doesNotThrow(() => showCurrentConfig('agy')); + // Just verify it doesn't throw (now async) + await assert.doesNotReject(async () => showCurrentConfig('agy')); }); - it('does not throw for gemini provider', () => { + it('does not throw for gemini provider', async () => { const { showCurrentConfig } = modelConfig; - assert.doesNotThrow(() => showCurrentConfig('gemini')); + await assert.doesNotReject(async () => showCurrentConfig('gemini')); }); - it('does not throw for unsupported provider', () => { + it('does not throw for unsupported provider', async () => { const { showCurrentConfig } = modelConfig; - assert.doesNotThrow(() => showCurrentConfig('codex')); + await assert.doesNotReject(async () => showCurrentConfig('codex')); }); });