From 2b0d3e89e39bf07265a3d73ea20e0d2143c38967 Mon Sep 17 00:00:00 2001 From: Luca Steeb Date: Fri, 19 Jun 2026 16:07:31 +0100 Subject: [PATCH 1/3] docs(reasoning): document Opus 4.6/4.7/4.8 adaptive reasoning Co-Authored-By: Claude Opus 4.8 (1M context) --- apps/docs/content/features/reasoning.mdx | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/apps/docs/content/features/reasoning.mdx b/apps/docs/content/features/reasoning.mdx index 039bd2808c..0419fb1fef 100644 --- a/apps/docs/content/features/reasoning.mdx +++ b/apps/docs/content/features/reasoning.mdx @@ -16,7 +16,7 @@ You can find all reasoning-enabled models on our [models page with reasoning fil - OpenAI's GPT-5 series (e.g., `gpt-5`, `gpt-5-mini`) - Note: GPT-5 models use reasoning but currently do not return the reasoning content in the response. -- Anthropic's Claude 3.7 Sonnet +- Anthropic's Claude 3.7 Sonnet, Claude Sonnet 4, and Claude Opus 4 / 4.5 / 4.6 / 4.7 / 4.8 - Google's Gemini 2.0 Flash Thinking and Gemini 2.5 Pro - GPT OSS models such as `gpt-oss-120b` and `gpt-oss-20b` - Z.AI's reasoning models @@ -164,11 +164,23 @@ curl -X POST "https://api.llmgateway.io/v1/chat/completions" \ The `reasoning.max_tokens` parameter is supported by: -- **Anthropic Claude**: Claude 3.7 Sonnet, Claude Sonnet 4, Claude Opus 4, Claude Opus 4.5 +- **Anthropic Claude**: Claude 3.7 Sonnet, Claude Sonnet 4, Claude Opus 4, Claude Opus 4.5, Claude Opus 4.6, Claude Opus 4.7, Claude Opus 4.8 - **Google Gemini**: Gemini 2.5 Pro, Gemini 2.5 Flash, Gemini 3 Pro Preview When using auto-routing or root models with `reasoning.max_tokens`, only providers that support this feature will be considered. + + **Adaptive reasoning (Claude Opus 4.6 / 4.7 / 4.8).** These models use + Anthropic's adaptive thinking, where the model itself decides how much to + think based on the difficulty of the prompt. Control the depth with + `reasoning_effort` (or `reasoning.effort`) — `low`, `medium`, `high`, `xhigh`, + or `max`. A `reasoning.max_tokens` value is still accepted on these models, + but it is routed to adaptive thinking rather than enforced as an exact budget. + For easy prompts the model may reason only briefly (or skip visible reasoning) + and begin its answer almost immediately even at `high` effort — this is + expected adaptive behavior, not a disabled-reasoning bug. + + ### Provider-Specific Constraints - **Anthropic**: Reasoning budget must be between 1,024 and 128,000 tokens. Values outside this range are automatically clamped. From 9f9e017202cb02c2ab31f756f4ff5e5473f40fb6 Mon Sep 17 00:00:00 2001 From: Luca Steeb Date: Fri, 19 Jun 2026 16:42:41 +0100 Subject: [PATCH 2/3] feat(reasoning): bucket max_tokens to effort on adaptive models Adaptive Claude Opus models (4.6+) reject an explicit thinking budget, so a bare reasoning.max_tokens was previously accepted but dropped. Map it onto an adaptive effort level (<2k low, <8k medium, <24k high, else xhigh) so the requested budget still influences depth while keeping budget-based clients (e.g. Claude Code) working. Factor the shared effort resolution into one helper and document adaptive thinking. Co-Authored-By: Claude Opus 4.8 (1M context) --- apps/docs/content/features/reasoning.mdx | 49 ++++--- .../src/prepare-request-body.adaptive.spec.ts | 37 ++++++ packages/actions/src/prepare-request-body.ts | 121 +++++++++++------- 3 files changed, 146 insertions(+), 61 deletions(-) diff --git a/apps/docs/content/features/reasoning.mdx b/apps/docs/content/features/reasoning.mdx index 0419fb1fef..99aa4f1b76 100644 --- a/apps/docs/content/features/reasoning.mdx +++ b/apps/docs/content/features/reasoning.mdx @@ -16,7 +16,7 @@ You can find all reasoning-enabled models on our [models page with reasoning fil - OpenAI's GPT-5 series (e.g., `gpt-5`, `gpt-5-mini`) - Note: GPT-5 models use reasoning but currently do not return the reasoning content in the response. -- Anthropic's Claude 3.7 Sonnet, Claude Sonnet 4, and Claude Opus 4 / 4.5 / 4.6 / 4.7 / 4.8 +- Anthropic's Claude Sonnet and Claude Opus models - Google's Gemini 2.0 Flash Thinking and Gemini 2.5 Pro - GPT OSS models such as `gpt-oss-120b` and `gpt-oss-20b` - Z.AI's reasoning models @@ -162,23 +162,13 @@ curl -X POST "https://api.llmgateway.io/v1/chat/completions" \ ### Supported Models -The `reasoning.max_tokens` parameter is supported by: +`reasoning.max_tokens` is supported by Anthropic Claude and Google Gemini thinking models. To see exactly which models support reasoning, filter the [models page by the reasoning capability](https://llmgateway.io/models?filters=1&reasoning=true). When using auto-routing or root models with `reasoning.max_tokens`, only providers that support this feature will be considered. -- **Anthropic Claude**: Claude 3.7 Sonnet, Claude Sonnet 4, Claude Opus 4, Claude Opus 4.5, Claude Opus 4.6, Claude Opus 4.7, Claude Opus 4.8 -- **Google Gemini**: Gemini 2.5 Pro, Gemini 2.5 Flash, Gemini 3 Pro Preview - -When using auto-routing or root models with `reasoning.max_tokens`, only providers that support this feature will be considered. - - - **Adaptive reasoning (Claude Opus 4.6 / 4.7 / 4.8).** These models use - Anthropic's adaptive thinking, where the model itself decides how much to - think based on the difficulty of the prompt. Control the depth with - `reasoning_effort` (or `reasoning.effort`) — `low`, `medium`, `high`, `xhigh`, - or `max`. A `reasoning.max_tokens` value is still accepted on these models, - but it is routed to adaptive thinking rather than enforced as an exact budget. - For easy prompts the model may reason only briefly (or skip visible reasoning) - and begin its answer almost immediately even at `high` effort — this is - expected adaptive behavior, not a disabled-reasoning bug. + + Anthropic's newer Claude Opus models use [adaptive + thinking](#adaptive-thinking-claude-opus) and do **not** honor an exact + `reasoning.max_tokens` budget — the value is mapped onto an effort level + instead. Prefer `reasoning_effort` / `reasoning.effort` for those models. ### Provider-Specific Constraints @@ -200,6 +190,31 @@ If you specify `reasoning.max_tokens` for a model that doesn't support it, you'l } ``` +## Adaptive Thinking (Claude Opus) + +Anthropic's newer Claude Opus models — **Claude Opus 4.6 and later** — use **adaptive thinking** instead of a fixed reasoning budget. With adaptive thinking, the model itself decides how much to reason based on the difficulty of each prompt. This changes how the reasoning parameters behave: + +- **No exact reasoning-token budget.** These models do not accept an explicit thinking budget, so `reasoning.max_tokens` is **not** enforced as a hard limit. For backward compatibility the gateway still accepts the parameter and maps the requested budget onto an effort level (`< 2k → low`, `< 8k → medium`, `< 24k → high`, otherwise `xhigh`) so it still influences depth — but the exact token count is not guaranteed. Prefer `reasoning_effort` / `reasoning.effort` for these models. +- **Effort is a hint, not a fixed depth.** Even when you specify a `reasoning_effort` such as `high`, the model can dynamically decide to reason only briefly — or skip visible reasoning entirely — for simple prompts and begin its answer almost immediately, while thinking more on harder ones. This is expected adaptive behavior, not reasoning being disabled. + +Control the depth with `reasoning_effort` or `reasoning.effort` (`low`, `medium`, `high`, `xhigh`, `max`); the gateway translates this into Anthropic's adaptive `output_config.effort`. + +```bash +curl -X POST "https://api.llmgateway.io/v1/chat/completions" \ + -H "Authorization: Bearer $LLM_GATEWAY_API_KEY" \ + -H "Content-Type: application/json" \ + -d '{ + "model": "claude-opus-4-6", + "messages": [ + { + "role": "user", + "content": "Explain the P vs NP problem and why it matters." + } + ], + "reasoning_effort": "high" + }' +``` + ## Streaming Reasoning Content When streaming is enabled, reasoning content will be streamed as part of the response chunks: diff --git a/packages/actions/src/prepare-request-body.adaptive.spec.ts b/packages/actions/src/prepare-request-body.adaptive.spec.ts index 46d6d58396..9953e689ee 100644 --- a/packages/actions/src/prepare-request-body.adaptive.spec.ts +++ b/packages/actions/src/prepare-request-body.adaptive.spec.ts @@ -87,4 +87,41 @@ describe("prepareRequestBody - adaptive thinking (Opus 4.6/4.7/4.8)", () => { display: "summarized", }); }); + + // A bare reasoning.max_tokens budget can't be enforced on adaptive models + // (they reject budget_tokens), so it is bucketed into an effort level instead + // of being dropped: <2k -> low, <8k -> medium, <24k -> high, else xhigh. + for (const model of [ + "claude-opus-4-6", + "claude-opus-4-7", + "claude-opus-4-8", + ]) { + test.each([ + [1024, "low"], + [4096, "medium"], + [8000, "high"], + [16000, "high"], + [32000, "xhigh"], + ] as const)( + `${model} buckets reasoning.max_tokens=%i to effort %s`, + async (maxTokens, expected) => { + const body = await buildAnthropicBody(model, { + reasoning_max_tokens: maxTokens, + }); + expect(body.thinking).toEqual({ + type: "adaptive", + display: "summarized", + }); + expect(body.output_config?.effort).toBe(expected); + }, + ); + } + + test("explicit reasoning_effort wins over a max_tokens budget", async () => { + const body = await buildAnthropicBody("claude-opus-4-6", { + reasoning_effort: "low", + reasoning_max_tokens: 32000, + }); + expect(body.output_config?.effort).toBe("low"); + }); }); diff --git a/packages/actions/src/prepare-request-body.ts b/packages/actions/src/prepare-request-body.ts index 934bd990ac..fcca505726 100644 --- a/packages/actions/src/prepare-request-body.ts +++ b/packages/actions/src/prepare-request-body.ts @@ -26,6 +26,63 @@ import { transformGoogleMessages } from "./transform-google-messages.js"; type OpenAIImageQuality = "low" | "medium" | "high" | "auto"; +type AdaptiveEffort = "low" | "medium" | "high" | "xhigh" | "max"; + +/** + * Resolve `output_config.effort` for adaptive-thinking Anthropic models + * (Opus 4.6+). Precedence: explicit `effort`, then `reasoning_effort` mapped + * onto the adaptive scale, then `reasoning.max_tokens` bucketed into an effort + * level. Adaptive models reject `budget_tokens`, so a requested budget is + * translated into a depth hint instead of being dropped. Returns undefined when + * no reasoning controls were sent, leaving the model at its default depth. + */ +function resolveAdaptiveEffort( + effort: "low" | "medium" | "high" | undefined, + reasoning_effort: + | "none" + | "minimal" + | "low" + | "medium" + | "high" + | "xhigh" + | "max" + | undefined, + reasoning_max_tokens: number | undefined, +): AdaptiveEffort | undefined { + if (effort !== undefined) { + return effort; + } + if (reasoning_effort) { + switch (reasoning_effort) { + case "minimal": + case "low": + return "low"; + case "medium": + return "medium"; + case "xhigh": + return "xhigh"; + case "max": + return "max"; + case "high": + default: + return "high"; + } + } + if (reasoning_max_tokens !== undefined) { + if (reasoning_max_tokens < 2000) { + return "low"; + } + if (reasoning_max_tokens < 8000) { + return "medium"; + } + if (reasoning_max_tokens < 24000) { + return "high"; + } + return "xhigh"; + } + return undefined; +} + function getProviderMapping( modelDef: ModelDefinition | undefined, usedProvider: ProviderId, @@ -1908,28 +1965,21 @@ export async function prepareRequestBody( // thinking text — their default flipped to "omitted" (empty thinking, // signature only), unlike Opus 4.6 which defaults to "summarized". requestBody.thinking = { type: "adaptive", display: "summarized" }; - if (effort === undefined && reasoning_effort) { - const mapEffort = ( - e: typeof reasoning_effort, - ): "low" | "medium" | "high" | "xhigh" | "max" => { - switch (e) { - case "minimal": - case "low": - return "low"; - case "medium": - return "medium"; - case "high": - return "high"; - case "xhigh": - return "xhigh"; - case "max": - return "max"; - default: - return "high"; - } - }; - requestBody.output_config ??= {}; - requestBody.output_config.effort = mapEffort(reasoning_effort); + // Explicit `effort` is applied below alongside the other optional + // parameters; here we derive the adaptive depth from + // `reasoning_effort` or, failing that, bucket a requested + // `reasoning.max_tokens` into an effort level so the budget still + // influences depth instead of being silently dropped. + if (effort === undefined) { + const adaptiveEffort = resolveAdaptiveEffort( + undefined, + reasoning_effort, + reasoning_max_tokens, + ); + if (adaptiveEffort !== undefined) { + requestBody.output_config ??= {}; + requestBody.output_config.effort = adaptiveEffort; + } } } else { requestBody.thinking = { @@ -2380,28 +2430,11 @@ export async function prepareRequestBody( type: "adaptive", display: "summarized", }; - const mapEffort = ( - e: typeof reasoning_effort, - ): "low" | "medium" | "high" | "xhigh" | "max" => { - switch (e) { - case "minimal": - case "low": - return "low"; - case "medium": - return "medium"; - case "high": - return "high"; - case "xhigh": - return "xhigh"; - case "max": - return "max"; - default: - return "high"; - } - }; - const adaptiveEffort = - effort ?? - (reasoning_effort ? mapEffort(reasoning_effort) : undefined); + const adaptiveEffort = resolveAdaptiveEffort( + effort, + reasoning_effort, + reasoning_max_tokens, + ); if (adaptiveEffort !== undefined) { requestBody.additionalModelRequestFields.output_config = { effort: adaptiveEffort, From bc81872b46c3d9e1380e40997b0a54cfec40afad Mon Sep 17 00:00:00 2001 From: Luca Steeb Date: Fri, 19 Jun 2026 16:49:06 +0100 Subject: [PATCH 3/3] fix(reasoning): map effort 'none' to no adaptive effort The shared effort switch fell through 'none' to a default of "high", which would force strong reasoning if 'none' ever reached it (it's normalized away upstream for Anthropic/Bedrock today). Handle 'none' explicitly as no effort and drop the misleading catch-all; the switch is now exhaustive. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../actions/src/prepare-request-body.adaptive.spec.ts | 10 +++++++++- packages/actions/src/prepare-request-body.ts | 10 +++++++--- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/packages/actions/src/prepare-request-body.adaptive.spec.ts b/packages/actions/src/prepare-request-body.adaptive.spec.ts index 9953e689ee..3154055cf1 100644 --- a/packages/actions/src/prepare-request-body.adaptive.spec.ts +++ b/packages/actions/src/prepare-request-body.adaptive.spec.ts @@ -22,7 +22,7 @@ interface AdaptiveThinkingBody { async function buildAnthropicBody( internalModel: string, opts: { - reasoning_effort?: "low" | "medium" | "high" | "xhigh"; + reasoning_effort?: "none" | "low" | "medium" | "high" | "xhigh"; reasoning_max_tokens?: number; }, ): Promise { @@ -124,4 +124,12 @@ describe("prepareRequestBody - adaptive thinking (Opus 4.6/4.7/4.8)", () => { }); expect(body.output_config?.effort).toBe("low"); }); + + test("reasoning_effort 'none' disables thinking on adaptive models", async () => { + const body = await buildAnthropicBody("claude-opus-4-6", { + reasoning_effort: "none", + }); + expect(body.thinking).toBeUndefined(); + expect(body.output_config?.effort).toBeUndefined(); + }); }); diff --git a/packages/actions/src/prepare-request-body.ts b/packages/actions/src/prepare-request-body.ts index fcca505726..50daa5b352 100644 --- a/packages/actions/src/prepare-request-body.ts +++ b/packages/actions/src/prepare-request-body.ts @@ -54,18 +54,22 @@ function resolveAdaptiveEffort( } if (reasoning_effort) { switch (reasoning_effort) { + // "none" disables reasoning; it carries no depth, so request no effort. + // (Anthropic/Bedrock normalize it to `undefined` upstream before reaching + // here — this case keeps the mapping correct if that ever changes.) + case "none": + return undefined; case "minimal": case "low": return "low"; case "medium": return "medium"; + case "high": + return "high"; case "xhigh": return "xhigh"; case "max": return "max"; - case "high": - default: - return "high"; } } if (reasoning_max_tokens !== undefined) {