diff --git a/src/ccs.ts b/src/ccs.ts index f8b018cf..9b6ca3f2 100644 --- a/src/ccs.ts +++ b/src/ccs.ts @@ -563,10 +563,29 @@ async function main(): Promise { // Special case: headless delegation (-p flag) if (args.includes('-p') || args.includes('--prompt')) { - const { DelegationHandler } = await import('./delegation/delegation-handler'); - const handler = new DelegationHandler(); - await handler.route(args); - return; + // CLIProxy profiles (codex/gemini/agy/etc, including user variants) must stay on + // the normal CLIProxy path so provider-specific flags (e.g. --effort/--thinking) + // and proxy chains are applied consistently. + let shouldUseDelegation = true; + const { profile } = detectProfile(args); + try { + const ProfileDetectorModule = await import('./auth/profile-detector'); + const ProfileDetector = ProfileDetectorModule.default; + const detector = new ProfileDetector(); + const profileInfo = detector.detectProfileType(profile); + if (profileInfo.type === 'cliproxy') { + shouldUseDelegation = false; + } + } catch { + // Best effort detection only; keep delegation fallback behavior. + } + + if (shouldUseDelegation) { + const { DelegationHandler } = await import('./delegation/delegation-handler'); + const handler = new DelegationHandler(); + await handler.route(args); + return; + } } // First-time install: offer setup wizard for interactive users diff --git a/src/cliproxy/config/thinking-config.ts b/src/cliproxy/config/thinking-config.ts index 69ec1762..cfb9430d 100644 --- a/src/cliproxy/config/thinking-config.ts +++ b/src/cliproxy/config/thinking-config.ts @@ -13,6 +13,30 @@ import { warn } from '../../utils/ui'; /** Model tier types for thinking budget defaults */ export type ModelTier = 'opus' | 'sonnet' | 'haiku'; +/** + * Normalize model ID for provider capability lookup. + * Codex may carry effort suffixes in either legacy "(high)" or current "-high" forms. + */ +function normalizeModelForThinkingLookup(model: string, provider: CLIProxyProvider): string { + const withoutExtendedContext = model.replace(/\[1m\]$/i, '').trim(); + + if (provider !== 'codex') return withoutExtendedContext; + + // New codex suffix form: gpt-5.3-codex-high -> gpt-5.3-codex + const codexSuffixMatch = withoutExtendedContext.match(/^(.*)-(xhigh|high|medium)$/i); + if (codexSuffixMatch?.[1]) { + return codexSuffixMatch[1].trim(); + } + + // Legacy codex suffix form: gpt-5.3-codex(high) -> gpt-5.3-codex + const codexLegacyMatch = withoutExtendedContext.match(/^(.*)\((xhigh|high|medium)\)$/i); + if (codexLegacyMatch?.[1]) { + return codexLegacyMatch[1].trim(); + } + + return withoutExtendedContext; +} + /** * Check if warnings should be shown based on thinking config. * Defaults to true if show_warnings is not explicitly false. @@ -41,11 +65,46 @@ export function detectTierFromModel(modelName: string): ModelTier { * @returns Model name with thinking suffix, e.g., "gemini-3-pro-preview(high)" */ export function applyThinkingSuffix(model: string, thinkingValue: string | number): string { - // Don't apply if model already ends with a parenthesized suffix (e.g., "model(high)" or "model(8192)") - // Matches: ends with "(...)" where content is non-empty - if (/\([^)]+\)$/.test(model)) { + return applyThinkingSuffixForProvider(model, thinkingValue); +} + +/** + * Apply provider-aware thinking suffix to model name. + * - Most providers: model(level|budget) e.g. "gemini-2.5-pro(high)" + * - Codex: model-effort e.g. "gpt-5.3-codex-xhigh" + */ +function applyThinkingSuffixForProvider( + model: string, + thinkingValue: string | number, + provider?: CLIProxyProvider +): string { + const codexEffortRegex = /^(medium|high|xhigh)$/i; + const codexModelSuffixRegex = /-(medium|high|xhigh)$/i; + const parenthesizedSuffixMatch = model.match(/\(([^)]+)\)$/); + + // Existing parenthesized suffix: + // - keep as-is for non-codex providers + // - for codex effort levels, normalize to codex model suffix style + if (parenthesizedSuffixMatch) { + if (provider === 'codex') { + const normalizedParensValue = parenthesizedSuffixMatch[1]?.trim().toLowerCase() || ''; + if (codexEffortRegex.test(normalizedParensValue)) { + return model.replace(/\([^)]+\)$/, `-${normalizedParensValue}`); + } + } return model; } + + if (provider === 'codex') { + if (codexModelSuffixRegex.test(model)) { + return model; + } + const normalized = String(thinkingValue).trim().toLowerCase(); + if (codexEffortRegex.test(normalized)) { + return `${model}-${normalized}`; + } + } + return `${model}(${thinkingValue})`; } @@ -120,7 +179,8 @@ export function applyThinkingConfig( // Get base model to check thinking support const baseModel = result.ANTHROPIC_MODEL || ''; - if (!supportsThinking(provider, baseModel)) { + const normalizedBaseModel = normalizeModelForThinkingLookup(baseModel, provider); + if (!supportsThinking(provider, normalizedBaseModel)) { // U2: Warn user if they explicitly provided --thinking but model doesn't support it if (thinkingOverride !== undefined && shouldShowWarnings(thinkingConfig)) { console.warn( @@ -143,7 +203,7 @@ export function applyThinkingConfig( thinkingValue = thinkingConfig.override; } else if (thinkingConfig.mode === 'auto') { // Auto mode: detect tier and apply default - const tier = detectTierFromModel(baseModel); + const tier = detectTierFromModel(normalizedBaseModel); // Check per-tier config first if composite const perTierValue = compositeTierThinking?.[tier]; if (perTierValue !== undefined) { @@ -156,7 +216,7 @@ export function applyThinkingConfig( } // Validate thinking value against model capabilities - const validation = validateThinking(provider, baseModel, thinkingValue); + const validation = validateThinking(provider, normalizedBaseModel, thinkingValue); if (validation.warning && shouldShowWarnings(thinkingConfig)) { console.warn(warn(validation.warning)); } @@ -174,7 +234,11 @@ export function applyThinkingConfig( // Otherwise, continue to process tiers with their own config (skip main model) } else if (result.ANTHROPIC_MODEL) { // Apply thinking suffix to main model (only if not off) - result.ANTHROPIC_MODEL = applyThinkingSuffix(result.ANTHROPIC_MODEL, thinkingValue); + result.ANTHROPIC_MODEL = applyThinkingSuffixForProvider( + result.ANTHROPIC_MODEL, + thinkingValue, + provider + ); } // Apply to tier models if they support thinking @@ -197,9 +261,10 @@ export function applyThinkingConfig( // P2 FIX: Use tier-specific provider from compositeTiers for mixed-provider composites // Falls back to the default provider if not a composite or tier not specified const tierProvider = compositeTiers?.[tier]?.provider ?? provider; + const normalizedTierModel = normalizeModelForThinkingLookup(model, tierProvider); // Check if this tier's model supports thinking (using tier-specific provider) - if (!supportsThinking(tierProvider, model)) { + if (!supportsThinking(tierProvider, normalizedTierModel)) { continue; } @@ -224,7 +289,7 @@ export function applyThinkingConfig( continue; } - result[tierVar] = applyThinkingSuffix(model, tierThinkingValue); + result[tierVar] = applyThinkingSuffixForProvider(model, tierThinkingValue, tierProvider); } } diff --git a/src/cliproxy/executor/env-resolver.ts b/src/cliproxy/executor/env-resolver.ts index 2f15749e..4390ee9c 100644 --- a/src/cliproxy/executor/env-resolver.ts +++ b/src/cliproxy/executor/env-resolver.ts @@ -176,7 +176,13 @@ export function buildClaudeEnvironment(config: ProxyChainConfig): Record arg.startsWith('--thinking=')); - if (thinkingEqArg) { - const val = thinkingEqArg.substring('--thinking='.length); - if (!val || val.trim() === '') { - console.error(fail('--thinking requires a value')); - console.error(' Examples: --thinking=low, --thinking=8192, --thinking=off'); + // Parse --thinking / --effort flags (aliases; first occurrence wins) + const thinkingParse = parseThinkingOverride(argsWithoutProxy); + if (thinkingParse.error) { + const { flag } = thinkingParse.error; + console.error(fail(`${flag} requires a value`)); + + if (provider === 'codex') { + console.error(' Codex examples: --effort xhigh, --effort high, --effort medium'); + console.error(' Alias: --thinking xhigh (same behavior)'); + } else { + console.error(' Examples: --thinking low, --thinking 8192, --thinking off'); console.error(' Levels: minimal, low, medium, high, xhigh, auto'); - process.exit(1); } - const numVal = parseInt(val, 10); - thinkingOverride = !isNaN(numVal) ? numVal : val; - const allThinkingFlags = argsWithoutProxy.filter( - (arg) => arg === '--thinking' || arg.startsWith('--thinking=') + process.exit(1); + } + + const thinkingOverride = thinkingParse.value; + if (thinkingParse.duplicateDisplays.length > 0) { + console.warn( + `[!] Multiple reasoning flags detected. Using first occurrence: ${thinkingParse.sourceDisplay}` ); - if (allThinkingFlags.length > 1) { - console.warn( - `[!] Multiple --thinking flags detected. Using first occurrence: ${thinkingEqArg}` - ); - } - } else { - const thinkingIdx = argsWithoutProxy.indexOf('--thinking'); - if (thinkingIdx !== -1) { - const nextArg = argsWithoutProxy[thinkingIdx + 1]; - if (!nextArg || nextArg.startsWith('-')) { - console.error(fail('--thinking requires a value')); - console.error(' Examples: --thinking low, --thinking 8192, --thinking off'); - console.error(' Levels: minimal, low, medium, high, xhigh, auto'); - process.exit(1); - } - const numVal = parseInt(nextArg, 10); - thinkingOverride = !isNaN(numVal) ? numVal : nextArg; + } - const allThinkingFlags = argsWithoutProxy.filter( - (arg) => arg === '--thinking' || arg.startsWith('--thinking=') - ); - if (allThinkingFlags.length > 1) { - console.warn( - `[!] Multiple --thinking flags detected. Using first occurrence: --thinking ${nextArg}` - ); - } - } + if (thinkingParse.sourceFlag === '--effort' && provider !== 'codex') { + console.warn( + warn( + '`--effort` is primarily for codex. Continuing as alias of `--thinking` for compatibility.' + ) + ); } // Parse --1m / --no-1m flags for extended context (1M token window) @@ -868,6 +855,7 @@ export async function execClaudeWithCLIProxy( '--use', '--nickname', '--thinking', + '--effort', '--1m', '--no-1m', '--incognito', @@ -879,11 +867,13 @@ export async function execClaudeWithCLIProxy( const claudeArgs = argsWithoutProxy.filter((arg, idx) => { if (ccsFlags.includes(arg)) return false; if (arg.startsWith('--thinking=')) return false; + if (arg.startsWith('--effort=')) return false; if (arg.startsWith('--1m=') || arg.startsWith('--no-1m=')) return false; if ( argsWithoutProxy[idx - 1] === '--use' || argsWithoutProxy[idx - 1] === '--nickname' || - argsWithoutProxy[idx - 1] === '--thinking' + argsWithoutProxy[idx - 1] === '--thinking' || + argsWithoutProxy[idx - 1] === '--effort' ) return false; return true; diff --git a/src/cliproxy/executor/thinking-arg-parser.ts b/src/cliproxy/executor/thinking-arg-parser.ts new file mode 100644 index 00000000..7f414d00 --- /dev/null +++ b/src/cliproxy/executor/thinking-arg-parser.ts @@ -0,0 +1,98 @@ +/** + * Parse CLI thinking/effort override flags. + * + * Supported aliases: + * - --thinking + * - --thinking= + * - --effort + * - --effort= + */ + +export type ThinkingFlag = '--thinking' | '--effort'; + +export interface ThinkingParseError { + flag: ThinkingFlag; + form: 'inline' | 'separate'; +} + +export interface ThinkingParseResult { + value?: string | number; + sourceFlag?: ThinkingFlag; + sourceDisplay?: string; + duplicateDisplays: string[]; + error?: ThinkingParseError; +} + +function parseThinkingValue(raw: string): string | number { + const trimmed = raw.trim(); + + // Keep strict integer parsing for legacy compatibility with --thinking numeric budgets. + if (/^-?\d+$/.test(trimmed)) { + return Number.parseInt(trimmed, 10); + } + + return trimmed; +} + +/** + * Parse first thinking/effort value from args. + * If multiple flags are provided, first occurrence wins and later ones are reported as duplicates. + */ +export function parseThinkingOverride(args: string[]): ThinkingParseResult { + let value: string | number | undefined; + let sourceFlag: ThinkingFlag | undefined; + let sourceDisplay: string | undefined; + const duplicateDisplays: string[] = []; + + for (let i = 0; i < args.length; i++) { + const arg = args[i]; + + let flag: ThinkingFlag | null = null; + let rawValue: string | undefined; + let display = ''; + let form: 'inline' | 'separate' = 'separate'; + + if (arg === '--thinking' || arg === '--effort') { + flag = arg; + const nextArg = args[i + 1]; + if (!nextArg || nextArg.startsWith('-')) { + if (value === undefined) { + return { value, sourceFlag, sourceDisplay, duplicateDisplays, error: { flag, form } }; + } + duplicateDisplays.push(`${flag} `); + continue; + } + rawValue = nextArg; + display = `${flag} ${rawValue}`; + i += 1; // Consume value + } else if (arg.startsWith('--thinking=') || arg.startsWith('--effort=')) { + const [rawFlag, ...rest] = arg.split('='); + const joined = rest.join('='); + flag = rawFlag as ThinkingFlag; + rawValue = joined; + form = 'inline'; + if (!rawValue || rawValue.trim() === '') { + if (value === undefined) { + return { value, sourceFlag, sourceDisplay, duplicateDisplays, error: { flag, form } }; + } + duplicateDisplays.push(`${flag}=`); + continue; + } + display = `${flag}=${rawValue}`; + } + + if (!flag || rawValue === undefined) { + continue; + } + + if (value === undefined) { + value = parseThinkingValue(rawValue); + sourceFlag = flag; + sourceDisplay = display; + } else { + duplicateDisplays.push(display); + } + } + + return { value, sourceFlag, sourceDisplay, duplicateDisplays }; +} diff --git a/src/commands/help-command.ts b/src/commands/help-command.ts index 16cd06f6..cd933ebf 100644 --- a/src/commands/help-command.ts +++ b/src/commands/help-command.ts @@ -168,7 +168,7 @@ Run ${color('ccs config', 'command')} for web dashboard`.trim(); ], [ ['ccs gemini', 'Google Gemini (gemini-2.5-pro or 3-pro)'], - ['ccs codex', 'OpenAI Codex (gpt-5.1-codex-max)'], + ['ccs codex', 'OpenAI Codex (gpt-5.3-codex)'], ['ccs agy', 'Antigravity (Claude/Gemini models)'], ['ccs qwen', 'Qwen Code (qwen3-coder)'], ['ccs kiro', 'Kiro (AWS CodeWhisperer Claude models)'], @@ -187,6 +187,7 @@ Run ${color('ccs config', 'command')} for web dashboard`.trim(); 'ccs --thinking ', 'Set thinking budget (low/medium/high/xhigh/auto/off or number)', ], + ['ccs codex --effort ', 'Set codex reasoning effort (medium/high/xhigh)'], ['ccs --1m', 'Enable 1M token context window'], ['ccs --no-1m', 'Disable 1M context (use 200K default)'], ['ccs --logout', 'Clear authentication'], @@ -332,7 +333,7 @@ Run ${color('ccs config', 'command')} for web dashboard`.trim(); ]); // W3: Thinking Budget explanation - printSubSection('Extended Thinking (--thinking)', [ + printSubSection('Extended Thinking / Reasoning', [ ['--thinking off', 'Disable extended thinking'], ['--thinking auto', 'Let model decide dynamically'], ['--thinking low', '1K tokens - Quick responses'], @@ -341,8 +342,12 @@ Run ${color('ccs config', 'command')} for web dashboard`.trim(); ['--thinking xhigh', '32K tokens - Maximum depth'], ['--thinking ', 'Custom token budget (512-100000)'], ['', ''], + ['--effort ', 'Codex alias for reasoning effort (medium/high/xhigh)'], + ['--effort xhigh', 'Codex 5.3 full-depth reasoning'], + ['', ''], ['Note:', 'Extended thinking allocates compute for step-by-step reasoning'], - ['', 'before responding. Supported: agy, gemini (thinking models).'], + ['', 'before responding.'], + ['', 'Providers: agy/gemini use --thinking, codex uses --effort (or --thinking alias).'], ]); // Extended Context (1M) diff --git a/src/config/unified-config-loader.ts b/src/config/unified-config-loader.ts index 773259c4..5f639d47 100644 --- a/src/config/unified-config-loader.ts +++ b/src/config/unified-config-loader.ts @@ -638,7 +638,9 @@ function generateYamlWithComments(config: UnifiedConfig): string { lines.push('# Thinking: Extended thinking/reasoning budget configuration'); lines.push('# Controls reasoning depth for supported providers (agy, gemini, codex).'); lines.push('#'); - lines.push('# Modes: auto (use tier_defaults), off (disable), manual (--thinking flag only)'); + lines.push( + '# Modes: auto (use tier_defaults), off (disable), manual (--thinking/--effort flags)' + ); lines.push('# Levels: minimal (512), low (1K), medium (8K), high (24K), xhigh (32K), auto'); lines.push('# Override: Set global override value (number or level name)'); lines.push('# Provider overrides: Per-provider tier defaults'); diff --git a/tests/unit/cliproxy/composite-env-routing.test.ts b/tests/unit/cliproxy/composite-env-routing.test.ts index 64076d48..9d29abba 100644 --- a/tests/unit/cliproxy/composite-env-routing.test.ts +++ b/tests/unit/cliproxy/composite-env-routing.test.ts @@ -57,4 +57,16 @@ describe('buildClaudeEnvironment - composite remote routing', () => { expect(env.ANTHROPIC_AUTH_TOKEN).toBe('remote-auth-token'); expect(env.ANTHROPIC_BASE_URL).not.toContain('/api/provider/'); }); + + it('applies thinking override to resolved env models', () => { + const env = buildClaudeEnvironment({ + provider: 'gemini', + useRemoteProxy: false, + localPort: 8318, + verbose: false, + thinkingOverride: 'high', + }); + + expect(env.ANTHROPIC_MODEL).toContain('(high)'); + }); }); diff --git a/tests/unit/cliproxy/composite-thinking.test.ts b/tests/unit/cliproxy/composite-thinking.test.ts index 0112d6b9..2464c31a 100644 --- a/tests/unit/cliproxy/composite-thinking.test.ts +++ b/tests/unit/cliproxy/composite-thinking.test.ts @@ -476,4 +476,36 @@ describe('applyThinkingConfig - composite variant integration', () => { // Opus stays unchanged because function returned early expect(result.ANTHROPIC_DEFAULT_OPUS_MODEL).toBe('claude-opus-4-6-thinking'); }); + + it('should use codex effort suffix style when provider is codex', () => { + const envVars: NodeJS.ProcessEnv = { + ANTHROPIC_MODEL: 'gpt-5.3-codex', + ANTHROPIC_DEFAULT_OPUS_MODEL: 'gpt-5.3-codex', + ANTHROPIC_DEFAULT_SONNET_MODEL: 'gpt-5.3-codex', + ANTHROPIC_DEFAULT_HAIKU_MODEL: 'gpt-5-mini', + }; + + const result = applyThinkingConfig( + envVars, + 'codex' as CLIProxyProvider, + 'xhigh', // Explicit CLI override + undefined + ); + + expect(result.ANTHROPIC_MODEL).toBe('gpt-5.3-codex-xhigh'); + expect(result.ANTHROPIC_DEFAULT_OPUS_MODEL).toBe('gpt-5.3-codex-xhigh'); + expect(result.ANTHROPIC_DEFAULT_SONNET_MODEL).toBe('gpt-5.3-codex-xhigh'); + expect(result.ANTHROPIC_DEFAULT_HAIKU_MODEL).toBe('gpt-5-mini-xhigh'); + }); + + it('should normalize legacy codex parenthesized tier suffix to effort suffix format', () => { + const envVars: NodeJS.ProcessEnv = { + ANTHROPIC_MODEL: 'gpt-5.3-codex', + ANTHROPIC_DEFAULT_OPUS_MODEL: 'gpt-5.3-codex(high)', + }; + + const result = applyThinkingConfig(envVars, 'codex' as CLIProxyProvider, 'high'); + + expect(result.ANTHROPIC_DEFAULT_OPUS_MODEL).toBe('gpt-5.3-codex-high'); + }); }); diff --git a/tests/unit/cliproxy/thinking-arg-parser.test.ts b/tests/unit/cliproxy/thinking-arg-parser.test.ts new file mode 100644 index 00000000..13310906 --- /dev/null +++ b/tests/unit/cliproxy/thinking-arg-parser.test.ts @@ -0,0 +1,65 @@ +import { describe, it, expect } from 'bun:test'; +import { parseThinkingOverride } from '../../../src/cliproxy/executor/thinking-arg-parser'; + +describe('parseThinkingOverride', () => { + it('parses --thinking with separate value', () => { + const out = parseThinkingOverride(['--thinking', 'high']); + expect(out.value).toBe('high'); + expect(out.sourceFlag).toBe('--thinking'); + expect(out.error).toBeUndefined(); + }); + + it('parses --thinking= inline', () => { + const out = parseThinkingOverride(['--thinking=xhigh']); + expect(out.value).toBe('xhigh'); + expect(out.sourceFlag).toBe('--thinking'); + expect(out.error).toBeUndefined(); + }); + + it('parses --effort alias', () => { + const out = parseThinkingOverride(['--effort', 'medium']); + expect(out.value).toBe('medium'); + expect(out.sourceFlag).toBe('--effort'); + expect(out.error).toBeUndefined(); + }); + + it('parses integer values as numbers', () => { + const out = parseThinkingOverride(['--thinking', '8192']); + expect(out.value).toBe(8192); + expect(out.sourceFlag).toBe('--thinking'); + }); + + it('returns first occurrence and tracks duplicates across aliases', () => { + const out = parseThinkingOverride(['--effort=high', '--thinking', 'xhigh']); + expect(out.value).toBe('high'); + expect(out.sourceFlag).toBe('--effort'); + expect(out.duplicateDisplays).toEqual(['--thinking xhigh']); + expect(out.error).toBeUndefined(); + }); + + it('returns error for missing separate value', () => { + const out = parseThinkingOverride(['--effort', '--verbose']); + expect(out.error).toEqual({ flag: '--effort', form: 'separate' }); + }); + + it('returns error for missing inline value', () => { + const out = parseThinkingOverride(['--thinking=']); + expect(out.error).toEqual({ flag: '--thinking', form: 'inline' }); + }); + + it('keeps first valid value when later duplicate is missing separate value', () => { + const out = parseThinkingOverride(['--effort', 'high', '--thinking']); + expect(out.value).toBe('high'); + expect(out.sourceFlag).toBe('--effort'); + expect(out.error).toBeUndefined(); + expect(out.duplicateDisplays).toEqual(['--thinking ']); + }); + + it('keeps first valid value when later duplicate is missing inline value', () => { + const out = parseThinkingOverride(['--thinking=medium', '--effort=']); + expect(out.value).toBe('medium'); + expect(out.sourceFlag).toBe('--thinking'); + expect(out.error).toBeUndefined(); + expect(out.duplicateDisplays).toEqual(['--effort=']); + }); +}); diff --git a/ui/src/pages/settings/sections/thinking/index.tsx b/ui/src/pages/settings/sections/thinking/index.tsx index 6268e7fd..3fd6bb19 100644 --- a/ui/src/pages/settings/sections/thinking/index.tsx +++ b/ui/src/pages/settings/sections/thinking/index.tsx @@ -140,7 +140,8 @@ export default function ThinkingSection() {

{mode === 'auto' && 'Automatically set thinking based on model tier'} {mode === 'off' && 'Disable extended thinking'} - {mode === 'manual' && 'Use --thinking flag to control manually'} + {mode === 'manual' && + 'Use --thinking (agy/gemini) or --effort (codex) per run'}

CLI Override

- Use --thinking flag to - override settings per session: + Override per session with{' '} + --thinking (agy/gemini) or{' '} + --effort (codex):

ccs gemini --thinking high

ccs agy --thinking 16384

-

ccs gemini --thinking off

+

ccs codex --effort xhigh

+

ccs codex -p "Refactor this module" --effort xhigh