mirror of
https://github.com/tiennm99/ccs.git
synced 2026-08-08 22:24:09 +00:00
fix(cliproxy): unify codex reasoning flags and thinking flow
This commit is contained in:
+23
-4
@@ -563,10 +563,29 @@ async function main(): Promise<void> {
|
|||||||
|
|
||||||
// Special case: headless delegation (-p flag)
|
// Special case: headless delegation (-p flag)
|
||||||
if (args.includes('-p') || args.includes('--prompt')) {
|
if (args.includes('-p') || args.includes('--prompt')) {
|
||||||
const { DelegationHandler } = await import('./delegation/delegation-handler');
|
// CLIProxy profiles (codex/gemini/agy/etc, including user variants) must stay on
|
||||||
const handler = new DelegationHandler();
|
// the normal CLIProxy path so provider-specific flags (e.g. --effort/--thinking)
|
||||||
await handler.route(args);
|
// and proxy chains are applied consistently.
|
||||||
return;
|
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
|
// First-time install: offer setup wizard for interactive users
|
||||||
|
|||||||
@@ -13,6 +13,30 @@ import { warn } from '../../utils/ui';
|
|||||||
/** Model tier types for thinking budget defaults */
|
/** Model tier types for thinking budget defaults */
|
||||||
export type ModelTier = 'opus' | 'sonnet' | 'haiku';
|
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.
|
* Check if warnings should be shown based on thinking config.
|
||||||
* Defaults to true if show_warnings is not explicitly false.
|
* 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)"
|
* @returns Model name with thinking suffix, e.g., "gemini-3-pro-preview(high)"
|
||||||
*/
|
*/
|
||||||
export function applyThinkingSuffix(model: string, thinkingValue: string | number): string {
|
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)")
|
return applyThinkingSuffixForProvider(model, thinkingValue);
|
||||||
// Matches: ends with "(...)" where content is non-empty
|
}
|
||||||
if (/\([^)]+\)$/.test(model)) {
|
|
||||||
|
/**
|
||||||
|
* 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;
|
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})`;
|
return `${model}(${thinkingValue})`;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -120,7 +179,8 @@ export function applyThinkingConfig(
|
|||||||
|
|
||||||
// Get base model to check thinking support
|
// Get base model to check thinking support
|
||||||
const baseModel = result.ANTHROPIC_MODEL || '';
|
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
|
// U2: Warn user if they explicitly provided --thinking but model doesn't support it
|
||||||
if (thinkingOverride !== undefined && shouldShowWarnings(thinkingConfig)) {
|
if (thinkingOverride !== undefined && shouldShowWarnings(thinkingConfig)) {
|
||||||
console.warn(
|
console.warn(
|
||||||
@@ -143,7 +203,7 @@ export function applyThinkingConfig(
|
|||||||
thinkingValue = thinkingConfig.override;
|
thinkingValue = thinkingConfig.override;
|
||||||
} else if (thinkingConfig.mode === 'auto') {
|
} else if (thinkingConfig.mode === 'auto') {
|
||||||
// Auto mode: detect tier and apply default
|
// Auto mode: detect tier and apply default
|
||||||
const tier = detectTierFromModel(baseModel);
|
const tier = detectTierFromModel(normalizedBaseModel);
|
||||||
// Check per-tier config first if composite
|
// Check per-tier config first if composite
|
||||||
const perTierValue = compositeTierThinking?.[tier];
|
const perTierValue = compositeTierThinking?.[tier];
|
||||||
if (perTierValue !== undefined) {
|
if (perTierValue !== undefined) {
|
||||||
@@ -156,7 +216,7 @@ export function applyThinkingConfig(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Validate thinking value against model capabilities
|
// Validate thinking value against model capabilities
|
||||||
const validation = validateThinking(provider, baseModel, thinkingValue);
|
const validation = validateThinking(provider, normalizedBaseModel, thinkingValue);
|
||||||
if (validation.warning && shouldShowWarnings(thinkingConfig)) {
|
if (validation.warning && shouldShowWarnings(thinkingConfig)) {
|
||||||
console.warn(warn(validation.warning));
|
console.warn(warn(validation.warning));
|
||||||
}
|
}
|
||||||
@@ -174,7 +234,11 @@ export function applyThinkingConfig(
|
|||||||
// Otherwise, continue to process tiers with their own config (skip main model)
|
// Otherwise, continue to process tiers with their own config (skip main model)
|
||||||
} else if (result.ANTHROPIC_MODEL) {
|
} else if (result.ANTHROPIC_MODEL) {
|
||||||
// Apply thinking suffix to main model (only if not off)
|
// 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
|
// 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
|
// 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
|
// Falls back to the default provider if not a composite or tier not specified
|
||||||
const tierProvider = compositeTiers?.[tier]?.provider ?? provider;
|
const tierProvider = compositeTiers?.[tier]?.provider ?? provider;
|
||||||
|
const normalizedTierModel = normalizeModelForThinkingLookup(model, tierProvider);
|
||||||
|
|
||||||
// Check if this tier's model supports thinking (using tier-specific provider)
|
// Check if this tier's model supports thinking (using tier-specific provider)
|
||||||
if (!supportsThinking(tierProvider, model)) {
|
if (!supportsThinking(tierProvider, normalizedTierModel)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -224,7 +289,7 @@ export function applyThinkingConfig(
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
result[tierVar] = applyThinkingSuffix(model, tierThinkingValue);
|
result[tierVar] = applyThinkingSuffixForProvider(model, tierThinkingValue, tierProvider);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -176,7 +176,13 @@ export function buildClaudeEnvironment(config: ProxyChainConfig): Record<string,
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Apply thinking configuration to model (auto tier-based or manual override)
|
// 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
|
// Apply extended context suffix for 1M token context window
|
||||||
// Auto-enabled for Gemini, opt-in for Claude (--1m flag)
|
// Auto-enabled for Gemini, opt-in for Claude (--1m flag)
|
||||||
|
|||||||
@@ -63,6 +63,7 @@ import {
|
|||||||
handleQuotaCheck,
|
handleQuotaCheck,
|
||||||
} from './retry-handler';
|
} from './retry-handler';
|
||||||
import { checkOrJoinProxy, registerProxySession, setupCleanupHandlers } from './session-bridge';
|
import { checkOrJoinProxy, registerProxySession, setupCleanupHandlers } from './session-bridge';
|
||||||
|
import { parseThinkingOverride } from './thinking-arg-parser';
|
||||||
import {
|
import {
|
||||||
warnCrossProviderDuplicates,
|
warnCrossProviderDuplicates,
|
||||||
cleanupStaleAutoPauses,
|
cleanupStaleAutoPauses,
|
||||||
@@ -306,50 +307,36 @@ export async function execClaudeWithCLIProxy(
|
|||||||
setNickname = argsWithoutProxy[nicknameIdx + 1];
|
setNickname = argsWithoutProxy[nicknameIdx + 1];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse --thinking flag
|
// Parse --thinking / --effort flags (aliases; first occurrence wins)
|
||||||
let thinkingOverride: string | number | undefined;
|
const thinkingParse = parseThinkingOverride(argsWithoutProxy);
|
||||||
const thinkingEqArg = argsWithoutProxy.find((arg) => arg.startsWith('--thinking='));
|
if (thinkingParse.error) {
|
||||||
if (thinkingEqArg) {
|
const { flag } = thinkingParse.error;
|
||||||
const val = thinkingEqArg.substring('--thinking='.length);
|
console.error(fail(`${flag} requires a value`));
|
||||||
if (!val || val.trim() === '') {
|
|
||||||
console.error(fail('--thinking requires a value'));
|
if (provider === 'codex') {
|
||||||
console.error(' Examples: --thinking=low, --thinking=8192, --thinking=off');
|
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');
|
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(
|
process.exit(1);
|
||||||
(arg) => arg === '--thinking' || arg.startsWith('--thinking=')
|
}
|
||||||
|
|
||||||
|
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(
|
if (thinkingParse.sourceFlag === '--effort' && provider !== 'codex') {
|
||||||
(arg) => arg === '--thinking' || arg.startsWith('--thinking=')
|
console.warn(
|
||||||
);
|
warn(
|
||||||
if (allThinkingFlags.length > 1) {
|
'`--effort` is primarily for codex. Continuing as alias of `--thinking` for compatibility.'
|
||||||
console.warn(
|
)
|
||||||
`[!] Multiple --thinking flags detected. Using first occurrence: --thinking ${nextArg}`
|
);
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse --1m / --no-1m flags for extended context (1M token window)
|
// Parse --1m / --no-1m flags for extended context (1M token window)
|
||||||
@@ -868,6 +855,7 @@ export async function execClaudeWithCLIProxy(
|
|||||||
'--use',
|
'--use',
|
||||||
'--nickname',
|
'--nickname',
|
||||||
'--thinking',
|
'--thinking',
|
||||||
|
'--effort',
|
||||||
'--1m',
|
'--1m',
|
||||||
'--no-1m',
|
'--no-1m',
|
||||||
'--incognito',
|
'--incognito',
|
||||||
@@ -879,11 +867,13 @@ export async function execClaudeWithCLIProxy(
|
|||||||
const claudeArgs = argsWithoutProxy.filter((arg, idx) => {
|
const claudeArgs = argsWithoutProxy.filter((arg, idx) => {
|
||||||
if (ccsFlags.includes(arg)) return false;
|
if (ccsFlags.includes(arg)) return false;
|
||||||
if (arg.startsWith('--thinking=')) 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 (arg.startsWith('--1m=') || arg.startsWith('--no-1m=')) return false;
|
||||||
if (
|
if (
|
||||||
argsWithoutProxy[idx - 1] === '--use' ||
|
argsWithoutProxy[idx - 1] === '--use' ||
|
||||||
argsWithoutProxy[idx - 1] === '--nickname' ||
|
argsWithoutProxy[idx - 1] === '--nickname' ||
|
||||||
argsWithoutProxy[idx - 1] === '--thinking'
|
argsWithoutProxy[idx - 1] === '--thinking' ||
|
||||||
|
argsWithoutProxy[idx - 1] === '--effort'
|
||||||
)
|
)
|
||||||
return false;
|
return false;
|
||||||
return true;
|
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 };
|
||||||
|
}
|
||||||
@@ -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 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 agy', 'Antigravity (Claude/Gemini models)'],
|
||||||
['ccs qwen', 'Qwen Code (qwen3-coder)'],
|
['ccs qwen', 'Qwen Code (qwen3-coder)'],
|
||||||
['ccs kiro', 'Kiro (AWS CodeWhisperer Claude models)'],
|
['ccs kiro', 'Kiro (AWS CodeWhisperer Claude models)'],
|
||||||
@@ -187,6 +187,7 @@ Run ${color('ccs config', 'command')} for web dashboard`.trim();
|
|||||||
'ccs <provider> --thinking <value>',
|
'ccs <provider> --thinking <value>',
|
||||||
'Set thinking budget (low/medium/high/xhigh/auto/off or number)',
|
'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> --1m', 'Enable 1M token context window'],
|
||||||
['ccs <provider> --no-1m', 'Disable 1M context (use 200K default)'],
|
['ccs <provider> --no-1m', 'Disable 1M context (use 200K default)'],
|
||||||
['ccs <provider> --logout', 'Clear authentication'],
|
['ccs <provider> --logout', 'Clear authentication'],
|
||||||
@@ -332,7 +333,7 @@ Run ${color('ccs config', 'command')} for web dashboard`.trim();
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
// W3: Thinking Budget explanation
|
// W3: Thinking Budget explanation
|
||||||
printSubSection('Extended Thinking (--thinking)', [
|
printSubSection('Extended Thinking / Reasoning', [
|
||||||
['--thinking off', 'Disable extended thinking'],
|
['--thinking off', 'Disable extended thinking'],
|
||||||
['--thinking auto', 'Let model decide dynamically'],
|
['--thinking auto', 'Let model decide dynamically'],
|
||||||
['--thinking low', '1K tokens - Quick responses'],
|
['--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 xhigh', '32K tokens - Maximum depth'],
|
||||||
['--thinking <number>', 'Custom token budget (512-100000)'],
|
['--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'],
|
['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)
|
// Extended Context (1M)
|
||||||
|
|||||||
@@ -638,7 +638,9 @@ function generateYamlWithComments(config: UnifiedConfig): string {
|
|||||||
lines.push('# Thinking: Extended thinking/reasoning budget configuration');
|
lines.push('# Thinking: Extended thinking/reasoning budget configuration');
|
||||||
lines.push('# Controls reasoning depth for supported providers (agy, gemini, codex).');
|
lines.push('# Controls reasoning depth for supported providers (agy, gemini, codex).');
|
||||||
lines.push('#');
|
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('# 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('# Override: Set global override value (number or level name)');
|
||||||
lines.push('# Provider overrides: Per-provider tier defaults');
|
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_AUTH_TOKEN).toBe('remote-auth-token');
|
||||||
expect(env.ANTHROPIC_BASE_URL).not.toContain('/api/provider/');
|
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
|
// Opus stays unchanged because function returned early
|
||||||
expect(result.ANTHROPIC_DEFAULT_OPUS_MODEL).toBe('claude-opus-4-6-thinking');
|
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">
|
<p className="text-sm text-muted-foreground">
|
||||||
{mode === 'auto' && 'Automatically set thinking based on model tier'}
|
{mode === 'auto' && 'Automatically set thinking based on model tier'}
|
||||||
{mode === 'off' && 'Disable extended thinking'}
|
{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>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
@@ -208,13 +209,15 @@ export default function ThinkingSection() {
|
|||||||
<div className="p-4 rounded-lg border bg-muted/30">
|
<div className="p-4 rounded-lg border bg-muted/30">
|
||||||
<h4 className="text-sm font-medium mb-2">CLI Override</h4>
|
<h4 className="text-sm font-medium mb-2">CLI Override</h4>
|
||||||
<p className="text-sm text-muted-foreground mb-2">
|
<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 per session with{' '}
|
||||||
override settings per session:
|
<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>
|
</p>
|
||||||
<div className="space-y-1 text-sm font-mono text-muted-foreground">
|
<div className="space-y-1 text-sm font-mono text-muted-foreground">
|
||||||
<p>ccs gemini --thinking high</p>
|
<p>ccs gemini --thinking high</p>
|
||||||
<p>ccs agy --thinking 16384</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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user