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',