diff --git a/.changeset/adaptive-thinking-env.md b/.changeset/adaptive-thinking-env.md new file mode 100644 index 00000000..308bf7ac --- /dev/null +++ b/.changeset/adaptive-thinking-env.md @@ -0,0 +1,7 @@ +--- +"@moonshot-ai/agent-core": minor +"@moonshot-ai/kimi-code": minor +"@moonshot-ai/kosong": minor +--- + +Add `KIMI_MODEL_ADAPTIVE_THINKING` (and a matching `adaptive_thinking` model-alias field) to force adaptive thinking (`thinking: { type: 'adaptive' }`) on or off, overriding the Anthropic model-name version inference. This lets custom-named staff endpoints that back an adaptive-capable model opt in even when the model name does not encode a parseable Claude version. diff --git a/docs/en/configuration/config-files.md b/docs/en/configuration/config-files.md index b6bc798b..689dc997 100644 --- a/docs/en/configuration/config-files.md +++ b/docs/en/configuration/config-files.md @@ -126,6 +126,7 @@ Each entry in the `models` table defines a model alias, keyed by a unique name. | `capabilities` | `array` | No | Capability tags to add explicitly, for example `thinking`, `image_in`, `video_in`, `audio_in`, `tool_use` | | `display_name` | `string` | No | Name shown in the UI; falls back to `model` when unset | | `reasoning_key` | `string` | No | `openai` provider only. Override the field name used for reasoning content. By default the provider auto-detects `reasoning_content`, `reasoning_details`, and `reasoning` on incoming responses and serializes thinking back as `reasoning_content` — set this only if your gateway uses a non-standard field name | +| `adaptive_thinking` | `boolean` | No | `anthropic` provider only. Force adaptive thinking (`thinking: { type: 'adaptive' }`) on or off, overriding the model-name version inference. Set `true` for a custom-named endpoint that backs an adaptive-capable model whose name does not encode a parseable Claude version; forcing it on for an endpoint that does not support adaptive thinking makes the API reject the request. Omit to infer from the model name (Claude ≥ 4.6 uses adaptive) | `capabilities` is unioned with the capabilities that the provider capability registry matches by model-name prefix — entries can only be added, never removed. You usually do not need to set this by hand; reach for it only when the model is not covered by the registry, or when you want to force-enable a particular capability. diff --git a/docs/en/configuration/env-vars.md b/docs/en/configuration/env-vars.md index f3ef74e4..337d147a 100644 --- a/docs/en/configuration/env-vars.md +++ b/docs/en/configuration/env-vars.md @@ -85,9 +85,12 @@ For testing you can make Kimi Code use a specific model **without editing `confi | `KIMI_MODEL_DEFAULT_THINKING` | No | Default Thinking toggle for new sessions | Unset follows the global default (Thinking on) | | `KIMI_MODEL_THINKING_MODE` | No | Thinking trigger policy; `auto`/`on`/`off` | — | | `KIMI_MODEL_THINKING_EFFORT` | No | Thinking effort (e.g. `low`/`medium`/`high`/`xhigh`/`max`; available levels depend on the provider) | — | +| `KIMI_MODEL_ADAPTIVE_THINKING` | No | Force adaptive thinking (`thinking: { type: 'adaptive' }`) on or off, overriding the model-name version inference (`anthropic` only) | Inferred from the model name (Claude ≥ 4.6 uses adaptive) | The synthesized entries use the reserved keys `__kimi_env__` (provider) and `__kimi_env_model__` (model alias). When `KIMI_MODEL_NAME` is set but a required variable is missing or invalid, startup fails with a clear error. +Set `KIMI_MODEL_ADAPTIVE_THINKING=true` when a custom-named Anthropic-compatible endpoint backs a model that supports adaptive thinking but whose model name does not encode a parseable Claude version (so the automatic inference would otherwise fall back to budget-based thinking). Forcing it on for an endpoint that does **not** support adaptive thinking makes the API reject the request, so leave it unset unless you know the backing model supports it. + ```sh export KIMI_MODEL_NAME="kimi-for-coding" export KIMI_MODEL_BASE_URL="https://api-staff.msh.team/v1" diff --git a/docs/zh/configuration/config-files.md b/docs/zh/configuration/config-files.md index 4d9af35f..41fcd4e7 100644 --- a/docs/zh/configuration/config-files.md +++ b/docs/zh/configuration/config-files.md @@ -126,6 +126,7 @@ custom_headers = { "X-Custom-Header" = "value" } | `capabilities` | `array` | 否 | 显式追加的模型能力标签,例如 `thinking`、`image_in`、`video_in`、`audio_in`、`tool_use` | | `display_name` | `string` | 否 | 在 UI 中显示的名称,未设置时回退到 `model` | | `reasoning_key` | `string` | 否 | 仅 `openai` 供应商。覆盖推理内容所用的字段名。默认情况下供应商会自动识别响应中的 `reasoning_content`、`reasoning_details`、`reasoning`,并以 `reasoning_content` 回传思考内容 —— 只有当网关使用非标准字段名时才需要设置 | +| `adaptive_thinking` | `boolean` | 否 | 仅 `anthropic` 供应商。强制开启或关闭 adaptive thinking(`thinking: { type: 'adaptive' }`),覆盖按模型名推断版本的逻辑。当某个自定义命名的端点背后的模型支持 adaptive、但其名称无法解析出可识别的 Claude 版本时,可设为 `true`;若对不支持 adaptive thinking 的端点强制开启,API 会直接拒绝请求。省略时按模型名推断(Claude ≥ 4.6 使用 adaptive) | `capabilities` 与供应商 capability registry 按模型名前缀自动匹配出来的能力做并集 —— 只能追加、不能移除。通常无需手写;只有当模型未被 registry 覆盖、或希望强制启用某项能力时才用得到。 diff --git a/docs/zh/configuration/env-vars.md b/docs/zh/configuration/env-vars.md index e287fba8..3ca15bcc 100644 --- a/docs/zh/configuration/env-vars.md +++ b/docs/zh/configuration/env-vars.md @@ -85,9 +85,12 @@ OAuth 流程默认连接 Kimi 官方的认证与托管端点,下列变量可 | `KIMI_MODEL_DEFAULT_THINKING` | 否 | 新会话的默认 Thinking 开关 | 未设时跟随全局默认(Thinking 开启) | | `KIMI_MODEL_THINKING_MODE` | 否 | Thinking 触发策略,可选 `auto`/`on`/`off` | — | | `KIMI_MODEL_THINKING_EFFORT` | 否 | Thinking 强度(如 `low`/`medium`/`high`/`xhigh`/`max`;实际可用等级由供应商决定) | — | +| `KIMI_MODEL_ADAPTIVE_THINKING` | 否 | 强制开启或关闭 adaptive thinking(`thinking: { type: 'adaptive' }`),覆盖按模型名推断版本的逻辑(仅 `anthropic`) | 按模型名推断(Claude ≥ 4.6 使用 adaptive) | 合成出的条目使用保留键 `__kimi_env__`(供应商)和 `__kimi_env_model__`(模型别名)。当设置了 `KIMI_MODEL_NAME` 但缺少必填变量或变量取值非法时,启动会以清晰的错误信息失败。 +当某个自定义命名的 Anthropic 兼容端点背后的模型支持 adaptive thinking、但其模型名无法解析出可识别的 Claude 版本(否则自动推断会回退到 budget 模式)时,设置 `KIMI_MODEL_ADAPTIVE_THINKING=true`。若对**不支持** adaptive thinking 的端点强制开启,API 会直接拒绝请求;因此在不确定背后模型是否支持时,请保持该变量为未设置。 + ```sh export KIMI_MODEL_NAME="kimi-for-coding" export KIMI_MODEL_BASE_URL="https://api-staff.msh.team/v1" diff --git a/packages/agent-core/src/config/env-model.ts b/packages/agent-core/src/config/env-model.ts index 5f46235a..1d0956b7 100644 --- a/packages/agent-core/src/config/env-model.ts +++ b/packages/agent-core/src/config/env-model.ts @@ -68,15 +68,13 @@ function parseCapabilities(raw: string | undefined): string[] | undefined { // `parseBooleanEnv` returns undefined for unrecognized input. Treat a non-empty // but unparseable value (e.g. a typo like `flase`) as a config error so it // fails fast like the other KIMI_MODEL_* values, instead of silently keeping -// config.toml's existing default_thinking. -function parseDefaultThinking(raw: string | undefined): boolean | undefined { +// config.toml's existing value. +function parseBooleanVar(raw: string | undefined, varName: string): boolean | undefined { const value = trimmed(raw); if (value === undefined) return undefined; const parsed = parseBooleanEnv(value); if (parsed === undefined) { - fail( - `KIMI_MODEL_DEFAULT_THINKING must be a boolean (true/false/1/0/yes/no/on/off), got "${raw}".`, - ); + fail(`${varName} must be a boolean (true/false/1/0/yes/no/on/off), got "${raw}".`); } return parsed; } @@ -124,6 +122,10 @@ export function applyEnvModelConfig(config: KimiConfig, env: Env = process.env): const capabilities = parseCapabilities(env['KIMI_MODEL_CAPABILITIES']) ?? DEFAULT_CAPABILITIES; const displayName = trimmed(env['KIMI_MODEL_DISPLAY_NAME']); const reasoningKey = trimmed(env['KIMI_MODEL_REASONING_KEY']); + const adaptiveThinking = parseBooleanVar( + env['KIMI_MODEL_ADAPTIVE_THINKING'], + 'KIMI_MODEL_ADAPTIVE_THINKING', + ); const alias: ModelAlias = { provider: ENV_MODEL_PROVIDER_KEY, @@ -133,6 +135,7 @@ export function applyEnvModelConfig(config: KimiConfig, env: Env = process.env): ...(displayName !== undefined ? { displayName } : {}), ...(maxOutputSize !== undefined ? { maxOutputSize } : {}), ...(reasoningKey !== undefined ? { reasoningKey } : {}), + ...(adaptiveThinking !== undefined ? { adaptiveThinking } : {}), }; const thinkingMode = trimmed(env['KIMI_MODEL_THINKING_MODE']); @@ -148,7 +151,10 @@ export function applyEnvModelConfig(config: KimiConfig, env: Env = process.env): ...(thinkingEffort !== undefined ? { effort: thinkingEffort } : {}), } : config.thinking; - const defaultThinking = parseDefaultThinking(env['KIMI_MODEL_DEFAULT_THINKING']); + const defaultThinking = parseBooleanVar( + env['KIMI_MODEL_DEFAULT_THINKING'], + 'KIMI_MODEL_DEFAULT_THINKING', + ); const merged: KimiConfig = { ...config, diff --git a/packages/agent-core/src/config/schema.ts b/packages/agent-core/src/config/schema.ts index ac3d17e1..e55d0f0b 100644 --- a/packages/agent-core/src/config/schema.ts +++ b/packages/agent-core/src/config/schema.ts @@ -43,6 +43,10 @@ export const ModelAliasSchema = z.object({ capabilities: z.array(z.string()).optional(), displayName: z.string().optional(), reasoningKey: z.string().optional(), + // Explicitly declare adaptive-thinking support, overriding the kosong + // model-name version inference. Needed for custom-named Anthropic endpoints + // whose model name does not encode a parseable Claude version. + adaptiveThinking: z.boolean().optional(), }); export type ModelAlias = z.infer; diff --git a/packages/agent-core/src/session/provider-manager.ts b/packages/agent-core/src/session/provider-manager.ts index c0e39179..81281863 100644 --- a/packages/agent-core/src/session/provider-manager.ts +++ b/packages/agent-core/src/session/provider-manager.ts @@ -108,6 +108,7 @@ export class ProviderManager implements ModelProvider { alias.maxOutputSize, alias.reasoningKey, this.options.promptCacheKey, + alias.adaptiveThinking, ); return { @@ -200,7 +201,15 @@ function resolveModelCapabilities( image_in: declared.has('image_in') || detected.image_in, video_in: declared.has('video_in') || detected.video_in, audio_in: declared.has('audio_in') || detected.audio_in, - thinking: declared.has('thinking') || declared.has('always_thinking') || detected.thinking, + // Forcing adaptive thinking implies the model supports thinking, so advertise + // it even when the alias declares no capabilities and the custom model name is + // not in the capability catalog. Otherwise the model picker would classify the + // alias as "thinking unsupported" and disable it. + thinking: + declared.has('thinking') || + declared.has('always_thinking') || + alias.adaptiveThinking === true || + detected.thinking, tool_use: declared.has('tool_use') || detected.tool_use, max_context_tokens: alias.maxContextSize, }; @@ -213,6 +222,7 @@ function toKosongProviderConfig( maxOutputSize: number | undefined, reasoningKey: string | undefined, promptCacheKey: string | undefined, + adaptiveThinking: boolean | undefined, ): KosongProviderConfig { switch (provider.type) { case 'anthropic': @@ -222,6 +232,7 @@ function toKosongProviderConfig( baseUrl: providerValue(provider.baseUrl, provider.env, 'ANTHROPIC_BASE_URL'), apiKey: providerApiKey(provider), ...(maxOutputSize !== undefined ? { defaultMaxTokens: maxOutputSize } : {}), + ...(adaptiveThinking !== undefined ? { adaptiveThinking } : {}), ...defaultHeadersField(provider.customHeaders), }; case 'openai': diff --git a/packages/agent-core/test/config/env-model.test.ts b/packages/agent-core/test/config/env-model.test.ts index 77c21a8a..c66a5713 100644 --- a/packages/agent-core/test/config/env-model.test.ts +++ b/packages/agent-core/test/config/env-model.test.ts @@ -151,6 +151,26 @@ describe('applyEnvModelConfig', () => { ); }); + it('maps KIMI_MODEL_ADAPTIVE_THINKING onto the alias', () => { + expect( + apply({ ...MIN, KIMI_MODEL_ADAPTIVE_THINKING: 'true' }) + .models?.[ENV_MODEL_ALIAS_KEY]?.adaptiveThinking, + ).toBe(true); + expect( + apply({ ...MIN, KIMI_MODEL_ADAPTIVE_THINKING: 'false' }) + .models?.[ENV_MODEL_ALIAS_KEY]?.adaptiveThinking, + ).toBe(false); + expect( + apply({ ...MIN }).models?.[ENV_MODEL_ALIAS_KEY]?.adaptiveThinking, + ).toBeUndefined(); + }); + + it('rejects an invalid KIMI_MODEL_ADAPTIVE_THINKING', () => { + expectConfigInvalid(() => + apply({ ...MIN, KIMI_MODEL_ADAPTIVE_THINKING: 'maybe' }), + ); + }); + it('preserves unrelated config fields', () => { const base = getDefaultConfig(); base.defaultPermissionMode = 'auto'; diff --git a/packages/agent-core/test/harness/runtime-provider.test.ts b/packages/agent-core/test/harness/runtime-provider.test.ts index a0b3e8c7..5eb810fb 100644 --- a/packages/agent-core/test/harness/runtime-provider.test.ts +++ b/packages/agent-core/test/harness/runtime-provider.test.ts @@ -155,6 +155,53 @@ describe('resolveRuntimeProvider model metadata', () => { }); }); + it('treats alias.adaptiveThinking as advertising the thinking capability', () => { + const resolved = resolveRuntimeProvider({ + config: { + ...BASE_CONFIG, + providers: { + ...BASE_CONFIG.providers, + anthropic: { type: 'anthropic', apiKey: 'sk-anthropic' }, + }, + models: { + ...BASE_CONFIG.models!, + 'okapi-alias': { + provider: 'anthropic', + model: 'coding-model-okapi-0527-vibe', + maxContextSize: 200000, + adaptiveThinking: true, + }, + }, + }, + model: 'okapi-alias', + }); + + expect(resolved.modelCapabilities.thinking).toBe(true); + }); + + it('does not advertise thinking for a custom-named alias without adaptiveThinking or a thinking capability', () => { + const resolved = resolveRuntimeProvider({ + config: { + ...BASE_CONFIG, + providers: { + ...BASE_CONFIG.providers, + anthropic: { type: 'anthropic', apiKey: 'sk-anthropic' }, + }, + models: { + ...BASE_CONFIG.models!, + 'okapi-alias': { + provider: 'anthropic', + model: 'coding-model-okapi-0527-vibe', + maxContextSize: 200000, + }, + }, + }, + model: 'okapi-alias', + }); + + expect(resolved.modelCapabilities.thinking).toBe(false); + }); + it('rejects provider model names that are not configured aliases', () => { expect(() => resolveRuntimeProvider({ @@ -281,6 +328,57 @@ describe('resolveRuntimeProvider maxOutputSize forwarding', () => { }); expect('defaultMaxTokens' in resolved.provider).toBe(false); }); + + it('forwards alias.adaptiveThinking to the anthropic provider config', () => { + const resolved = resolveRuntimeProvider({ + config: { + ...BASE_CONFIG, + providers: { + ...BASE_CONFIG.providers, + anthropic: { type: 'anthropic', apiKey: 'sk-anthropic' }, + }, + models: { + ...BASE_CONFIG.models!, + 'okapi-alias': { + provider: 'anthropic', + model: 'coding-model-okapi-0527-vibe', + maxContextSize: 200000, + adaptiveThinking: true, + }, + }, + }, + model: 'okapi-alias', + }); + + expect(resolved.provider).toMatchObject({ + type: 'anthropic', + model: 'coding-model-okapi-0527-vibe', + adaptiveThinking: true, + }); + }); + + it('omits adaptiveThinking when alias.adaptiveThinking is unset', () => { + const resolved = resolveRuntimeProvider({ + config: { + ...BASE_CONFIG, + providers: { + ...BASE_CONFIG.providers, + anthropic: { type: 'anthropic', apiKey: 'sk-anthropic' }, + }, + models: { + ...BASE_CONFIG.models!, + 'opus-alias': { + provider: 'anthropic', + model: 'claude-opus-4-7', + maxContextSize: 200000, + }, + }, + }, + model: 'opus-alias', + }); + + expect('adaptiveThinking' in resolved.provider).toBe(false); + }); }); describe('resolveRuntimeProvider Kimi request headers', () => { diff --git a/packages/kosong/src/providers/anthropic.ts b/packages/kosong/src/providers/anthropic.ts index 6240a51a..4028ce93 100644 --- a/packages/kosong/src/providers/anthropic.ts +++ b/packages/kosong/src/providers/anthropic.ts @@ -85,6 +85,13 @@ export interface AnthropicOptions { metadata?: Record | undefined; /** Use streaming API. Defaults to true. Set to false for non-streaming (test/fallback). */ stream?: boolean | undefined; + /** + * Explicitly declare whether the model supports adaptive thinking + * (`thinking: { type: 'adaptive' }`), overriding the model-name version + * inference. Useful for custom-named endpoints whose model name does not + * encode a parseable Claude version. Leave undefined to infer from the name. + */ + adaptiveThinking?: boolean | undefined; clientFactory?: (auth: ProviderRequestAuth) => Anthropic; } @@ -285,22 +292,22 @@ function isOpus47(model: string): boolean { return version.major === 4 && version.minor === 7; } -function supportsEffortParam(model: string): boolean { - if (supportsAdaptiveThinking(model)) { +function supportsEffortParam(model: string, adaptive: boolean): boolean { + if (adaptive) { return true; } const normalized = model.toLowerCase(); return normalized.includes('opus-4-5') || normalized.includes('opus-4.5'); } -function clampEffort(effort: ThinkingEffort, model: string): ThinkingEffort { +function clampEffort(effort: ThinkingEffort, model: string, adaptive: boolean): ThinkingEffort { if (effort === 'off') { return effort; } if (effort === 'xhigh' && !isOpus47(model)) { return 'high'; } - if (effort === 'max' && !supportsAdaptiveThinking(model)) { + if (effort === 'max' && !adaptive) { return 'high'; } return effort; @@ -807,11 +814,13 @@ export class AnthropicChatProvider implements ChatProvider { private _baseUrl: string | undefined; private _defaultHeaders: Record | undefined; private _clientFactory: ((auth: ProviderRequestAuth) => Anthropic) | undefined; + private _adaptiveThinking: boolean | undefined; constructor(options: AnthropicOptions) { this._model = options.model; this._stream = options.stream ?? true; this._metadata = options.metadata; + this._adaptiveThinking = options.adaptiveThinking; const apiKey = options.apiKey ?? process.env['ANTHROPIC_API_KEY']; this._apiKey = apiKey === undefined || apiKey.length === 0 ? undefined : apiKey; this._baseUrl = options.baseUrl; @@ -1020,9 +1029,13 @@ export class AnthropicChatProvider implements ChatProvider { } withThinking(effort: ThinkingEffort): AnthropicChatProvider { + // Resolve once: an explicit `adaptiveThinking` option overrides the + // model-name version inference, so custom-named endpoints can opt in/out. + const adaptive = this._adaptiveThinking ?? supportsAdaptiveThinking(this._model); + if (effort === 'off') { let newBetas = [...(this._generationKwargs.betaFeatures ?? [])]; - if (supportsAdaptiveThinking(this._model)) { + if (adaptive) { newBetas = newBetas.filter((b) => b !== INTERLEAVED_THINKING_BETA); } const clone = this._withGenerationKwargs({ @@ -1033,14 +1046,14 @@ export class AnthropicChatProvider implements ChatProvider { return clone; } - const effectiveEffort = clampEffort(effort, this._model); + const effectiveEffort = clampEffort(effort, this._model, adaptive); if (effectiveEffort === 'off') { throw new Error('Non-off thinking effort unexpectedly clamped to off.'); } let newBetas = [...(this._generationKwargs.betaFeatures ?? [])]; - if (supportsAdaptiveThinking(this._model)) { + if (adaptive) { newBetas = newBetas.filter((b) => b !== INTERLEAVED_THINKING_BETA); return this._withGenerationKwargs({ thinking: { type: 'adaptive', display: 'summarized' }, @@ -1053,13 +1066,13 @@ export class AnthropicChatProvider implements ChatProvider { thinking: { type: 'enabled', budget_tokens: budgetTokensForEffort(effectiveEffort) }, betaFeatures: newBetas, }; - if (supportsEffortParam(this._model)) { + if (supportsEffortParam(this._model, adaptive)) { kwargs.output_config = { effort: effectiveEffort }; } else { kwargs.output_config = undefined; } const clone = this._withGenerationKwargs(kwargs); - if (!supportsEffortParam(this._model)) { + if (!supportsEffortParam(this._model, adaptive)) { delete clone._generationKwargs.output_config; } return clone; diff --git a/packages/kosong/test/anthropic.test.ts b/packages/kosong/test/anthropic.test.ts index b68dbbd0..b539ca1d 100644 --- a/packages/kosong/test/anthropic.test.ts +++ b/packages/kosong/test/anthropic.test.ts @@ -925,6 +925,61 @@ describe('AnthropicChatProvider', () => { expect(body['output_config']).toEqual({ effort: 'max' }); }); + it('adaptiveThinking=true forces adaptive on an unversioned model name', async () => { + const provider = new AnthropicChatProvider({ + model: 'coding-model-okapi-0527-vibe', + apiKey: 'test-key', + defaultMaxTokens: 1024, + stream: false, + adaptiveThinking: true, + }).withThinking('high'); + const body = await captureRequestBody(provider, '', [], thinkHistory); + + expect(body['thinking']).toEqual({ type: 'adaptive', display: 'summarized' }); + expect(body['output_config']).toEqual({ effort: 'high' }); + }); + + it('forced adaptive allows max effort without clamping to high', async () => { + const provider = new AnthropicChatProvider({ + model: 'coding-model-okapi-0527-vibe', + apiKey: 'test-key', + defaultMaxTokens: 1024, + stream: false, + adaptiveThinking: true, + }).withThinking('max'); + const body = await captureRequestBody(provider, '', [], thinkHistory); + + expect(body['thinking']).toEqual({ type: 'adaptive', display: 'summarized' }); + expect(body['output_config']).toEqual({ effort: 'max' }); + }); + + it('unversioned model name without adaptiveThinking stays budget-based', async () => { + const provider = new AnthropicChatProvider({ + model: 'coding-model-okapi-0527-vibe', + apiKey: 'test-key', + defaultMaxTokens: 1024, + stream: false, + }).withThinking('high'); + const body = await captureRequestBody(provider, '', [], thinkHistory); + + expect(body['thinking']).toEqual({ type: 'enabled', budget_tokens: 32000 }); + expect(body['output_config']).toBeUndefined(); + }); + + it('adaptiveThinking=false forces budget on a 4.6 model name', async () => { + const provider = new AnthropicChatProvider({ + model: 'claude-opus-4-6', + apiKey: 'test-key', + defaultMaxTokens: 1024, + stream: false, + adaptiveThinking: false, + }).withThinking('high'); + const body = await captureRequestBody(provider, '', [], thinkHistory); + + expect(body['thinking']).toEqual({ type: 'enabled', budget_tokens: 32000 }); + expect(body['output_config']).toBeUndefined(); + }); + it('pre-4.6 model clamps xhigh and max to high without output_config', async () => { for (const effort of ['xhigh', 'max'] as const) { const provider = createProvider('claude-sonnet-4-5').withThinking(effort);