mirror of
https://github.com/tiennm99/ccs.git
synced 2026-08-15 18:23:12 +00:00
feat(thinking): complete thinking UX/DX for all providers
- Fix codex off-mode bug: add disableEffort to CodexReasoningProxy so mode=off actually skips reasoning injection - Add CCS_THINKING env var (priority: flag > env > config) - Add startup feedback: [i] Thinking: <level> (<source>) - Add ccs config thinking CLI subcommand with --mode, --override, --tier, --provider-override, --clear-override - Dashboard: add manual override selector when mode=manual - Dashboard: add provider overrides collapsible section - Dashboard: update info box with CCS_THINKING env var docs - Update help-command.ts with CCS_THINKING and config thinking Closes #583
This commit is contained in:
@@ -25,6 +25,8 @@ export interface CodexReasoningProxyConfig {
|
|||||||
* Example: '/api/provider/codex' will transform '/api/provider/codex/v1/messages' to '/v1/messages'
|
* Example: '/api/provider/codex' will transform '/api/provider/codex/v1/messages' to '/v1/messages'
|
||||||
*/
|
*/
|
||||||
stripPathPrefix?: string;
|
stripPathPrefix?: string;
|
||||||
|
/** When true, skip reasoning effort injection entirely (thinking mode: off) */
|
||||||
|
disableEffort?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const EXTENDED_CONTEXT_SUFFIX_REGEX = /\[1m\]$/i;
|
const EXTENDED_CONTEXT_SUFFIX_REGEX = /\[1m\]$/i;
|
||||||
@@ -147,7 +149,12 @@ export class CodexReasoningProxy {
|
|||||||
private readonly config: Required<
|
private readonly config: Required<
|
||||||
Pick<
|
Pick<
|
||||||
CodexReasoningProxyConfig,
|
CodexReasoningProxyConfig,
|
||||||
'upstreamBaseUrl' | 'verbose' | 'timeoutMs' | 'defaultEffort' | 'traceFilePath'
|
| 'upstreamBaseUrl'
|
||||||
|
| 'verbose'
|
||||||
|
| 'timeoutMs'
|
||||||
|
| 'defaultEffort'
|
||||||
|
| 'traceFilePath'
|
||||||
|
| 'disableEffort'
|
||||||
>
|
>
|
||||||
> &
|
> &
|
||||||
Pick<CodexReasoningProxyConfig, 'modelMap' | 'stripPathPrefix'>;
|
Pick<CodexReasoningProxyConfig, 'modelMap' | 'stripPathPrefix'>;
|
||||||
@@ -170,6 +177,7 @@ export class CodexReasoningProxy {
|
|||||||
defaultEffort: config.defaultEffort ?? 'medium',
|
defaultEffort: config.defaultEffort ?? 'medium',
|
||||||
traceFilePath: config.traceFilePath ?? '',
|
traceFilePath: config.traceFilePath ?? '',
|
||||||
stripPathPrefix: config.stripPathPrefix,
|
stripPathPrefix: config.stripPathPrefix,
|
||||||
|
disableEffort: config.disableEffort ?? false,
|
||||||
};
|
};
|
||||||
this.modelEffort = buildCodexModelEffortMap(this.config.modelMap, this.config.defaultEffort);
|
this.modelEffort = buildCodexModelEffortMap(this.config.modelMap, this.config.defaultEffort);
|
||||||
}
|
}
|
||||||
@@ -330,6 +338,20 @@ export class CodexReasoningProxy {
|
|||||||
? stripExtendedContextSuffix(originalModel)
|
? stripExtendedContextSuffix(originalModel)
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
|
// When effort is disabled (thinking mode: off), strip model suffix but don't inject reasoning
|
||||||
|
if (this.config.disableEffort) {
|
||||||
|
const suffixParsed = normalizedRequestModel
|
||||||
|
? parseModelEffortSuffix(normalizedRequestModel)
|
||||||
|
: null;
|
||||||
|
const upstreamModel = suffixParsed?.upstreamModel ?? normalizedRequestModel;
|
||||||
|
const forwarded =
|
||||||
|
upstreamModel && isRecord(parsed) ? { ...parsed, model: upstreamModel } : parsed;
|
||||||
|
|
||||||
|
this.log(`[disabled] model=${originalModel ?? 'null'} -> passthrough (no reasoning)`);
|
||||||
|
await this.forwardJson(req, res, fullUpstreamUrl, forwarded);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Support "model aliases" like `gpt-5.2-codex-xhigh` by translating to:
|
// Support "model aliases" like `gpt-5.2-codex-xhigh` by translating to:
|
||||||
// - upstream model: `gpt-5.2-codex`
|
// - upstream model: `gpt-5.2-codex`
|
||||||
// - reasoning.effort: `xhigh`
|
// - reasoning.effort: `xhigh`
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ import {
|
|||||||
installWebSearchHook,
|
installWebSearchHook,
|
||||||
displayWebSearchStatus,
|
displayWebSearchStatus,
|
||||||
} from '../../utils/websearch-manager';
|
} from '../../utils/websearch-manager';
|
||||||
import { loadOrCreateUnifiedConfig } from '../../config/unified-config-loader';
|
import { loadOrCreateUnifiedConfig, getThinkingConfig } from '../../config/unified-config-loader';
|
||||||
import { installImageAnalyzerHook } from '../../utils/hooks';
|
import { installImageAnalyzerHook } from '../../utils/hooks';
|
||||||
import { HttpsTunnelProxy } from '../https-tunnel-proxy';
|
import { HttpsTunnelProxy } from '../https-tunnel-proxy';
|
||||||
import { isKiroAuthMethod, KiroAuthMethod, normalizeKiroAuthMethod } from '../auth/auth-types';
|
import { isKiroAuthMethod, KiroAuthMethod, normalizeKiroAuthMethod } from '../auth/auth-types';
|
||||||
@@ -354,7 +354,20 @@ export async function execClaudeWithCLIProxy(
|
|||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
const thinkingOverride = thinkingParse.value;
|
// Priority: CLI flag > CCS_THINKING env var > config.yaml
|
||||||
|
let thinkingOverride = thinkingParse.value;
|
||||||
|
let thinkingSource: 'flag' | 'env' | 'config' | undefined =
|
||||||
|
thinkingOverride !== undefined ? 'flag' : undefined;
|
||||||
|
|
||||||
|
if (thinkingOverride === undefined && process.env.CCS_THINKING) {
|
||||||
|
const envVal = process.env.CCS_THINKING.trim();
|
||||||
|
if (envVal) {
|
||||||
|
// Parse same as CLI: integer string → number, else string
|
||||||
|
thinkingOverride = /^-?\d+$/.test(envVal) ? Number.parseInt(envVal, 10) : envVal;
|
||||||
|
thinkingSource = 'env';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (thinkingParse.duplicateDisplays.length > 0) {
|
if (thinkingParse.duplicateDisplays.length > 0) {
|
||||||
console.warn(
|
console.warn(
|
||||||
`[!] Multiple reasoning flags detected. Using first occurrence: ${thinkingParse.sourceDisplay}`
|
`[!] Multiple reasoning flags detected. Using first occurrence: ${thinkingParse.sourceDisplay}`
|
||||||
@@ -804,10 +817,18 @@ export async function execClaudeWithCLIProxy(
|
|||||||
process.env.CCS_CODEX_REASONING_TRACE === '1' ||
|
process.env.CCS_CODEX_REASONING_TRACE === '1' ||
|
||||||
process.env.CCS_CODEX_REASONING_TRACE === 'true';
|
process.env.CCS_CODEX_REASONING_TRACE === 'true';
|
||||||
const stripPathPrefix = useRemoteProxy ? '/api/provider/codex' : undefined;
|
const stripPathPrefix = useRemoteProxy ? '/api/provider/codex' : undefined;
|
||||||
|
const thinkingCfg = getThinkingConfig();
|
||||||
|
const codexThinkingOff =
|
||||||
|
(thinkingCfg.mode === 'off' && thinkingOverride === undefined) ||
|
||||||
|
thinkingOverride === 'off' ||
|
||||||
|
(thinkingOverride === undefined &&
|
||||||
|
thinkingCfg.mode === 'manual' &&
|
||||||
|
thinkingCfg.override === 'off');
|
||||||
codexReasoningProxy = new CodexReasoningProxy({
|
codexReasoningProxy = new CodexReasoningProxy({
|
||||||
upstreamBaseUrl: postSanitizationBaseUrl,
|
upstreamBaseUrl: postSanitizationBaseUrl,
|
||||||
verbose,
|
verbose,
|
||||||
defaultEffort: 'medium',
|
defaultEffort: 'medium',
|
||||||
|
disableEffort: codexThinkingOff,
|
||||||
traceFilePath: traceEnabled ? path.join(getCcsDir(), 'codex-reasoning-proxy.log') : '',
|
traceFilePath: traceEnabled ? path.join(getCcsDir(), 'codex-reasoning-proxy.log') : '',
|
||||||
modelMap: {
|
modelMap: {
|
||||||
defaultModel: initialEnvVars.ANTHROPIC_MODEL,
|
defaultModel: initialEnvVars.ANTHROPIC_MODEL,
|
||||||
@@ -875,6 +896,33 @@ export async function execClaudeWithCLIProxy(
|
|||||||
const webSearchEnv = getWebSearchHookEnv();
|
const webSearchEnv = getWebSearchHookEnv();
|
||||||
logEnvironment(env, webSearchEnv, verbose);
|
logEnvironment(env, webSearchEnv, verbose);
|
||||||
|
|
||||||
|
// 11b. Print thinking status feedback (TTY only, non-piped sessions)
|
||||||
|
if (process.stderr.isTTY) {
|
||||||
|
const thinkingCfgStatus = getThinkingConfig();
|
||||||
|
let thinkingLabel: string;
|
||||||
|
let sourceLabel: string;
|
||||||
|
|
||||||
|
if (thinkingOverride === 'off' || thinkingCfgStatus.mode === 'off') {
|
||||||
|
thinkingLabel = 'off';
|
||||||
|
sourceLabel =
|
||||||
|
thinkingSource === 'flag' ? 'flag' : thinkingSource === 'env' ? 'env' : 'config';
|
||||||
|
} else if (thinkingSource === 'flag') {
|
||||||
|
thinkingLabel = String(thinkingOverride);
|
||||||
|
sourceLabel = `flag: ${thinkingParse.sourceDisplay}`;
|
||||||
|
} else if (thinkingSource === 'env') {
|
||||||
|
thinkingLabel = String(thinkingOverride);
|
||||||
|
sourceLabel = 'env: CCS_THINKING';
|
||||||
|
} else if (thinkingCfgStatus.mode === 'manual' && thinkingCfgStatus.override !== undefined) {
|
||||||
|
thinkingLabel = String(thinkingCfgStatus.override);
|
||||||
|
sourceLabel = 'config: manual';
|
||||||
|
} else {
|
||||||
|
thinkingLabel = thinkingCfgStatus.mode === 'auto' ? 'auto' : 'default';
|
||||||
|
sourceLabel = 'config: auto';
|
||||||
|
}
|
||||||
|
|
||||||
|
console.error(`[i] Thinking: ${thinkingLabel} (${sourceLabel})`);
|
||||||
|
}
|
||||||
|
|
||||||
// 12. Filter CCS-specific flags before passing to Claude CLI
|
// 12. Filter CCS-specific flags before passing to Claude CLI
|
||||||
const ccsFlags = [
|
const ccsFlags = [
|
||||||
'--auth',
|
'--auth',
|
||||||
|
|||||||
@@ -68,6 +68,11 @@ function showHelp(): void {
|
|||||||
console.log(' --timeout <s> Set analysis timeout (seconds)');
|
console.log(' --timeout <s> Set analysis timeout (seconds)');
|
||||||
console.log(' --set-model <p> <m> Set model for provider');
|
console.log(' --set-model <p> <m> Set model for provider');
|
||||||
console.log('');
|
console.log('');
|
||||||
|
console.log(' thinking Manage thinking/reasoning settings');
|
||||||
|
console.log(' --mode <mode> Set mode (auto, off, manual)');
|
||||||
|
console.log(' --override <l> Set persistent override level');
|
||||||
|
console.log(' --tier <t> <l> Set tier default level');
|
||||||
|
console.log('');
|
||||||
console.log('Options:');
|
console.log('Options:');
|
||||||
console.log(' --port, -p PORT Specify server port (default: auto-detect)');
|
console.log(' --port, -p PORT Specify server port (default: auto-detect)');
|
||||||
console.log(' --dev Development mode with Vite HMR');
|
console.log(' --dev Development mode with Vite HMR');
|
||||||
@@ -80,6 +85,8 @@ function showHelp(): void {
|
|||||||
console.log(' ccs config auth setup Configure dashboard login');
|
console.log(' ccs config auth setup Configure dashboard login');
|
||||||
console.log(' ccs config image-analysis Show image settings');
|
console.log(' ccs config image-analysis Show image settings');
|
||||||
console.log(' ccs config image-analysis --enable Enable feature');
|
console.log(' ccs config image-analysis --enable Enable feature');
|
||||||
|
console.log(' ccs config thinking Show thinking settings');
|
||||||
|
console.log(' ccs config thinking --mode auto Set auto mode');
|
||||||
console.log('');
|
console.log('');
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -101,6 +108,13 @@ export async function handleConfigCommand(args: string[]): Promise<void> {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Route thinking subcommand
|
||||||
|
if (args[0] === 'thinking') {
|
||||||
|
const { handleConfigThinkingCommand } = await import('./config-thinking-command');
|
||||||
|
await handleConfigThinkingCommand(args.slice(1));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
await initUI();
|
await initUI();
|
||||||
|
|
||||||
const options = parseArgs(args);
|
const options = parseArgs(args);
|
||||||
|
|||||||
@@ -0,0 +1,279 @@
|
|||||||
|
/**
|
||||||
|
* Config Thinking Command Handler
|
||||||
|
*
|
||||||
|
* Manages thinking section of config.yaml via CLI.
|
||||||
|
* Usage: ccs config thinking [options]
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { initUI, header, ok, info, warn, fail, subheader, color, dim } from '../utils/ui';
|
||||||
|
import {
|
||||||
|
getThinkingConfig,
|
||||||
|
updateUnifiedConfig,
|
||||||
|
loadOrCreateUnifiedConfig,
|
||||||
|
} from '../config/unified-config-loader';
|
||||||
|
import { DEFAULT_THINKING_TIER_DEFAULTS } from '../config/unified-config-types';
|
||||||
|
import { VALID_THINKING_LEVELS } from '../cliproxy/thinking-validator';
|
||||||
|
|
||||||
|
const VALID_THINKING_MODES = ['auto', 'off', 'manual'] as const;
|
||||||
|
|
||||||
|
interface ThinkingCommandOptions {
|
||||||
|
mode?: string;
|
||||||
|
override?: string;
|
||||||
|
clearOverride?: boolean;
|
||||||
|
tier?: { tier: string; level: string };
|
||||||
|
providerOverride?: { provider: string; tier: string; level: string };
|
||||||
|
help?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const VALID_TIERS = ['opus', 'sonnet', 'haiku'] as const;
|
||||||
|
|
||||||
|
function parseArgs(args: string[]): ThinkingCommandOptions {
|
||||||
|
const options: ThinkingCommandOptions = {};
|
||||||
|
|
||||||
|
for (let i = 0; i < args.length; i++) {
|
||||||
|
const arg = args[i];
|
||||||
|
|
||||||
|
if (arg === '--mode' && args[i + 1]) {
|
||||||
|
options.mode = args[++i];
|
||||||
|
} else if (arg === '--override' && args[i + 1]) {
|
||||||
|
options.override = args[++i];
|
||||||
|
} else if (arg === '--clear-override') {
|
||||||
|
options.clearOverride = true;
|
||||||
|
} else if (arg === '--tier' && args[i + 1] && args[i + 2]) {
|
||||||
|
options.tier = { tier: args[++i], level: args[++i] };
|
||||||
|
} else if (arg === '--provider-override' && args[i + 1] && args[i + 2] && args[i + 3]) {
|
||||||
|
options.providerOverride = {
|
||||||
|
provider: args[++i],
|
||||||
|
tier: args[++i],
|
||||||
|
level: args[++i],
|
||||||
|
};
|
||||||
|
} else if (arg === '--help' || arg === '-h') {
|
||||||
|
options.help = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return options;
|
||||||
|
}
|
||||||
|
|
||||||
|
function showHelp(): void {
|
||||||
|
console.log('');
|
||||||
|
console.log(header('ccs config thinking'));
|
||||||
|
console.log('');
|
||||||
|
console.log(' Configure extended thinking/reasoning for CLIProxy providers.');
|
||||||
|
console.log('');
|
||||||
|
|
||||||
|
console.log(subheader('Usage:'));
|
||||||
|
console.log(` ${color('ccs config thinking', 'command')} [options]`);
|
||||||
|
console.log('');
|
||||||
|
|
||||||
|
console.log(subheader('Options:'));
|
||||||
|
console.log(
|
||||||
|
` ${color('--mode <mode>', 'command')} Set mode (auto, off, manual)`
|
||||||
|
);
|
||||||
|
console.log(
|
||||||
|
` ${color('--override <level>', 'command')} Set persistent override (manual mode)`
|
||||||
|
);
|
||||||
|
console.log(
|
||||||
|
` ${color('--clear-override', 'command')} Remove persistent override`
|
||||||
|
);
|
||||||
|
console.log(
|
||||||
|
` ${color('--tier <tier> <level>', 'command')} Set tier default (opus/sonnet/haiku)`
|
||||||
|
);
|
||||||
|
console.log(
|
||||||
|
` ${color('--provider-override <p> <t> <l>', 'command')} Set provider-specific tier override`
|
||||||
|
);
|
||||||
|
console.log(` ${color('--help, -h', 'command')} Show this help`);
|
||||||
|
console.log('');
|
||||||
|
|
||||||
|
console.log(subheader('Levels:'));
|
||||||
|
console.log(
|
||||||
|
` ${dim('minimal (512), low (1K), medium (8K), high (24K), xhigh (32K), auto, off')}`
|
||||||
|
);
|
||||||
|
console.log('');
|
||||||
|
|
||||||
|
console.log(subheader('Examples:'));
|
||||||
|
console.log(
|
||||||
|
` $ ${color('ccs config thinking', 'command')} ${dim('# Show status')}`
|
||||||
|
);
|
||||||
|
console.log(
|
||||||
|
` $ ${color('ccs config thinking --mode auto', 'command')} ${dim('# Auto mode')}`
|
||||||
|
);
|
||||||
|
console.log(
|
||||||
|
` $ ${color('ccs config thinking --mode manual --override high', 'command')} ${dim('# Persistent high')}`
|
||||||
|
);
|
||||||
|
console.log(
|
||||||
|
` $ ${color('ccs config thinking --tier opus xhigh', 'command')} ${dim('# Opus -> xhigh')}`
|
||||||
|
);
|
||||||
|
console.log(
|
||||||
|
` $ ${color('ccs config thinking --provider-override codex opus xhigh', 'command')}`
|
||||||
|
);
|
||||||
|
console.log('');
|
||||||
|
|
||||||
|
console.log(subheader('Environment:'));
|
||||||
|
console.log(
|
||||||
|
` ${color('CCS_THINKING', 'command')} Override per-session via env var (priority: flag > env > config)`
|
||||||
|
);
|
||||||
|
console.log(` ${dim('Example: CCS_THINKING=high ccs codex "debug this"')}`);
|
||||||
|
console.log('');
|
||||||
|
}
|
||||||
|
|
||||||
|
function showStatus(): void {
|
||||||
|
const config = getThinkingConfig();
|
||||||
|
|
||||||
|
console.log('');
|
||||||
|
console.log(header('Thinking Configuration'));
|
||||||
|
console.log('');
|
||||||
|
|
||||||
|
// Mode
|
||||||
|
const modeText =
|
||||||
|
config.mode === 'auto' ? ok('Auto') : config.mode === 'off' ? warn('Off') : info('Manual');
|
||||||
|
console.log(` Mode: ${modeText}`);
|
||||||
|
|
||||||
|
// Override
|
||||||
|
if (config.override !== undefined) {
|
||||||
|
console.log(` Override: ${color(String(config.override), 'command')}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Warnings
|
||||||
|
console.log(` Warnings: ${config.show_warnings !== false ? 'on' : 'off'}`);
|
||||||
|
console.log('');
|
||||||
|
|
||||||
|
// Tier defaults
|
||||||
|
console.log(subheader('Tier Defaults:'));
|
||||||
|
for (const tier of VALID_TIERS) {
|
||||||
|
const level = config.tier_defaults?.[tier] ?? DEFAULT_THINKING_TIER_DEFAULTS[tier];
|
||||||
|
const isDefault = level === DEFAULT_THINKING_TIER_DEFAULTS[tier];
|
||||||
|
const suffix = isDefault ? dim(' (default)') : '';
|
||||||
|
console.log(` ${color(tier.padEnd(10), 'command')} ${level}${suffix}`);
|
||||||
|
}
|
||||||
|
console.log('');
|
||||||
|
|
||||||
|
// Provider overrides
|
||||||
|
const overrides = config.provider_overrides;
|
||||||
|
if (overrides && Object.keys(overrides).length > 0) {
|
||||||
|
console.log(subheader('Provider Overrides:'));
|
||||||
|
for (const [provider, tierOverrides] of Object.entries(overrides)) {
|
||||||
|
const parts = Object.entries(tierOverrides)
|
||||||
|
.map(([t, l]) => `${t}=${l}`)
|
||||||
|
.join(', ');
|
||||||
|
console.log(` ${color(provider.padEnd(10), 'command')} ${parts}`);
|
||||||
|
}
|
||||||
|
console.log('');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Config location
|
||||||
|
console.log(subheader('Configuration:'));
|
||||||
|
console.log(` File: ${color('~/.ccs/config.yaml', 'path')}`);
|
||||||
|
console.log(` Section: ${dim('thinking')}`);
|
||||||
|
console.log('');
|
||||||
|
|
||||||
|
// Env var hint
|
||||||
|
if (process.env.CCS_THINKING) {
|
||||||
|
console.log(info(`CCS_THINKING env var active: ${process.env.CCS_THINKING}`));
|
||||||
|
console.log('');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function handleConfigThinkingCommand(args: string[]): Promise<void> {
|
||||||
|
await initUI();
|
||||||
|
|
||||||
|
const options = parseArgs(args);
|
||||||
|
|
||||||
|
if (options.help) {
|
||||||
|
showHelp();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let hasChanges = false;
|
||||||
|
const config = loadOrCreateUnifiedConfig();
|
||||||
|
const thinkingConfig = config.thinking ?? {
|
||||||
|
mode: 'auto' as const,
|
||||||
|
tier_defaults: { ...DEFAULT_THINKING_TIER_DEFAULTS },
|
||||||
|
show_warnings: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Validate and apply --mode
|
||||||
|
if (options.mode !== undefined) {
|
||||||
|
if (!(VALID_THINKING_MODES as readonly string[]).includes(options.mode)) {
|
||||||
|
console.error(fail(`Invalid mode: ${options.mode}`));
|
||||||
|
console.error(info(`Valid modes: ${VALID_THINKING_MODES.join(', ')}`));
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
thinkingConfig.mode = options.mode as 'auto' | 'off' | 'manual';
|
||||||
|
hasChanges = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate and apply --override
|
||||||
|
if (options.override !== undefined) {
|
||||||
|
const normalized = options.override.toLowerCase().trim();
|
||||||
|
if (
|
||||||
|
!(VALID_THINKING_LEVELS as readonly string[]).includes(normalized) &&
|
||||||
|
!/^\d+$/.test(normalized)
|
||||||
|
) {
|
||||||
|
console.error(fail(`Invalid override: ${options.override}`));
|
||||||
|
console.error(info(`Valid levels: ${VALID_THINKING_LEVELS.join(', ')}, or a number`));
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
thinkingConfig.override = /^\d+$/.test(normalized)
|
||||||
|
? Number.parseInt(normalized, 10)
|
||||||
|
: normalized;
|
||||||
|
hasChanges = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply --clear-override
|
||||||
|
if (options.clearOverride) {
|
||||||
|
thinkingConfig.override = undefined;
|
||||||
|
hasChanges = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate and apply --tier
|
||||||
|
if (options.tier) {
|
||||||
|
const { tier, level } = options.tier;
|
||||||
|
if (!(VALID_TIERS as readonly string[]).includes(tier)) {
|
||||||
|
console.error(fail(`Invalid tier: ${tier}`));
|
||||||
|
console.error(info(`Valid tiers: ${VALID_TIERS.join(', ')}`));
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
if (!(VALID_THINKING_LEVELS as readonly string[]).includes(level.toLowerCase())) {
|
||||||
|
console.error(fail(`Invalid level for ${tier}: ${level}`));
|
||||||
|
console.error(info(`Valid levels: ${VALID_THINKING_LEVELS.join(', ')}`));
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
thinkingConfig.tier_defaults = {
|
||||||
|
...DEFAULT_THINKING_TIER_DEFAULTS,
|
||||||
|
...thinkingConfig.tier_defaults,
|
||||||
|
[tier]: level.toLowerCase(),
|
||||||
|
};
|
||||||
|
hasChanges = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate and apply --provider-override
|
||||||
|
if (options.providerOverride) {
|
||||||
|
const { provider, tier, level } = options.providerOverride;
|
||||||
|
if (!(VALID_TIERS as readonly string[]).includes(tier)) {
|
||||||
|
console.error(fail(`Invalid tier: ${tier}`));
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
if (!(VALID_THINKING_LEVELS as readonly string[]).includes(level.toLowerCase())) {
|
||||||
|
console.error(fail(`Invalid level: ${level}`));
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
thinkingConfig.provider_overrides = {
|
||||||
|
...thinkingConfig.provider_overrides,
|
||||||
|
[provider]: {
|
||||||
|
...thinkingConfig.provider_overrides?.[provider],
|
||||||
|
[tier]: level.toLowerCase(),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
hasChanges = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasChanges) {
|
||||||
|
updateUnifiedConfig({ thinking: thinkingConfig });
|
||||||
|
console.log(ok('Configuration updated'));
|
||||||
|
console.log('');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Always show current status
|
||||||
|
showStatus();
|
||||||
|
}
|
||||||
@@ -282,6 +282,8 @@ Run ${color('ccs config', 'command')} for web dashboard`.trim();
|
|||||||
['ccs config auth show', 'Show dashboard auth status'],
|
['ccs config auth show', 'Show dashboard auth status'],
|
||||||
['ccs config image-analysis', 'Show image analysis settings'],
|
['ccs config image-analysis', 'Show image analysis settings'],
|
||||||
['ccs config image-analysis --enable', 'Enable image analysis'],
|
['ccs config image-analysis --enable', 'Enable image analysis'],
|
||||||
|
['ccs config thinking', 'Show thinking/reasoning settings'],
|
||||||
|
['ccs config thinking --mode auto', 'Set thinking mode'],
|
||||||
['ccs config --port 3000', 'Use specific port'],
|
['ccs config --port 3000', 'Use specific port'],
|
||||||
['ccs persist <profile>', 'Write profile env to ~/.claude/settings.json'],
|
['ccs persist <profile>', 'Write profile env to ~/.claude/settings.json'],
|
||||||
['ccs persist --list-backups', 'List available settings.json backups'],
|
['ccs persist --list-backups', 'List available settings.json backups'],
|
||||||
@@ -404,6 +406,7 @@ Run ${color('ccs config', 'command')} for web dashboard`.trim();
|
|||||||
['CCS_DIR', 'Override CCS config directory (default: ~/.ccs)'],
|
['CCS_DIR', 'Override CCS config directory (default: ~/.ccs)'],
|
||||||
['CCS_HOME', 'Override home directory (legacy, appends .ccs)'],
|
['CCS_HOME', 'Override home directory (legacy, appends .ccs)'],
|
||||||
['CCS_DEBUG', 'Enable debug logging'],
|
['CCS_DEBUG', 'Enable debug logging'],
|
||||||
|
['CCS_THINKING', 'Override thinking level (flag > env > config)'],
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// CLI Proxy env vars
|
// CLI Proxy env vars
|
||||||
|
|||||||
@@ -156,6 +156,42 @@ export function useThinkingConfig() {
|
|||||||
[saveConfig]
|
[saveConfig]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const setOverride = useCallback(
|
||||||
|
(value: string | number | undefined) => {
|
||||||
|
saveConfig({ override: value });
|
||||||
|
},
|
||||||
|
[saveConfig]
|
||||||
|
);
|
||||||
|
|
||||||
|
const setProviderOverride = useCallback(
|
||||||
|
(provider: string, tier: 'opus' | 'sonnet' | 'haiku', level: string | undefined) => {
|
||||||
|
const currentOverrides = config?.provider_overrides || {};
|
||||||
|
const currentProviderOverrides = currentOverrides[provider] || {};
|
||||||
|
|
||||||
|
if (level === undefined) {
|
||||||
|
// Remove the tier override
|
||||||
|
const { [tier]: _, ...rest } = currentProviderOverrides;
|
||||||
|
const updatedProvider = Object.keys(rest).length > 0 ? rest : undefined;
|
||||||
|
const { [provider]: __, ...otherProviders } = currentOverrides;
|
||||||
|
const updatedOverrides = updatedProvider
|
||||||
|
? { ...otherProviders, [provider]: updatedProvider }
|
||||||
|
: otherProviders;
|
||||||
|
saveConfig({
|
||||||
|
provider_overrides:
|
||||||
|
Object.keys(updatedOverrides).length > 0 ? updatedOverrides : undefined,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
saveConfig({
|
||||||
|
provider_overrides: {
|
||||||
|
...currentOverrides,
|
||||||
|
[provider]: { ...currentProviderOverrides, [tier]: level },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[config, saveConfig]
|
||||||
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
config: config || DEFAULT_THINKING_CONFIG,
|
config: config || DEFAULT_THINKING_CONFIG,
|
||||||
loading,
|
loading,
|
||||||
@@ -167,5 +203,7 @@ export function useThinkingConfig() {
|
|||||||
setMode,
|
setMode,
|
||||||
setTierDefault,
|
setTierDefault,
|
||||||
setShowWarnings,
|
setShowWarnings,
|
||||||
|
setOverride,
|
||||||
|
setProviderOverride,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
* Settings section for thinking budget configuration
|
* Settings section for thinking budget configuration
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { useEffect } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Alert, AlertDescription } from '@/components/ui/alert';
|
import { Alert, AlertDescription } from '@/components/ui/alert';
|
||||||
import { ScrollArea } from '@/components/ui/scroll-area';
|
import { ScrollArea } from '@/components/ui/scroll-area';
|
||||||
@@ -16,7 +16,7 @@ import {
|
|||||||
SelectTrigger,
|
SelectTrigger,
|
||||||
SelectValue,
|
SelectValue,
|
||||||
} from '@/components/ui/select';
|
} from '@/components/ui/select';
|
||||||
import { RefreshCw, CheckCircle2, AlertCircle, Brain, Info } from 'lucide-react';
|
import { RefreshCw, CheckCircle2, AlertCircle, Brain, Info, ChevronDown } from 'lucide-react';
|
||||||
import { useThinkingConfig } from '../../hooks';
|
import { useThinkingConfig } from '../../hooks';
|
||||||
import type { ThinkingMode } from '../../types';
|
import type { ThinkingMode } from '../../types';
|
||||||
|
|
||||||
@@ -29,6 +29,14 @@ const THINKING_LEVELS = [
|
|||||||
{ value: 'auto', label: 'Auto (dynamic)' },
|
{ value: 'auto', label: 'Auto (dynamic)' },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const OVERRIDE_LEVELS = [
|
||||||
|
{ value: '__none__', label: 'None (use CLI flags only)' },
|
||||||
|
...THINKING_LEVELS,
|
||||||
|
{ value: 'off', label: 'Off (disable thinking)' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const KNOWN_PROVIDERS = ['agy', 'gemini', 'codex'] as const;
|
||||||
|
|
||||||
export default function ThinkingSection() {
|
export default function ThinkingSection() {
|
||||||
const {
|
const {
|
||||||
config,
|
config,
|
||||||
@@ -40,7 +48,10 @@ export default function ThinkingSection() {
|
|||||||
setMode,
|
setMode,
|
||||||
setTierDefault,
|
setTierDefault,
|
||||||
setShowWarnings,
|
setShowWarnings,
|
||||||
|
setOverride,
|
||||||
|
setProviderOverride,
|
||||||
} = useThinkingConfig();
|
} = useThinkingConfig();
|
||||||
|
const [providerOverridesOpen, setProviderOverridesOpen] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchConfig();
|
fetchConfig();
|
||||||
@@ -146,7 +157,7 @@ export default function ThinkingSection() {
|
|||||||
{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' &&
|
{mode === 'manual' &&
|
||||||
'Use --thinking (agy/gemini) or --effort (codex) per run'}
|
'Set a persistent override level or use CLI flags per run'}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
@@ -195,6 +206,94 @@ export default function ThinkingSection() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Manual Override (shown when mode is manual) */}
|
||||||
|
{config.mode === 'manual' && (
|
||||||
|
<div className="space-y-3">
|
||||||
|
<h3 className="text-base font-medium">Persistent Override</h3>
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
Applied to all sessions. CLI flags still take priority.
|
||||||
|
</p>
|
||||||
|
<Select
|
||||||
|
value={config.override !== undefined ? String(config.override) : '__none__'}
|
||||||
|
onValueChange={(value) => setOverride(value === '__none__' ? undefined : value)}
|
||||||
|
disabled={saving}
|
||||||
|
>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{OVERRIDE_LEVELS.map((level) => (
|
||||||
|
<SelectItem key={level.value} value={level.value}>
|
||||||
|
{level.label}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Provider Overrides (collapsible) */}
|
||||||
|
<div className="space-y-3">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="flex items-center gap-2 text-base font-medium w-full text-left"
|
||||||
|
onClick={() => setProviderOverridesOpen(!providerOverridesOpen)}
|
||||||
|
>
|
||||||
|
<ChevronDown
|
||||||
|
className={`w-4 h-4 transition-transform ${providerOverridesOpen ? 'rotate-0' : '-rotate-90'}`}
|
||||||
|
/>
|
||||||
|
Provider Overrides
|
||||||
|
</button>
|
||||||
|
{providerOverridesOpen && (
|
||||||
|
<div className="space-y-3">
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
Override tier defaults for specific providers.
|
||||||
|
</p>
|
||||||
|
{KNOWN_PROVIDERS.map((provider) => (
|
||||||
|
<div key={provider} className="space-y-2 p-3 rounded-lg bg-muted/30">
|
||||||
|
<Label className="capitalize font-medium text-sm">{provider}</Label>
|
||||||
|
<div className="grid grid-cols-3 gap-2">
|
||||||
|
{(['opus', 'sonnet', 'haiku'] as const).map((tier) => {
|
||||||
|
const currentValue =
|
||||||
|
config.provider_overrides?.[provider]?.[tier] || '__default__';
|
||||||
|
return (
|
||||||
|
<div key={tier} className="space-y-1">
|
||||||
|
<Label className="text-xs text-muted-foreground capitalize">
|
||||||
|
{tier}
|
||||||
|
</Label>
|
||||||
|
<Select
|
||||||
|
value={currentValue}
|
||||||
|
onValueChange={(value) =>
|
||||||
|
setProviderOverride(
|
||||||
|
provider,
|
||||||
|
tier,
|
||||||
|
value === '__default__' ? undefined : value
|
||||||
|
)
|
||||||
|
}
|
||||||
|
disabled={saving}
|
||||||
|
>
|
||||||
|
<SelectTrigger className="h-8 text-xs">
|
||||||
|
<SelectValue />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="__default__">Default</SelectItem>
|
||||||
|
{THINKING_LEVELS.map((level) => (
|
||||||
|
<SelectItem key={level.value} value={level.value}>
|
||||||
|
{level.label}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Show Warnings Toggle */}
|
{/* Show Warnings Toggle */}
|
||||||
<div className="flex items-center justify-between p-4 rounded-lg bg-muted/50">
|
<div className="flex items-center justify-between p-4 rounded-lg bg-muted/50">
|
||||||
<div>
|
<div>
|
||||||
@@ -212,17 +311,17 @@ export default function ThinkingSection() {
|
|||||||
|
|
||||||
{/* Info Box */}
|
{/* Info Box */}
|
||||||
<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 & Env Override</h4>
|
||||||
<p className="text-sm text-muted-foreground mb-2">
|
<p className="text-sm text-muted-foreground mb-2">
|
||||||
Override per session with{' '}
|
Override per session with flags or{' '}
|
||||||
<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">CCS_THINKING</code> env var.
|
||||||
<code className="px-1.5 py-0.5 rounded bg-muted">--effort</code> (codex):
|
Priority: flag > env > config.
|
||||||
</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 codex --effort xhigh</p>
|
<p>ccs codex --effort xhigh</p>
|
||||||
<p>ccs codex -p "Refactor this module" --effort xhigh</p>
|
<p>CCS_THINKING=high ccs codex "debug this"</p>
|
||||||
|
<p>ccs config thinking --mode auto</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user