fix(cliproxy): unify codex reasoning flags and thinking flow

This commit is contained in:
Tam Nhu Tran
2026-02-14 07:44:59 +07:00
parent 60ee88cabc
commit f9df3cfa7a
11 changed files with 359 additions and 62 deletions
+23 -4
View File
@@ -563,10 +563,29 @@ async function main(): Promise<void> {
// 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
+74 -9
View File
@@ -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);
}
}
+7 -1
View File
@@ -176,7 +176,13 @@ export function buildClaudeEnvironment(config: ProxyChainConfig): Record<string,
}
// Apply thinking configuration to model (auto tier-based or manual override)
applyThinkingConfig(envVars, provider, thinkingOverride, compositeTierThinking, compositeTiers);
envVars = applyThinkingConfig(
envVars,
provider,
thinkingOverride,
compositeTierThinking,
compositeTiers
);
// Apply extended context suffix for 1M token context window
// Auto-enabled for Gemini, opt-in for Claude (--1m flag)
+30 -40
View File
@@ -63,6 +63,7 @@ import {
handleQuotaCheck,
} from './retry-handler';
import { checkOrJoinProxy, registerProxySession, setupCleanupHandlers } from './session-bridge';
import { parseThinkingOverride } from './thinking-arg-parser';
import {
warnCrossProviderDuplicates,
cleanupStaleAutoPauses,
@@ -306,50 +307,36 @@ export async function execClaudeWithCLIProxy(
setNickname = argsWithoutProxy[nicknameIdx + 1];
}
// Parse --thinking flag
let thinkingOverride: string | number | undefined;
const thinkingEqArg = argsWithoutProxy.find((arg) => 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;
@@ -0,0 +1,98 @@
/**
* Parse CLI thinking/effort override flags.
*
* Supported aliases:
* - --thinking <value>
* - --thinking=<value>
* - --effort <value>
* - --effort=<value>
*/
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} <missing-value>`);
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}=<missing-value>`);
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 };
}
+8 -3
View File
@@ -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 <provider> --thinking <value>',
'Set thinking budget (low/medium/high/xhigh/auto/off or number)',
],
['ccs codex --effort <level>', 'Set codex reasoning effort (medium/high/xhigh)'],
['ccs <provider> --1m', 'Enable 1M token context window'],
['ccs <provider> --no-1m', 'Disable 1M context (use 200K default)'],
['ccs <provider> --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 <number>', 'Custom token budget (512-100000)'],
['', ''],
['--effort <level>', '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)
+3 -1
View File
@@ -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');
@@ -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)');
});
});
@@ -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');
});
});
@@ -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=<value> 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 <missing-value>']);
});
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=<missing-value>']);
});
});
@@ -140,7 +140,8 @@ export default function ThinkingSection() {
<p className="text-sm text-muted-foreground">
{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'}
</p>
</div>
<div
@@ -208,13 +209,15 @@ export default function ThinkingSection() {
<div className="p-4 rounded-lg border bg-muted/30">
<h4 className="text-sm font-medium mb-2">CLI Override</h4>
<p className="text-sm text-muted-foreground mb-2">
Use <code className="px-1.5 py-0.5 rounded bg-muted">--thinking</code> flag to
override settings per session:
Override per session with{' '}
<code className="px-1.5 py-0.5 rounded bg-muted">--thinking</code> (agy/gemini) or{' '}
<code className="px-1.5 py-0.5 rounded bg-muted">--effort</code> (codex):
</p>
<div className="space-y-1 text-sm font-mono text-muted-foreground">
<p>ccs gemini --thinking high</p>
<p>ccs agy --thinking 16384</p>
<p>ccs gemini --thinking off</p>
<p>ccs codex --effort xhigh</p>
<p>ccs codex -p "Refactor this module" --effort xhigh</p>
</div>
</div>
</div>