Merge pull request #1069 from sgaluza/feat/opus-4-7-adaptive-thinking

fix(cliproxy): use adaptive thinking for Opus 4.7 + add 'max' level
This commit is contained in:
Kai (Tam Nhu) Tran
2026-04-22 22:03:21 -04:00
committed by GitHub
20 changed files with 220 additions and 22 deletions
+8
View File
@@ -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,
+12 -2
View File
@@ -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;
}
+1 -1
View File
@@ -574,7 +574,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);
+8 -5
View File
@@ -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 */
@@ -295,11 +295,14 @@ export const MODEL_CATALOG: Partial<Record<CLIProxyProvider, ProviderCatalog>> =
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.
// `max` is a distinct adaptive effort above `xhigh` exposed by Anthropic.
thinking: {
type: 'budget',
min: 1024,
max: 128000,
zeroAllowed: false,
type: 'levels',
levels: ['low', 'medium', 'high', 'xhigh', 'max'],
maxLevel: 'max',
dynamicAllowed: true,
},
extendedContext: true,
+22 -2
View File
@@ -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<string, number> = {
minimal: 512,
@@ -39,6 +44,7 @@ export const THINKING_LEVEL_BUDGETS: Record<string, number> = {
medium: 8192,
high: 24576,
xhigh: 32768,
max: 65536,
};
/**
@@ -50,12 +56,21 @@ export const THINKING_LEVEL_RANK: Record<string, number> = {
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<string, string> = {
min: 'minimal',
lo: 'low',
+1 -1
View File
@@ -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('');
+3 -1
View File
@@ -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('# ----------------------------------------------------------------------------');
+18 -5
View File
@@ -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,
};
+3
View File
@@ -41,6 +41,9 @@ export interface CursorAnthropicRequest {
system?: string | AnthropicTextBlock[];
stream?: boolean;
tools?: CursorTool[];
output_config?: {
effort?: string;
};
thinking?: {
type?: string;
budget_tokens?: number;
@@ -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[] {
+44
View File
@@ -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;