From 46920dbc08a18c4c853547fe1e1ec32981e765de Mon Sep 17 00:00:00 2001 From: Sergey Galuza Date: Wed, 22 Apr 2026 21:09:21 +0200 Subject: [PATCH 1/4] fix(cliproxy): use adaptive thinking for Claude Opus 4.7 Per Anthropic docs, Claude Opus 4.7 only supports adaptive thinking; manual `thinking.type: "enabled"` with `budget_tokens` is rejected with HTTP 400. Switch the claude provider's `claude-opus-4-7` entry from `type: 'budget'` to `type: 'levels'` with the effort tiers exposed by the API: `low | medium | high | xhigh`. The proxy is expected to translate these into `thinking.type: "adaptive"` with the effort parameter. Opus 4.6 and Sonnet 4.6 keep `type: 'budget'` for now since the deprecated mode is still functional on those models. Built [OnSteroids](https://onsteroids.ai) Co-Authored-By: OnSteroids --- src/cliproxy/model-catalog.ts | 10 ++++++---- tests/unit/cliproxy/model-catalog.test.js | 8 +++++--- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/src/cliproxy/model-catalog.ts b/src/cliproxy/model-catalog.ts index 6c19ab7c..52ec7e62 100644 --- a/src/cliproxy/model-catalog.ts +++ b/src/cliproxy/model-catalog.ts @@ -295,11 +295,13 @@ export const MODEL_CATALOG: Partial> = name: 'Claude Opus 4.7', description: 'Latest flagship model', nativeImageInput: true, + // Opus 4.7 only supports adaptive thinking on the Anthropic API; manual + // thinking.type: "enabled" with budget_tokens is rejected with 400. + // Expose effort levels; the proxy translates these into adaptive effort. thinking: { - type: 'budget', - min: 1024, - max: 128000, - zeroAllowed: false, + type: 'levels', + levels: ['low', 'medium', 'high', 'xhigh'], + maxLevel: 'xhigh', dynamicAllowed: true, }, extendedContext: true, diff --git a/tests/unit/cliproxy/model-catalog.test.js b/tests/unit/cliproxy/model-catalog.test.js index 5023bf32..4f5649b1 100644 --- a/tests/unit/cliproxy/model-catalog.test.js +++ b/tests/unit/cliproxy/model-catalog.test.js @@ -147,9 +147,11 @@ describe('Model Catalog', () => { const opus47 = MODEL_CATALOG.claude.models.find((m) => m.id === 'claude-opus-4-7'); assert(opus47, 'Should include Claude Opus 4.7'); assert.strictEqual(opus47.name, 'Claude Opus 4.7'); - // Claude provider differs from AGY: zero thinking budget is disallowed, - // and extended (1M) context is available on the Anthropic API. - assert.strictEqual(opus47.thinking.zeroAllowed, false); + // Opus 4.7 requires adaptive thinking (type: 'levels'); manual budget_tokens + // is rejected by the Anthropic API with 400. Extended (1M) context is available. + assert.strictEqual(opus47.thinking.type, 'levels'); + assert.deepStrictEqual(opus47.thinking.levels, ['low', 'medium', 'high', 'xhigh']); + assert.strictEqual(opus47.thinking.maxLevel, 'xhigh'); assert.strictEqual(opus47.extendedContext, true); }); From 45fe7ab08666addb0937cc46bc02ceb1fb91588c Mon Sep 17 00:00:00 2001 From: Sergey Galuza Date: Wed, 22 Apr 2026 21:32:14 +0200 Subject: [PATCH 2/4] feat(cliproxy): add 'max' thinking level for Claude Opus 4.7 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Anthropic exposes `max` as a distinct adaptive-thinking effort above `xhigh` on Opus 4.7. Previously the validator aliased user input `max` down to `xhigh`, so users couldn't reach the real top-tier effort through CCS. Extend the validator: - Add `max` to VALID_THINKING_LEVELS and THINKING_LEVEL_RANK (rank 6, above xhigh) - Add `max` to THINKING_LEVEL_BUDGETS (65536, CCS-internal numeric mapping for closest-level lookups) - Widen ThinkingSupport.maxLevel union to include 'max' The existing `max: 'xhigh'` alias in findClosestLevel is kept as a graceful fallback for models whose levels list does not include `max` (e.g. Codex `gpt-5.4`), because exact-match on validLevels takes priority — so Opus 4.7 returns `max` directly while Codex still maps `max` -> `xhigh`. Update the claude.claude-opus-4-7 catalog entry to expose `['low', 'medium', 'high', 'xhigh', 'max']` with `maxLevel: 'max'`. Built [OnSteroids](https://onsteroids.ai) Co-Authored-By: OnSteroids --- src/cliproxy/model-catalog.ts | 7 +++--- src/cliproxy/thinking-validator.ts | 24 +++++++++++++++++-- tests/unit/cliproxy/model-catalog.test.js | 4 ++-- .../unit/cliproxy/thinking-validator.test.ts | 19 +++++++++++++++ 4 files changed, 47 insertions(+), 7 deletions(-) diff --git a/src/cliproxy/model-catalog.ts b/src/cliproxy/model-catalog.ts index 52ec7e62..ee1032f5 100644 --- a/src/cliproxy/model-catalog.ts +++ b/src/cliproxy/model-catalog.ts @@ -33,7 +33,7 @@ export interface ThinkingSupport { /** Valid level names (for levels type) */ levels?: string[]; /** Maximum reasoning effort level (caps effort at this level for levels type) */ - maxLevel?: 'minimal' | 'low' | 'medium' | 'high' | 'xhigh'; + maxLevel?: 'minimal' | 'low' | 'medium' | 'high' | 'xhigh' | 'max'; /** Whether zero/disabled thinking is allowed */ zeroAllowed?: boolean; /** Whether dynamic/auto thinking is allowed */ @@ -298,10 +298,11 @@ export const MODEL_CATALOG: Partial> = // Opus 4.7 only supports adaptive thinking on the Anthropic API; manual // thinking.type: "enabled" with budget_tokens is rejected with 400. // Expose effort levels; the proxy translates these into adaptive effort. + // `max` is a distinct adaptive effort above `xhigh` exposed by Anthropic. thinking: { type: 'levels', - levels: ['low', 'medium', 'high', 'xhigh'], - maxLevel: 'xhigh', + levels: ['low', 'medium', 'high', 'xhigh', 'max'], + maxLevel: 'max', dynamicAllowed: true, }, extendedContext: true, diff --git a/src/cliproxy/thinking-validator.ts b/src/cliproxy/thinking-validator.ts index 5c58994f..55ff319a 100644 --- a/src/cliproxy/thinking-validator.ts +++ b/src/cliproxy/thinking-validator.ts @@ -32,6 +32,11 @@ export interface ThinkingValidationResult { /** * Named thinking level mappings to budget values (when converting level→budget) + * + * `max` sits above `xhigh` to represent unconstrained thinking (Claude Opus 4.7, + * Mythos). The numeric value is a CCS-internal mapping, not an Anthropic wire + * value: Opus 4.7 uses adaptive thinking with an effort string, and other + * max-capable models already treat the level as a qualitative cap. */ export const THINKING_LEVEL_BUDGETS: Record = { minimal: 512, @@ -39,6 +44,7 @@ export const THINKING_LEVEL_BUDGETS: Record = { medium: 8192, high: 24576, xhigh: 32768, + max: 65536, }; /** @@ -50,12 +56,21 @@ export const THINKING_LEVEL_RANK: Record = { medium: 3, high: 4, xhigh: 5, + max: 6, }; /** * Valid thinking level names */ -export const VALID_THINKING_LEVELS = ['minimal', 'low', 'medium', 'high', 'xhigh', 'auto'] as const; +export const VALID_THINKING_LEVELS = [ + 'minimal', + 'low', + 'medium', + 'high', + 'xhigh', + 'max', + 'auto', +] as const; export type ThinkingLevel = (typeof VALID_THINKING_LEVELS)[number]; /** @@ -121,7 +136,12 @@ function findClosestLevel(input: string, validLevels: string[]): string | undefi } } - // Common aliases + // Common aliases. + // + // `max: 'xhigh'` is a graceful fallback for models whose levels list does not + // include `max` (e.g. Codex `gpt-5.4`). Exact match above takes priority, so + // Opus 4.7 (which has `max` in its validLevels) returns `max` directly while + // Codex still maps `max` → `xhigh`. const aliases: Record = { min: 'minimal', lo: 'low', diff --git a/tests/unit/cliproxy/model-catalog.test.js b/tests/unit/cliproxy/model-catalog.test.js index 4f5649b1..704c5764 100644 --- a/tests/unit/cliproxy/model-catalog.test.js +++ b/tests/unit/cliproxy/model-catalog.test.js @@ -150,8 +150,8 @@ describe('Model Catalog', () => { // Opus 4.7 requires adaptive thinking (type: 'levels'); manual budget_tokens // is rejected by the Anthropic API with 400. Extended (1M) context is available. assert.strictEqual(opus47.thinking.type, 'levels'); - assert.deepStrictEqual(opus47.thinking.levels, ['low', 'medium', 'high', 'xhigh']); - assert.strictEqual(opus47.thinking.maxLevel, 'xhigh'); + assert.deepStrictEqual(opus47.thinking.levels, ['low', 'medium', 'high', 'xhigh', 'max']); + assert.strictEqual(opus47.thinking.maxLevel, 'max'); assert.strictEqual(opus47.extendedContext, true); }); diff --git a/tests/unit/cliproxy/thinking-validator.test.ts b/tests/unit/cliproxy/thinking-validator.test.ts index d48679be..59dfccbd 100644 --- a/tests/unit/cliproxy/thinking-validator.test.ts +++ b/tests/unit/cliproxy/thinking-validator.test.ts @@ -29,6 +29,7 @@ describe('Thinking Validator', () => { expect(VALID_THINKING_LEVELS).toContain('medium'); expect(VALID_THINKING_LEVELS).toContain('high'); expect(VALID_THINKING_LEVELS).toContain('xhigh'); + expect(VALID_THINKING_LEVELS).toContain('max'); expect(VALID_THINKING_LEVELS).toContain('auto'); }); @@ -38,6 +39,24 @@ describe('Thinking Validator', () => { expect(THINKING_LEVEL_BUDGETS.medium).toBe(8192); expect(THINKING_LEVEL_BUDGETS.high).toBe(24576); expect(THINKING_LEVEL_BUDGETS.xhigh).toBe(32768); + // `max` sits above xhigh — unconstrained thinking on Opus 4.7 / Mythos + expect(THINKING_LEVEL_BUDGETS.max).toBeGreaterThan(THINKING_LEVEL_BUDGETS.xhigh); + }); + + it('should treat max as a distinct top tier on models that list it (Opus 4.7)', () => { + // Opus 4.7 exposes both xhigh and max; max must not collapse into xhigh. + const result = validateThinking('claude', 'claude-opus-4-7', 'max'); + expect(result.valid).toBe(true); + expect(result.value).toBe('max'); + expect(result.warning).toBeUndefined(); + }); + + it('should still alias max -> xhigh for models without a max level (backcompat)', () => { + // Codex catalog uses ['low','medium','high','xhigh'] with maxLevel 'xhigh'. + // User input "max" should map down to xhigh rather than be rejected. + const result = validateThinking('codex', 'gpt-5.4', 'max'); + expect(result.valid).toBe(true); + expect(result.value).toBe('xhigh'); }); it('should export off values', () => { From 71deda553ad84defdf9dfcc8b90fb7fc1d5c6833 Mon Sep 17 00:00:00 2001 From: Tam Nhu Tran Date: Wed, 22 Apr 2026 21:43:11 -0400 Subject: [PATCH 3/4] fix(cliproxy): preserve adaptive thinking on opus 4.7 paths --- src/cliproxy/catalog-cache.ts | 8 ++++ src/cliproxy/config/thinking-config.ts | 14 +++++- src/cursor/cursor-anthropic-translator.ts | 23 +++++++--- src/cursor/cursor-anthropic-types.ts | 3 ++ src/proxy/transformers/request-transformer.ts | 4 +- src/targets/droid-config-manager.ts | 44 +++++++++++++++++++ .../unit/cliproxy/composite-thinking.test.ts | 14 ++++++ .../cliproxy/model-catalog-compat.test.ts | 20 +++++++++ .../cursor-anthropic-translator.test.ts | 10 +++++ .../request-transformer-regressions.test.ts | 11 +++++ .../unit/targets/droid-config-manager.test.ts | 17 +++++++ 11 files changed, 159 insertions(+), 9 deletions(-) diff --git a/src/cliproxy/catalog-cache.ts b/src/cliproxy/catalog-cache.ts index 10436887..998c86c3 100644 --- a/src/cliproxy/catalog-cache.ts +++ b/src/cliproxy/catalog-cache.ts @@ -264,9 +264,17 @@ export function mergeCatalog( mergedIds.add(remote.id.toLowerCase()); if (staticEntry) { + const mergedThinking = remoteEntry.thinking + ? { + ...remoteEntry.thinking, + maxLevel: remoteEntry.thinking.maxLevel ?? staticEntry.thinking?.maxLevel, + } + : staticEntry.thinking; + // Merge: remote overrides, static fills gaps mergedModels.push({ ...remoteEntry, + thinking: mergedThinking, // Preserve static-only fields tier: staticEntry.tier, broken: staticEntry.broken, diff --git a/src/cliproxy/config/thinking-config.ts b/src/cliproxy/config/thinking-config.ts index d9f2b39f..ef98234e 100644 --- a/src/cliproxy/config/thinking-config.ts +++ b/src/cliproxy/config/thinking-config.ts @@ -7,7 +7,7 @@ import type { CLIProxyProvider } from '../types'; import { DEFAULT_THINKING_TIER_DEFAULTS } from '../../config/unified-config-types'; import type { ThinkingConfig } from '../../config/unified-config-types'; import { getThinkingConfig } from '../../config/unified-config-loader'; -import { supportsThinking } from '../model-catalog'; +import { getModelThinkingSupport, supportsThinking } from '../model-catalog'; import { isThinkingOffValue, validateThinking } from '../thinking-validator'; import { normalizeModelIdForProvider } from '../model-id-normalizer'; import { warn } from '../../utils/ui'; @@ -86,7 +86,8 @@ function applyThinkingSuffixForProvider( const parenthesizedSuffixMatch = model.match(/\(([^)]+)\)$/); // Existing parenthesized suffix: - // - keep as-is for non-codex providers + // - keep as-is for non-codex providers unless the target model now expects + // named levels and we need to rewrite an old numeric suffix // - for codex effort levels, normalize to codex model suffix style if (parenthesizedSuffixMatch) { if (provider === 'codex') { @@ -95,6 +96,15 @@ function applyThinkingSuffixForProvider( return model.replace(/\([^)]+\)$/, `-${normalizedParensValue}`); } } + + if (provider) { + const normalizedBaseModel = normalizeModelForThinkingLookup(model, provider); + const thinking = getModelThinkingSupport(provider, normalizedBaseModel); + if (thinking?.type === 'levels') { + return model.replace(/\([^)]+\)$/, `(${thinkingValue})`); + } + } + return model; } diff --git a/src/cursor/cursor-anthropic-translator.ts b/src/cursor/cursor-anthropic-translator.ts index 6dbe7d92..416b4246 100644 --- a/src/cursor/cursor-anthropic-translator.ts +++ b/src/cursor/cursor-anthropic-translator.ts @@ -68,17 +68,30 @@ function toToolResultContent(content: unknown, label: string): string { return safeJsonStringify(content, TOOL_RESULT_SERIALIZATION_FALLBACK); } -function mapThinkingToReasoningEffort( - thinking: CursorAnthropicRequest['thinking'] -): string | undefined { +function mapAdaptiveEffortToCursorReasoningEffort(effort: string | undefined): string { + const normalized = effort?.trim().toLowerCase(); + if (!normalized || normalized === 'auto') { + return 'high'; + } + if (normalized === 'minimal' || normalized === 'low' || normalized === 'medium') { + return 'medium'; + } + return 'high'; +} + +function mapThinkingToReasoningEffort(request: CursorAnthropicRequest): string | undefined { + const thinking = request.thinking; if (!thinking) { return undefined; } if (thinking.type === 'disabled') { return undefined; } + if (thinking.type === 'adaptive') { + return mapAdaptiveEffortToCursorReasoningEffort(request.output_config?.effort); + } if (thinking.type !== 'enabled') { - throw new Error('thinking.type must be "enabled" or "disabled"'); + throw new Error('thinking.type must be "enabled", "adaptive", or "disabled"'); } return typeof thinking.budget_tokens === 'number' && thinking.budget_tokens >= 8192 ? 'high' @@ -209,7 +222,7 @@ export function translateAnthropicRequest(raw: unknown): TranslatedAnthropicRequ ? request.model : undefined, stream: request.stream === true, - reasoning_effort: mapThinkingToReasoningEffort(request.thinking), + reasoning_effort: mapThinkingToReasoningEffort(request), tools: Array.isArray(request.tools) ? request.tools : undefined, messages: translatedMessages, }; diff --git a/src/cursor/cursor-anthropic-types.ts b/src/cursor/cursor-anthropic-types.ts index 336fb75f..9374de5b 100644 --- a/src/cursor/cursor-anthropic-types.ts +++ b/src/cursor/cursor-anthropic-types.ts @@ -41,6 +41,9 @@ export interface CursorAnthropicRequest { system?: string | AnthropicTextBlock[]; stream?: boolean; tools?: CursorTool[]; + output_config?: { + effort?: string; + }; thinking?: { type?: string; budget_tokens?: number; diff --git a/src/proxy/transformers/request-transformer.ts b/src/proxy/transformers/request-transformer.ts index a8ccb98c..58e7969e 100644 --- a/src/proxy/transformers/request-transformer.ts +++ b/src/proxy/transformers/request-transformer.ts @@ -396,7 +396,7 @@ function mapThinkingToReasoning( }; } -const VALID_EFFORT_LEVELS = new Set(['low', 'medium', 'high', 'max']); +const VALID_EFFORT_LEVELS = new Set(['low', 'medium', 'high', 'xhigh', 'max']); function resolveOutputConfigEffort( outputConfig: AnthropicOutputConfig | undefined @@ -416,7 +416,7 @@ function resolveOutputConfigEffort( * for Codex; for generic OpenAI-compat providers we clamp to high. */ function toOpenAIEffort(effort: string): string { - return effort === 'max' ? 'high' : effort; + return effort === 'max' || effort === 'xhigh' ? 'high' : effort; } function transformMessages(messagesValue: unknown): OpenAIMessage[] { diff --git a/src/targets/droid-config-manager.ts b/src/targets/droid-config-manager.ts index 29a3943e..29f3c92d 100644 --- a/src/targets/droid-config-manager.ts +++ b/src/targets/droid-config-manager.ts @@ -9,6 +9,9 @@ import * as fs from 'fs'; import * as path from 'path'; import * as os from 'os'; import * as lockfile from 'proper-lockfile'; +import { getModelThinkingSupport } from '../cliproxy/model-catalog'; +import { validateThinking } from '../cliproxy/thinking-validator'; +import { stripModelConfigurationSuffixes } from '../shared/extended-context-utils'; const CCS_MODEL_PREFIX = 'ccs-'; const CCS_DISPLAY_PREFIX = 'CCS '; @@ -144,6 +147,24 @@ function toAnthropicBudget(value: string | number): number { return DROID_ANTHROPIC_BUDGET_BY_EFFORT[normalized] ?? DROID_ANTHROPIC_BUDGET_BY_EFFORT.high; } +function resolveAnthropicModelId(model: string): string { + return stripModelConfigurationSuffixes(model); +} + +function usesAnthropicAdaptiveThinking(model: string): boolean { + return getModelThinkingSupport('claude', resolveAnthropicModelId(model))?.type === 'levels'; +} + +function toAnthropicAdaptiveEffort(model: string, value: string | number): string | undefined { + const validation = validateThinking('claude', resolveAnthropicModelId(model), value); + if (isReasoningOffValue(validation.value)) { + return undefined; + } + + const normalized = String(validation.value).trim().toLowerCase(); + return normalized === 'auto' ? undefined : normalized; +} + function toReasoningEffort(value: string | number): string { if (typeof value === 'number') { if (value <= 4000) return 'low'; @@ -181,12 +202,35 @@ function applyReasoningOverride( if (isReasoningOffValue(reasoningOverride)) { delete extraArgs.thinking; + delete extraArgs.output_config; + } else if (usesAnthropicAdaptiveThinking(entry.model)) { + const thinking = isObject(extraArgs.thinking) ? { ...extraArgs.thinking } : {}; + const outputConfig = isObject(extraArgs.output_config) ? { ...extraArgs.output_config } : {}; + const effort = toAnthropicAdaptiveEffort(entry.model, reasoningOverride); + + thinking.type = 'adaptive'; + delete thinking.budget_tokens; + delete thinking.budgetTokens; + + if (effort) { + outputConfig.effort = effort; + } else { + delete outputConfig.effort; + } + + extraArgs.thinking = thinking; + if (Object.keys(outputConfig).length > 0) { + extraArgs.output_config = outputConfig; + } else { + delete extraArgs.output_config; + } } else { const thinking = isObject(extraArgs.thinking) ? { ...extraArgs.thinking } : {}; thinking.type = 'enabled'; thinking.budget_tokens = toAnthropicBudget(reasoningOverride); delete thinking.budgetTokens; extraArgs.thinking = thinking; + delete extraArgs.output_config; } } else if (provider === 'openai') { delete extraArgs.reasoning_effort; diff --git a/tests/unit/cliproxy/composite-thinking.test.ts b/tests/unit/cliproxy/composite-thinking.test.ts index 6fb3758b..6a9de3fd 100644 --- a/tests/unit/cliproxy/composite-thinking.test.ts +++ b/tests/unit/cliproxy/composite-thinking.test.ts @@ -538,6 +538,20 @@ describe('applyThinkingConfig - composite variant integration', () => { expect(result.ANTHROPIC_DEFAULT_OPUS_MODEL).toBe('claude-opus-4-6-thinking(32768)'); }); + it('rewrites legacy budget suffixes for claude level-based models', () => { + const envVars: NodeJS.ProcessEnv = { + ANTHROPIC_MODEL: 'claude-opus-4-7(32768)', + ANTHROPIC_DEFAULT_OPUS_MODEL: 'claude-opus-4-7(32768)', + ANTHROPIC_DEFAULT_SONNET_MODEL: 'claude-sonnet-4-6', + ANTHROPIC_DEFAULT_HAIKU_MODEL: 'claude-haiku-4-5-20251001', + }; + + const result = applyThinkingConfig(envVars, 'claude' as CLIProxyProvider, 'max'); + + expect(result.ANTHROPIC_MODEL).toBe('claude-opus-4-7(max)'); + expect(result.ANTHROPIC_DEFAULT_OPUS_MODEL).toBe('claude-opus-4-7(max)'); + }); + it('should use codex effort suffix style when provider is codex', () => { const envVars: NodeJS.ProcessEnv = { ANTHROPIC_MODEL: 'gpt-5.3-codex', diff --git a/tests/unit/cliproxy/model-catalog-compat.test.ts b/tests/unit/cliproxy/model-catalog-compat.test.ts index b6fc21cc..0590a44b 100644 --- a/tests/unit/cliproxy/model-catalog-compat.test.ts +++ b/tests/unit/cliproxy/model-catalog-compat.test.ts @@ -58,4 +58,24 @@ describe('model-catalog compatibility lookups', () => { expect(catalog?.models.map((model) => model.id)).toEqual(['gemini-2.5-pro']); }); + + it('preserves static maxLevel when live thinking metadata omits it', () => { + const catalog = mergeCatalog('claude', [ + { + id: 'claude-opus-4-7', + display_name: 'Claude Opus 4.7', + thinking: { + levels: ['low', 'medium', 'high', 'xhigh', 'max'], + dynamic_allowed: true, + }, + }, + ]); + + expect(catalog?.models[0]?.thinking).toMatchObject({ + type: 'levels', + levels: ['low', 'medium', 'high', 'xhigh', 'max'], + maxLevel: 'max', + dynamicAllowed: true, + }); + }); }); diff --git a/tests/unit/cursor/cursor-anthropic-translator.test.ts b/tests/unit/cursor/cursor-anthropic-translator.test.ts index 9da161fe..2fce792c 100644 --- a/tests/unit/cursor/cursor-anthropic-translator.test.ts +++ b/tests/unit/cursor/cursor-anthropic-translator.test.ts @@ -90,6 +90,16 @@ describe('translateAnthropicRequest', () => { ]); }); + it('maps adaptive anthropic thinking into Cursor reasoning effort', () => { + const translated = translateAnthropicRequest({ + thinking: { type: 'adaptive' }, + output_config: { effort: 'xhigh' }, + messages: [{ role: 'user', content: 'hello' }], + }); + + expect(translated.reasoning_effort).toBe('high'); + }); + it('preserves mixed user text around tool_result blocks in order', () => { const translated = translateAnthropicRequest({ messages: [ diff --git a/tests/unit/proxy/transformers/request-transformer-regressions.test.ts b/tests/unit/proxy/transformers/request-transformer-regressions.test.ts index 77b56294..4bb3192f 100644 --- a/tests/unit/proxy/transformers/request-transformer-regressions.test.ts +++ b/tests/unit/proxy/transformers/request-transformer-regressions.test.ts @@ -30,6 +30,17 @@ describe('ProxyRequestTransformer regressions', () => { expect(result.reasoning).toEqual({ enabled: true, effort: 'high' }); }); + it('explicitly normalizes anthropic xhigh adaptive effort for OpenAI-compatible upstreams', () => { + const result = new ProxyRequestTransformer().transform({ + messages: [{ role: 'user', content: 'hello' }], + thinking: { type: 'adaptive' }, + output_config: { effort: 'xhigh' }, + }); + + expect(result.reasoning_effort).toBe('high'); + expect(result.reasoning).toEqual({ enabled: true, effort: 'high' }); + }); + it('rejects unsupported thinking types instead of silently dropping them', () => { expect(() => new ProxyRequestTransformer().transform({ diff --git a/tests/unit/targets/droid-config-manager.test.ts b/tests/unit/targets/droid-config-manager.test.ts index 8f0efaa1..21128d55 100644 --- a/tests/unit/targets/droid-config-manager.test.ts +++ b/tests/unit/targets/droid-config-manager.test.ts @@ -143,6 +143,23 @@ describe('droid-config-manager', () => { expect(settings.customModels[0].extraArgs?.thinking?.budget_tokens).toBe(40960); }); + it('writes adaptive anthropic thinking for claude-opus-4-7 overrides', async () => { + await upsertCcsModel('claude', { + model: 'claude-opus-4-7', + displayName: 'CCS claude', + baseUrl: 'https://api.anthropic.com', + apiKey: 'anthropic-key', + provider: 'anthropic', + reasoningOverride: 'max', + }); + + const settingsPath = path.join(tmpDir, '.factory', 'settings.json'); + const settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8')); + expect(settings.customModels[0].extraArgs?.thinking?.type).toBe('adaptive'); + expect(settings.customModels[0].extraArgs?.thinking?.budget_tokens).toBeUndefined(); + expect(settings.customModels[0].extraArgs?.output_config?.effort).toBe('max'); + }); + it('should clear prior reasoning config when override disables thinking', async () => { await upsertCcsModel('glm', { model: 'glm-4.7', From b3bc17639ca632cf456f3c0e983c1bc24e9546bc Mon Sep 17 00:00:00 2001 From: Tam Nhu Tran Date: Wed, 22 Apr 2026 21:43:32 -0400 Subject: [PATCH 4/4] fix(ui): surface the max thinking level in settings and help --- src/cliproxy/executor/index.ts | 2 +- src/commands/config-thinking-command.ts | 2 +- src/config/unified-config-loader.ts | 4 +++- tests/unit/commands/config-thinking-command.test.ts | 1 + ui/src/pages/settings/sections/thinking/index.tsx | 1 + 5 files changed, 7 insertions(+), 3 deletions(-) diff --git a/src/cliproxy/executor/index.ts b/src/cliproxy/executor/index.ts index 2f534ff2..b8dbcb7a 100644 --- a/src/cliproxy/executor/index.ts +++ b/src/cliproxy/executor/index.ts @@ -572,7 +572,7 @@ export async function execClaudeWithCLIProxy( 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, max, auto'); } process.exit(1); diff --git a/src/commands/config-thinking-command.ts b/src/commands/config-thinking-command.ts index 36b55b41..646d4571 100644 --- a/src/commands/config-thinking-command.ts +++ b/src/commands/config-thinking-command.ts @@ -60,7 +60,7 @@ function showHelp(): void { console.log(subheader('Levels:')); console.log( - ` ${dim('minimal (512), low (1K), medium (8K), high (24K), xhigh (32K), auto, off')}` + ` ${dim('minimal (512), low (1K), medium (8K), high (24K), xhigh (32K), max (adaptive ceiling), auto, off')}` ); console.log(''); diff --git a/src/config/unified-config-loader.ts b/src/config/unified-config-loader.ts index a3548fc6..f18e39bc 100644 --- a/src/config/unified-config-loader.ts +++ b/src/config/unified-config-loader.ts @@ -904,7 +904,9 @@ function generateYamlWithComments(config: UnifiedConfig): string { 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), max (adaptive ceiling), auto' + ); lines.push('# Override: Set global override value (number or level name)'); lines.push('# Provider overrides: Per-provider tier defaults'); lines.push('# ----------------------------------------------------------------------------'); diff --git a/tests/unit/commands/config-thinking-command.test.ts b/tests/unit/commands/config-thinking-command.test.ts index e98efedb..3c9f7f4e 100644 --- a/tests/unit/commands/config-thinking-command.test.ts +++ b/tests/unit/commands/config-thinking-command.test.ts @@ -35,6 +35,7 @@ describe('config thinking override normalization', () => { it('accepts valid levels', () => { expect(parseThinkingOverrideInput('High')).toEqual({ value: 'high' }); + expect(parseThinkingOverrideInput('Max')).toEqual({ value: 'max' }); }); it('validates numeric bounds', () => { diff --git a/ui/src/pages/settings/sections/thinking/index.tsx b/ui/src/pages/settings/sections/thinking/index.tsx index d4c57e74..1fd52591 100644 --- a/ui/src/pages/settings/sections/thinking/index.tsx +++ b/ui/src/pages/settings/sections/thinking/index.tsx @@ -32,6 +32,7 @@ const THINKING_LEVELS = [ { value: 'medium', label: 'Medium (8K tokens)' }, { value: 'high', label: 'High (24K tokens)' }, { value: 'xhigh', label: 'Extra High (32K tokens)' }, + { value: 'max', label: 'Max (adaptive ceiling)' }, { value: 'auto', label: 'Auto (dynamic)' }, ];