mirror of
https://github.com/tiennm99/ccs.git
synced 2026-10-19 04:15:45 +00:00
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:
20 files changed
+220
-22
No files matched your search
@@ -264,9 +264,17 @@ export function mergeCatalog(
|
|||||||
mergedIds.add(remote.id.toLowerCase());
|
mergedIds.add(remote.id.toLowerCase());
|
||||||
|
|
||||||
if (staticEntry) {
|
if (staticEntry) {
|
||||||
|
const mergedThinking = remoteEntry.thinking
|
||||||
|
? {
|
||||||
|
...remoteEntry.thinking,
|
||||||
|
maxLevel: remoteEntry.thinking.maxLevel ?? staticEntry.thinking?.maxLevel,
|
||||||
|
}
|
||||||
|
: staticEntry.thinking;
|
||||||
|
|
||||||
// Merge: remote overrides, static fills gaps
|
// Merge: remote overrides, static fills gaps
|
||||||
mergedModels.push({
|
mergedModels.push({
|
||||||
...remoteEntry,
|
...remoteEntry,
|
||||||
|
thinking: mergedThinking,
|
||||||
// Preserve static-only fields
|
// Preserve static-only fields
|
||||||
tier: staticEntry.tier,
|
tier: staticEntry.tier,
|
||||||
broken: staticEntry.broken,
|
broken: staticEntry.broken,
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import type { CLIProxyProvider } from '../types';
|
|||||||
import { DEFAULT_THINKING_TIER_DEFAULTS } from '../../config/unified-config-types';
|
import { DEFAULT_THINKING_TIER_DEFAULTS } from '../../config/unified-config-types';
|
||||||
import type { ThinkingConfig } from '../../config/unified-config-types';
|
import type { ThinkingConfig } from '../../config/unified-config-types';
|
||||||
import { getThinkingConfig } from '../../config/unified-config-loader';
|
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 { isThinkingOffValue, validateThinking } from '../thinking-validator';
|
||||||
import { normalizeModelIdForProvider } from '../model-id-normalizer';
|
import { normalizeModelIdForProvider } from '../model-id-normalizer';
|
||||||
import { warn } from '../../utils/ui';
|
import { warn } from '../../utils/ui';
|
||||||
@@ -86,7 +86,8 @@ function applyThinkingSuffixForProvider(
|
|||||||
const parenthesizedSuffixMatch = model.match(/\(([^)]+)\)$/);
|
const parenthesizedSuffixMatch = model.match(/\(([^)]+)\)$/);
|
||||||
|
|
||||||
// Existing parenthesized suffix:
|
// 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
|
// - for codex effort levels, normalize to codex model suffix style
|
||||||
if (parenthesizedSuffixMatch) {
|
if (parenthesizedSuffixMatch) {
|
||||||
if (provider === 'codex') {
|
if (provider === 'codex') {
|
||||||
@@ -95,6 +96,15 @@ function applyThinkingSuffixForProvider(
|
|||||||
return model.replace(/\([^)]+\)$/, `-${normalizedParensValue}`);
|
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;
|
return model;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -574,7 +574,7 @@ export async function execClaudeWithCLIProxy(
|
|||||||
console.error(' Alias: --thinking xhigh (same behavior)');
|
console.error(' Alias: --thinking xhigh (same behavior)');
|
||||||
} else {
|
} else {
|
||||||
console.error(' Examples: --thinking low, --thinking 8192, --thinking off');
|
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);
|
process.exit(1);
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ export interface ThinkingSupport {
|
|||||||
/** Valid level names (for levels type) */
|
/** Valid level names (for levels type) */
|
||||||
levels?: string[];
|
levels?: string[];
|
||||||
/** Maximum reasoning effort level (caps effort at this level for levels type) */
|
/** 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 */
|
/** Whether zero/disabled thinking is allowed */
|
||||||
zeroAllowed?: boolean;
|
zeroAllowed?: boolean;
|
||||||
/** Whether dynamic/auto thinking is allowed */
|
/** Whether dynamic/auto thinking is allowed */
|
||||||
@@ -295,11 +295,14 @@ export const MODEL_CATALOG: Partial<Record<CLIProxyProvider, ProviderCatalog>> =
|
|||||||
name: 'Claude Opus 4.7',
|
name: 'Claude Opus 4.7',
|
||||||
description: 'Latest flagship model',
|
description: 'Latest flagship model',
|
||||||
nativeImageInput: true,
|
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: {
|
thinking: {
|
||||||
type: 'budget',
|
type: 'levels',
|
||||||
min: 1024,
|
levels: ['low', 'medium', 'high', 'xhigh', 'max'],
|
||||||
max: 128000,
|
maxLevel: 'max',
|
||||||
zeroAllowed: false,
|
|
||||||
dynamicAllowed: true,
|
dynamicAllowed: true,
|
||||||
},
|
},
|
||||||
extendedContext: true,
|
extendedContext: true,
|
||||||
|
|||||||
@@ -32,6 +32,11 @@ export interface ThinkingValidationResult {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Named thinking level mappings to budget values (when converting level→budget)
|
* 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> = {
|
export const THINKING_LEVEL_BUDGETS: Record<string, number> = {
|
||||||
minimal: 512,
|
minimal: 512,
|
||||||
@@ -39,6 +44,7 @@ export const THINKING_LEVEL_BUDGETS: Record<string, number> = {
|
|||||||
medium: 8192,
|
medium: 8192,
|
||||||
high: 24576,
|
high: 24576,
|
||||||
xhigh: 32768,
|
xhigh: 32768,
|
||||||
|
max: 65536,
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -50,12 +56,21 @@ export const THINKING_LEVEL_RANK: Record<string, number> = {
|
|||||||
medium: 3,
|
medium: 3,
|
||||||
high: 4,
|
high: 4,
|
||||||
xhigh: 5,
|
xhigh: 5,
|
||||||
|
max: 6,
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Valid thinking level names
|
* 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];
|
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> = {
|
const aliases: Record<string, string> = {
|
||||||
min: 'minimal',
|
min: 'minimal',
|
||||||
lo: 'low',
|
lo: 'low',
|
||||||
|
|||||||
@@ -60,7 +60,7 @@ function showHelp(): void {
|
|||||||
|
|
||||||
console.log(subheader('Levels:'));
|
console.log(subheader('Levels:'));
|
||||||
console.log(
|
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('');
|
console.log('');
|
||||||
|
|
||||||
|
|||||||
@@ -904,7 +904,9 @@ function generateYamlWithComments(config: UnifiedConfig): string {
|
|||||||
lines.push(
|
lines.push(
|
||||||
'# Modes: auto (use tier_defaults), off (disable), manual (--thinking/--effort flags)'
|
'# 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('# Override: Set global override value (number or level name)');
|
||||||
lines.push('# Provider overrides: Per-provider tier defaults');
|
lines.push('# Provider overrides: Per-provider tier defaults');
|
||||||
lines.push('# ----------------------------------------------------------------------------');
|
lines.push('# ----------------------------------------------------------------------------');
|
||||||
|
|||||||
@@ -68,17 +68,30 @@ function toToolResultContent(content: unknown, label: string): string {
|
|||||||
return safeJsonStringify(content, TOOL_RESULT_SERIALIZATION_FALLBACK);
|
return safeJsonStringify(content, TOOL_RESULT_SERIALIZATION_FALLBACK);
|
||||||
}
|
}
|
||||||
|
|
||||||
function mapThinkingToReasoningEffort(
|
function mapAdaptiveEffortToCursorReasoningEffort(effort: string | undefined): string {
|
||||||
thinking: CursorAnthropicRequest['thinking']
|
const normalized = effort?.trim().toLowerCase();
|
||||||
): string | undefined {
|
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) {
|
if (!thinking) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
if (thinking.type === 'disabled') {
|
if (thinking.type === 'disabled') {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
if (thinking.type === 'adaptive') {
|
||||||
|
return mapAdaptiveEffortToCursorReasoningEffort(request.output_config?.effort);
|
||||||
|
}
|
||||||
if (thinking.type !== 'enabled') {
|
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
|
return typeof thinking.budget_tokens === 'number' && thinking.budget_tokens >= 8192
|
||||||
? 'high'
|
? 'high'
|
||||||
@@ -209,7 +222,7 @@ export function translateAnthropicRequest(raw: unknown): TranslatedAnthropicRequ
|
|||||||
? request.model
|
? request.model
|
||||||
: undefined,
|
: undefined,
|
||||||
stream: request.stream === true,
|
stream: request.stream === true,
|
||||||
reasoning_effort: mapThinkingToReasoningEffort(request.thinking),
|
reasoning_effort: mapThinkingToReasoningEffort(request),
|
||||||
tools: Array.isArray(request.tools) ? request.tools : undefined,
|
tools: Array.isArray(request.tools) ? request.tools : undefined,
|
||||||
messages: translatedMessages,
|
messages: translatedMessages,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -41,6 +41,9 @@ export interface CursorAnthropicRequest {
|
|||||||
system?: string | AnthropicTextBlock[];
|
system?: string | AnthropicTextBlock[];
|
||||||
stream?: boolean;
|
stream?: boolean;
|
||||||
tools?: CursorTool[];
|
tools?: CursorTool[];
|
||||||
|
output_config?: {
|
||||||
|
effort?: string;
|
||||||
|
};
|
||||||
thinking?: {
|
thinking?: {
|
||||||
type?: string;
|
type?: string;
|
||||||
budget_tokens?: number;
|
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(
|
function resolveOutputConfigEffort(
|
||||||
outputConfig: AnthropicOutputConfig | undefined
|
outputConfig: AnthropicOutputConfig | undefined
|
||||||
@@ -416,7 +416,7 @@ function resolveOutputConfigEffort(
|
|||||||
* for Codex; for generic OpenAI-compat providers we clamp to high.
|
* for Codex; for generic OpenAI-compat providers we clamp to high.
|
||||||
*/
|
*/
|
||||||
function toOpenAIEffort(effort: string): string {
|
function toOpenAIEffort(effort: string): string {
|
||||||
return effort === 'max' ? 'high' : effort;
|
return effort === 'max' || effort === 'xhigh' ? 'high' : effort;
|
||||||
}
|
}
|
||||||
|
|
||||||
function transformMessages(messagesValue: unknown): OpenAIMessage[] {
|
function transformMessages(messagesValue: unknown): OpenAIMessage[] {
|
||||||
|
|||||||
@@ -9,6 +9,9 @@ import * as fs from 'fs';
|
|||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
import * as os from 'os';
|
import * as os from 'os';
|
||||||
import * as lockfile from 'proper-lockfile';
|
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_MODEL_PREFIX = 'ccs-';
|
||||||
const CCS_DISPLAY_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;
|
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 {
|
function toReasoningEffort(value: string | number): string {
|
||||||
if (typeof value === 'number') {
|
if (typeof value === 'number') {
|
||||||
if (value <= 4000) return 'low';
|
if (value <= 4000) return 'low';
|
||||||
@@ -181,12 +202,35 @@ function applyReasoningOverride(
|
|||||||
|
|
||||||
if (isReasoningOffValue(reasoningOverride)) {
|
if (isReasoningOffValue(reasoningOverride)) {
|
||||||
delete extraArgs.thinking;
|
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 {
|
} else {
|
||||||
const thinking = isObject(extraArgs.thinking) ? { ...extraArgs.thinking } : {};
|
const thinking = isObject(extraArgs.thinking) ? { ...extraArgs.thinking } : {};
|
||||||
thinking.type = 'enabled';
|
thinking.type = 'enabled';
|
||||||
thinking.budget_tokens = toAnthropicBudget(reasoningOverride);
|
thinking.budget_tokens = toAnthropicBudget(reasoningOverride);
|
||||||
delete thinking.budgetTokens;
|
delete thinking.budgetTokens;
|
||||||
extraArgs.thinking = thinking;
|
extraArgs.thinking = thinking;
|
||||||
|
delete extraArgs.output_config;
|
||||||
}
|
}
|
||||||
} else if (provider === 'openai') {
|
} else if (provider === 'openai') {
|
||||||
delete extraArgs.reasoning_effort;
|
delete extraArgs.reasoning_effort;
|
||||||
|
|||||||
@@ -538,6 +538,20 @@ describe('applyThinkingConfig - composite variant integration', () => {
|
|||||||
expect(result.ANTHROPIC_DEFAULT_OPUS_MODEL).toBe('claude-opus-4-6-thinking(32768)');
|
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', () => {
|
it('should use codex effort suffix style when provider is codex', () => {
|
||||||
const envVars: NodeJS.ProcessEnv = {
|
const envVars: NodeJS.ProcessEnv = {
|
||||||
ANTHROPIC_MODEL: 'gpt-5.3-codex',
|
ANTHROPIC_MODEL: 'gpt-5.3-codex',
|
||||||
|
|||||||
@@ -58,4 +58,24 @@ describe('model-catalog compatibility lookups', () => {
|
|||||||
|
|
||||||
expect(catalog?.models.map((model) => model.id)).toEqual(['gemini-2.5-pro']);
|
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,
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
@@ -147,9 +147,11 @@ describe('Model Catalog', () => {
|
|||||||
const opus47 = MODEL_CATALOG.claude.models.find((m) => m.id === 'claude-opus-4-7');
|
const opus47 = MODEL_CATALOG.claude.models.find((m) => m.id === 'claude-opus-4-7');
|
||||||
assert(opus47, 'Should include Claude Opus 4.7');
|
assert(opus47, 'Should include Claude Opus 4.7');
|
||||||
assert.strictEqual(opus47.name, 'Claude Opus 4.7');
|
assert.strictEqual(opus47.name, 'Claude Opus 4.7');
|
||||||
// Claude provider differs from AGY: zero thinking budget is disallowed,
|
// Opus 4.7 requires adaptive thinking (type: 'levels'); manual budget_tokens
|
||||||
// and extended (1M) context is available on the Anthropic API.
|
// is rejected by the Anthropic API with 400. Extended (1M) context is available.
|
||||||
assert.strictEqual(opus47.thinking.zeroAllowed, false);
|
assert.strictEqual(opus47.thinking.type, 'levels');
|
||||||
|
assert.deepStrictEqual(opus47.thinking.levels, ['low', 'medium', 'high', 'xhigh', 'max']);
|
||||||
|
assert.strictEqual(opus47.thinking.maxLevel, 'max');
|
||||||
assert.strictEqual(opus47.extendedContext, true);
|
assert.strictEqual(opus47.extendedContext, true);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ describe('Thinking Validator', () => {
|
|||||||
expect(VALID_THINKING_LEVELS).toContain('medium');
|
expect(VALID_THINKING_LEVELS).toContain('medium');
|
||||||
expect(VALID_THINKING_LEVELS).toContain('high');
|
expect(VALID_THINKING_LEVELS).toContain('high');
|
||||||
expect(VALID_THINKING_LEVELS).toContain('xhigh');
|
expect(VALID_THINKING_LEVELS).toContain('xhigh');
|
||||||
|
expect(VALID_THINKING_LEVELS).toContain('max');
|
||||||
expect(VALID_THINKING_LEVELS).toContain('auto');
|
expect(VALID_THINKING_LEVELS).toContain('auto');
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -38,6 +39,24 @@ describe('Thinking Validator', () => {
|
|||||||
expect(THINKING_LEVEL_BUDGETS.medium).toBe(8192);
|
expect(THINKING_LEVEL_BUDGETS.medium).toBe(8192);
|
||||||
expect(THINKING_LEVEL_BUDGETS.high).toBe(24576);
|
expect(THINKING_LEVEL_BUDGETS.high).toBe(24576);
|
||||||
expect(THINKING_LEVEL_BUDGETS.xhigh).toBe(32768);
|
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', () => {
|
it('should export off values', () => {
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ describe('config thinking override normalization', () => {
|
|||||||
|
|
||||||
it('accepts valid levels', () => {
|
it('accepts valid levels', () => {
|
||||||
expect(parseThinkingOverrideInput('High')).toEqual({ value: 'high' });
|
expect(parseThinkingOverrideInput('High')).toEqual({ value: 'high' });
|
||||||
|
expect(parseThinkingOverrideInput('Max')).toEqual({ value: 'max' });
|
||||||
});
|
});
|
||||||
|
|
||||||
it('validates numeric bounds', () => {
|
it('validates numeric bounds', () => {
|
||||||
|
|||||||
@@ -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', () => {
|
it('preserves mixed user text around tool_result blocks in order', () => {
|
||||||
const translated = translateAnthropicRequest({
|
const translated = translateAnthropicRequest({
|
||||||
messages: [
|
messages: [
|
||||||
|
|||||||
@@ -30,6 +30,17 @@ describe('ProxyRequestTransformer regressions', () => {
|
|||||||
expect(result.reasoning).toEqual({ enabled: true, effort: 'high' });
|
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', () => {
|
it('rejects unsupported thinking types instead of silently dropping them', () => {
|
||||||
expect(() =>
|
expect(() =>
|
||||||
new ProxyRequestTransformer().transform({
|
new ProxyRequestTransformer().transform({
|
||||||
|
|||||||
@@ -143,6 +143,23 @@ describe('droid-config-manager', () => {
|
|||||||
expect(settings.customModels[0].extraArgs?.thinking?.budget_tokens).toBe(40960);
|
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 () => {
|
it('should clear prior reasoning config when override disables thinking', async () => {
|
||||||
await upsertCcsModel('glm', {
|
await upsertCcsModel('glm', {
|
||||||
model: 'glm-4.7',
|
model: 'glm-4.7',
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ const THINKING_LEVELS = [
|
|||||||
{ value: 'medium', label: 'Medium (8K tokens)' },
|
{ value: 'medium', label: 'Medium (8K tokens)' },
|
||||||
{ value: 'high', label: 'High (24K tokens)' },
|
{ value: 'high', label: 'High (24K tokens)' },
|
||||||
{ value: 'xhigh', label: 'Extra High (32K tokens)' },
|
{ value: 'xhigh', label: 'Extra High (32K tokens)' },
|
||||||
|
{ value: 'max', label: 'Max (adaptive ceiling)' },
|
||||||
{ value: 'auto', label: 'Auto (dynamic)' },
|
{ value: 'auto', label: 'Auto (dynamic)' },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user