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:
Tam Nhu Tran committed 2026-02-19 12:45:23 +07:00
1 parent 7efbac6c1a
commit c48ed2ea7f
7 files changed
+515 -12

No files matched your search

+23 -1
View File
@@ -25,6 +25,8 @@ export interface CodexReasoningProxyConfig {
* Example: '/api/provider/codex' will transform '/api/provider/codex/v1/messages' to '/v1/messages'
*/
stripPathPrefix?: string;
/** When true, skip reasoning effort injection entirely (thinking mode: off) */
disableEffort?: boolean;
}
const EXTENDED_CONTEXT_SUFFIX_REGEX = /\[1m\]$/i;
@@ -147,7 +149,12 @@ export class CodexReasoningProxy {
private readonly config: Required<
Pick<
CodexReasoningProxyConfig,
'upstreamBaseUrl' | 'verbose' | 'timeoutMs' | 'defaultEffort' | 'traceFilePath'
| 'upstreamBaseUrl'
| 'verbose'
| 'timeoutMs'
| 'defaultEffort'
| 'traceFilePath'
| 'disableEffort'
>
> &
Pick<CodexReasoningProxyConfig, 'modelMap' | 'stripPathPrefix'>;
@@ -170,6 +177,7 @@ export class CodexReasoningProxy {
defaultEffort: config.defaultEffort ?? 'medium',
traceFilePath: config.traceFilePath ?? '',
stripPathPrefix: config.stripPathPrefix,
disableEffort: config.disableEffort ?? false,
};
this.modelEffort = buildCodexModelEffortMap(this.config.modelMap, this.config.defaultEffort);
}
@@ -330,6 +338,20 @@ export class CodexReasoningProxy {
? stripExtendedContextSuffix(originalModel)
: 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:
// - upstream model: `gpt-5.2-codex`
// - reasoning.effort: `xhigh`
+50 -2
View File
@@ -49,7 +49,7 @@ import {
installWebSearchHook,
displayWebSearchStatus,
} 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 { HttpsTunnelProxy } from '../https-tunnel-proxy';
import { isKiroAuthMethod, KiroAuthMethod, normalizeKiroAuthMethod } from '../auth/auth-types';
@@ -354,7 +354,20 @@ export async function execClaudeWithCLIProxy(
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) {
console.warn(
`[!] 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 === 'true';
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({
upstreamBaseUrl: postSanitizationBaseUrl,
verbose,
defaultEffort: 'medium',
disableEffort: codexThinkingOff,
traceFilePath: traceEnabled ? path.join(getCcsDir(), 'codex-reasoning-proxy.log') : '',
modelMap: {
defaultModel: initialEnvVars.ANTHROPIC_MODEL,
@@ -875,6 +896,33 @@ export async function execClaudeWithCLIProxy(
const webSearchEnv = getWebSearchHookEnv();
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
const ccsFlags = [
'--auth',
+14
View File
@@ -68,6 +68,11 @@ function showHelp(): void {
console.log(' --timeout <s> Set analysis timeout (seconds)');
console.log(' --set-model <p> <m> Set model for provider');
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(' --port, -p PORT Specify server port (default: auto-detect)');
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 image-analysis Show image settings');
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('');
}
@@ -101,6 +108,13 @@ export async function handleConfigCommand(args: string[]): Promise<void> {
return;
}
// Route thinking subcommand
if (args[0] === 'thinking') {
const { handleConfigThinkingCommand } = await import('./config-thinking-command');
await handleConfigThinkingCommand(args.slice(1));
return;
}
await initUI();
const options = parseArgs(args);
+279
View File
@@ -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();
}
+3
View File
@@ -282,6 +282,8 @@ Run ${color('ccs config', 'command')} for web dashboard`.trim();
['ccs config auth show', 'Show dashboard auth status'],
['ccs config image-analysis', 'Show image analysis settings'],
['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 persist <profile>', 'Write profile env to ~/.claude/settings.json'],
['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_HOME', 'Override home directory (legacy, appends .ccs)'],
['CCS_DEBUG', 'Enable debug logging'],
['CCS_THINKING', 'Override thinking level (flag > env > config)'],
]);
// CLI Proxy env vars