mirror of
https://github.com/tiennm99/ccs.git
synced 2026-10-14 22:17:20 +00:00
fix(thinking): harden codex reasoning controls across cli and dashboard
This commit is contained in:
1 parent
c48ed2ea7f
commit
92e2ec111d
16 files changed
+860
-146
No files matched your search
@@ -141,4 +141,87 @@ describe('CodexReasoningProxy extended-context compatibility', () => {
|
||||
expect(capturedBody?.model).toBe('gpt-5.3-codex');
|
||||
expect((capturedBody?.reasoning as JsonRecord | undefined)?.effort).toBe('high');
|
||||
});
|
||||
|
||||
it('skips reasoning injection when disableEffort is enabled', async () => {
|
||||
let capturedBody: JsonRecord | null = null;
|
||||
|
||||
const upstream = http.createServer((req, res) => {
|
||||
let rawBody = '';
|
||||
req.setEncoding('utf8');
|
||||
req.on('data', (chunk) => {
|
||||
rawBody += chunk;
|
||||
});
|
||||
req.on('end', () => {
|
||||
capturedBody = rawBody ? (JSON.parse(rawBody) as JsonRecord) : {};
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ ok: true }));
|
||||
});
|
||||
});
|
||||
cleanupServers.push(upstream);
|
||||
|
||||
const upstreamPort = await listenOnRandomPort(upstream);
|
||||
const proxy = new CodexReasoningProxy({
|
||||
upstreamBaseUrl: `http://127.0.0.1:${upstreamPort}`,
|
||||
modelMap: {
|
||||
sonnetModel: 'gpt-5.3-codex-high',
|
||||
},
|
||||
disableEffort: true,
|
||||
});
|
||||
|
||||
const proxyPort = await proxy.start();
|
||||
const response = await postJson(
|
||||
`http://127.0.0.1:${proxyPort}/api/provider/codex/v1/messages`,
|
||||
{
|
||||
model: 'gpt-5.3-codex-high',
|
||||
messages: [],
|
||||
}
|
||||
);
|
||||
|
||||
proxy.stop();
|
||||
|
||||
expect(response.statusCode).toBe(200);
|
||||
expect(capturedBody?.model).toBe('gpt-5.3-codex');
|
||||
expect((capturedBody?.reasoning as JsonRecord | undefined)?.effort).toBeUndefined();
|
||||
});
|
||||
|
||||
it('does not strip unknown model ids that merely end with "-high"', async () => {
|
||||
let capturedBody: JsonRecord | null = null;
|
||||
|
||||
const upstream = http.createServer((req, res) => {
|
||||
let rawBody = '';
|
||||
req.setEncoding('utf8');
|
||||
req.on('data', (chunk) => {
|
||||
rawBody += chunk;
|
||||
});
|
||||
req.on('end', () => {
|
||||
capturedBody = rawBody ? (JSON.parse(rawBody) as JsonRecord) : {};
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ ok: true }));
|
||||
});
|
||||
});
|
||||
cleanupServers.push(upstream);
|
||||
|
||||
const upstreamPort = await listenOnRandomPort(upstream);
|
||||
const proxy = new CodexReasoningProxy({
|
||||
upstreamBaseUrl: `http://127.0.0.1:${upstreamPort}`,
|
||||
modelMap: {
|
||||
defaultModel: 'gpt-5.1-codex-mini',
|
||||
},
|
||||
defaultEffort: 'medium',
|
||||
});
|
||||
|
||||
const proxyPort = await proxy.start();
|
||||
const response = await postJson(
|
||||
`http://127.0.0.1:${proxyPort}/api/provider/codex/v1/messages`,
|
||||
{
|
||||
model: 'enterprise-internal-high',
|
||||
messages: [],
|
||||
}
|
||||
);
|
||||
|
||||
proxy.stop();
|
||||
|
||||
expect(response.statusCode).toBe(200);
|
||||
expect(capturedBody?.model).toBe('enterprise-internal-high');
|
||||
});
|
||||
});
|
||||
@@ -311,6 +311,24 @@ describe('applyThinkingConfig - composite variant integration', () => {
|
||||
expect(result.ANTHROPIC_DEFAULT_HAIKU_MODEL).toBe('claude-haiku-4-5-20251001');
|
||||
});
|
||||
|
||||
it('treats off override aliases case-insensitively', () => {
|
||||
const envVars: NodeJS.ProcessEnv = {
|
||||
ANTHROPIC_MODEL: 'claude-sonnet-4-5-thinking',
|
||||
ANTHROPIC_DEFAULT_OPUS_MODEL: 'claude-opus-4-6-thinking',
|
||||
ANTHROPIC_DEFAULT_SONNET_MODEL: 'claude-sonnet-4-5-thinking',
|
||||
ANTHROPIC_DEFAULT_HAIKU_MODEL: 'claude-haiku-4-5-20251001',
|
||||
};
|
||||
|
||||
const result = applyThinkingConfig(envVars, 'agy' as CLIProxyProvider, 'OFF', {
|
||||
opus: 'xhigh',
|
||||
sonnet: 'high',
|
||||
});
|
||||
|
||||
expect(result.ANTHROPIC_MODEL).toBe('claude-sonnet-4-5-thinking');
|
||||
expect(result.ANTHROPIC_DEFAULT_OPUS_MODEL).toBe('claude-opus-4-6-thinking');
|
||||
expect(result.ANTHROPIC_DEFAULT_SONNET_MODEL).toBe('claude-sonnet-4-5-thinking');
|
||||
});
|
||||
|
||||
it('uses per-tier provider capability checks for mixed-provider composites', () => {
|
||||
const envVars: NodeJS.ProcessEnv = {
|
||||
ANTHROPIC_MODEL: 'gemini-2.5-pro',
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
import { describe, expect, it } from 'bun:test';
|
||||
import type { ThinkingConfig } from '../../../src/config/unified-config-types';
|
||||
import {
|
||||
buildThinkingStartupStatus,
|
||||
parseEnvThinkingOverride,
|
||||
resolveRuntimeThinkingOverride,
|
||||
shouldDisableCodexReasoning,
|
||||
} from '../../../src/cliproxy/executor/thinking-override-resolver';
|
||||
|
||||
const baseConfig: ThinkingConfig = {
|
||||
mode: 'auto',
|
||||
tier_defaults: {
|
||||
opus: 'high',
|
||||
sonnet: 'medium',
|
||||
haiku: 'low',
|
||||
},
|
||||
show_warnings: true,
|
||||
};
|
||||
|
||||
describe('thinking-override-resolver', () => {
|
||||
it('parses env thinking values with CLI-compatible integer handling', () => {
|
||||
expect(parseEnvThinkingOverride(undefined)).toBeUndefined();
|
||||
expect(parseEnvThinkingOverride(' ')).toBeUndefined();
|
||||
expect(parseEnvThinkingOverride('8192')).toBe(8192);
|
||||
expect(parseEnvThinkingOverride(' OFF ')).toBe('off');
|
||||
expect(parseEnvThinkingOverride('bogus')).toBeUndefined();
|
||||
expect(parseEnvThinkingOverride('-1')).toBeUndefined();
|
||||
expect(parseEnvThinkingOverride('100001')).toBeUndefined();
|
||||
});
|
||||
|
||||
it('resolves runtime priority as flag > env', () => {
|
||||
expect(resolveRuntimeThinkingOverride('high', 'low')).toEqual({
|
||||
thinkingOverride: 'high',
|
||||
thinkingSource: 'flag',
|
||||
});
|
||||
expect(resolveRuntimeThinkingOverride(undefined, 'xhigh')).toEqual({
|
||||
thinkingOverride: 'xhigh',
|
||||
thinkingSource: 'env',
|
||||
});
|
||||
expect(resolveRuntimeThinkingOverride(undefined, 'invalid')).toEqual({
|
||||
thinkingOverride: undefined,
|
||||
thinkingSource: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
it('disables codex reasoning for off aliases regardless of case', () => {
|
||||
expect(shouldDisableCodexReasoning(baseConfig, 'OFF')).toBe(true);
|
||||
expect(
|
||||
shouldDisableCodexReasoning(
|
||||
{
|
||||
...baseConfig,
|
||||
mode: 'off',
|
||||
},
|
||||
'high'
|
||||
)
|
||||
).toBe(false);
|
||||
expect(
|
||||
shouldDisableCodexReasoning(
|
||||
{
|
||||
...baseConfig,
|
||||
mode: 'manual',
|
||||
override: 'off',
|
||||
},
|
||||
undefined
|
||||
)
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it('builds startup status from effective precedence instead of raw config mode', () => {
|
||||
const offConfig: ThinkingConfig = { ...baseConfig, mode: 'off' };
|
||||
|
||||
expect(buildThinkingStartupStatus(offConfig, 'high', 'env')).toEqual({
|
||||
thinkingLabel: 'high',
|
||||
sourceLabel: 'env: CCS_THINKING',
|
||||
});
|
||||
|
||||
expect(buildThinkingStartupStatus(offConfig, undefined, undefined)).toEqual({
|
||||
thinkingLabel: 'off',
|
||||
sourceLabel: 'config: off',
|
||||
});
|
||||
|
||||
expect(buildThinkingStartupStatus(baseConfig, 'off', 'flag', '--effort off')).toEqual({
|
||||
thinkingLabel: 'off',
|
||||
sourceLabel: 'flag: --effort off',
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,43 @@
|
||||
import { describe, expect, it } from 'bun:test';
|
||||
import {
|
||||
parseThinkingCommandArgs,
|
||||
parseThinkingOverrideInput,
|
||||
} from '../../../src/commands/config-thinking-command';
|
||||
|
||||
describe('config thinking command parser', () => {
|
||||
it('rejects missing required option values', () => {
|
||||
const result = parseThinkingCommandArgs(['--mode']);
|
||||
expect(result.error).toBe('--mode requires a value');
|
||||
});
|
||||
|
||||
it('rejects unknown options', () => {
|
||||
const result = parseThinkingCommandArgs(['--unknown-flag']);
|
||||
expect(result.error).toBe('Unknown option: --unknown-flag');
|
||||
});
|
||||
|
||||
it('parses clear-provider-override with optional tier', () => {
|
||||
const withTier = parseThinkingCommandArgs(['--clear-provider-override', 'codex', 'opus']);
|
||||
expect(withTier.error).toBeUndefined();
|
||||
expect(withTier.options.clearProviderOverride).toEqual({ provider: 'codex', tier: 'opus' });
|
||||
|
||||
const withoutTier = parseThinkingCommandArgs(['--clear-provider-override', 'codex']);
|
||||
expect(withoutTier.error).toBeUndefined();
|
||||
expect(withoutTier.options.clearProviderOverride).toEqual({ provider: 'codex', tier: undefined });
|
||||
});
|
||||
});
|
||||
|
||||
describe('config thinking override normalization', () => {
|
||||
it('normalizes off aliases and case', () => {
|
||||
expect(parseThinkingOverrideInput('OFF')).toEqual({ value: 'off' });
|
||||
expect(parseThinkingOverrideInput('0')).toEqual({ value: 'off' });
|
||||
});
|
||||
|
||||
it('accepts valid levels', () => {
|
||||
expect(parseThinkingOverrideInput('High')).toEqual({ value: 'high' });
|
||||
});
|
||||
|
||||
it('validates numeric bounds', () => {
|
||||
expect(parseThinkingOverrideInput('100001').error).toContain('between 0 and 100000');
|
||||
expect(parseThinkingOverrideInput('8192')).toEqual({ value: 8192 });
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user