diff --git a/docs/concepts/models/index.md b/docs/concepts/models/index.md index d43c31962f..61b68f2a2f 100644 --- a/docs/concepts/models/index.md +++ b/docs/concepts/models/index.md @@ -108,13 +108,13 @@ See the [Model Providers]({{ '/providers/overview/' | relative_url }}) section f Control how much the model "thinks" before responding: -| Provider | Format | Values | Default | -| ---------- | ---------- | -------------------------------------------- | -------------------------------- | -| OpenAI | string | `minimal`, `low`, `medium`, `high`, `xhigh` | `medium` (always-reasoning models only) | -| Anthropic | int | 1024–32768 tokens | off | -| Gemini 2.5 | int | 0 (off), -1 (dynamic), or token count | -1 (dynamic) | -| Gemini 3 | string | `minimal`, `low`, `medium`, `high` | varies | -| All | string/int | `none` or `0` to disable | — | +| Provider | Format | Values | Default | +| ---------- | ---------- | ------------------------------------------------------------------- | -------------------------------- | +| OpenAI | string | `minimal`, `low`, `medium`, `high`, `xhigh` | `medium` (always-reasoning models only) | +| Anthropic | int or str | 1024–32768 tokens, or `adaptive`, `adaptive/`, effort level | off | +| Gemini 2.5 | int | 0 (off), -1 (dynamic), or token count | -1 (dynamic) | +| Gemini 3 | string | `minimal`, `low`, `medium`, `high` | varies | +| All | string/int | `none` or `0` to disable | — | ```yaml models: diff --git a/docs/configuration/models/index.md b/docs/configuration/models/index.md index d3b6093f81..99df872fbb 100644 --- a/docs/configuration/models/index.md +++ b/docs/configuration/models/index.md @@ -138,12 +138,12 @@ models: gpt: provider: openai model: gpt-5 - thinking_budget: low # minimal | low | medium | high | xhigh | max | adaptive/ + thinking_budget: low # minimal | low | medium | high | xhigh ``` ### Anthropic -Uses an integer token budget (1024–32768): +Uses an integer token budget (1024–32768), or — on adaptive-capable models (Opus 4.6+) — `adaptive`, `adaptive/`, or a bare effort level: ```yaml models: @@ -151,6 +151,11 @@ models: provider: anthropic model: claude-sonnet-4-5 thinking_budget: 16384 # must be < max_tokens + + opus: + provider: anthropic + model: claude-opus-4-6 + thinking_budget: adaptive # or adaptive/, or low | medium | high | xhigh | max ``` ### Google Gemini 2.5 @@ -161,7 +166,7 @@ Uses an integer token budget. `0` disables, `-1` lets the model decide: models: gemini: provider: google - model: gemini-3.5-flash + model: gemini-2.5-flash thinking_budget: -1 # dynamic (default) ``` @@ -174,7 +179,7 @@ models: gemini3: provider: google model: gemini-3-flash - thinking_budget: medium # minimal | low | medium | high | xhigh | max | adaptive/ + thinking_budget: medium # minimal | low | medium | high ``` ### Disabling Thinking @@ -185,6 +190,10 @@ Works for all providers: thinking_budget: none # or 0 ``` +Models that always reason (OpenAI o-series, gpt-5, Gemini 3) fall back to the API default and still reason internally. + +See the [Thinking / Reasoning guide]({{ '/guides/thinking/' | relative_url }}) for per-provider details, including AWS Bedrock and Docker Model Runner. + ## Task Budget **Anthropic-only.** @@ -369,7 +378,7 @@ providers: provider: anthropic token_key: MY_ANTHROPIC_KEY max_tokens: 16384 - thinking_budget: high + thinking_budget: 8192 temperature: 0.5 models: @@ -381,7 +390,7 @@ models: claude_fast: provider: my_anthropic model: claude-haiku-4-5 - thinking_budget: low # Overrides provider default + thinking_budget: 1024 # Overrides provider default ``` See [Provider Definitions]({{ '/providers/custom/' | relative_url }}) for the full list of inheritable properties. diff --git a/docs/configuration/overview/index.md b/docs/configuration/overview/index.md index 2460aa0f56..0de16e04bc 100644 --- a/docs/configuration/overview/index.md +++ b/docs/configuration/overview/index.md @@ -412,7 +412,7 @@ providers: provider: anthropic token_key: TEAM_ANTHROPIC_KEY max_tokens: 32768 - thinking_budget: high + thinking_budget: 16384 models: azure_gpt: diff --git a/docs/guides/thinking/index.md b/docs/guides/thinking/index.md index 19239fbb46..f51d2534f3 100644 --- a/docs/guides/thinking/index.md +++ b/docs/guides/thinking/index.md @@ -1,12 +1,12 @@ --- title: "Thinking / Reasoning" -description: "Control how much a model reasons before responding. Works across OpenAI, Anthropic, Google Gemini, and AWS Bedrock." +description: "Control how much a model reasons before responding. Works across OpenAI, Anthropic, Google Gemini, AWS Bedrock, and Docker Model Runner." permalink: /guides/thinking/ --- # Thinking / Reasoning -_Control how much a model reasons before responding. Works across OpenAI, Anthropic, Google Gemini, and AWS Bedrock._ +_Control how much a model reasons before responding. Works across OpenAI, Anthropic, Google Gemini, AWS Bedrock, and Docker Model Runner._ ## What Is Thinking? @@ -22,14 +22,18 @@ docker-agent exposes this through a single `thinking_budget` field on any named ## Quick Reference -| Provider | Format | Values | Default | -| -------------- | ---------- | ------------------------------------------------------------ | ------------ | -| OpenAI | string | `minimal`, `low`, `medium`, `high`, `xhigh`, `none`, `adaptive/` (`max` only via `adaptive/max`) | `medium` | -| Anthropic | int or str | 1024–32768 tokens, or `adaptive`, `low`–`max`, `none` | off | -| Gemini 2.5 | int | `0` (off), `-1` (dynamic), or token count (max 24576 / 32768) | `-1` (dynamic)| -| Gemini 3 | string | `minimal`, `low`, `medium`, `high` | model-dependent | -| AWS Bedrock | int or str | 1024–32768 tokens (`minimal`–`max` mapped to tokens) | off | -| xAI / Mistral | string | `minimal`, `low`, `medium`, `high`, `xhigh`, `none` | off | +| Provider | Format | Values | Default | +| ------------------- | ---------- | --------------------------------------------------------------------------------------- | ------------------ | +| OpenAI | string | `minimal`, `low`, `medium`, `high`, `xhigh`, `none` | `medium` (API default) | +| Anthropic | int or str | 1024–32768 tokens, or `minimal`–`max`, `adaptive`, `adaptive/`, `none` | off | +| Gemini 2.5 | int | `0` (off), `-1` (dynamic), or token count (max 24576 / 32768) | `-1` (dynamic) | +| Gemini 3 | string | `minimal`, `low`, `medium`, `high` | API default (model-dependent) | +| AWS Bedrock | int or str | 1024–32768 tokens (`minimal`–`max` mapped to tokens); `adaptive`, `adaptive/` for Opus 4.6+ (rejected by older Claude models) | off | +| Docker Model Runner | int or str | token count, `minimal`–`max` (mapped to tokens), `adaptive` (unlimited), `none` | engine default | + +String values are case-insensitive. The full set of accepted strings is `none`, `minimal`, `low`, `medium`, `high`, `xhigh`, `max`, `adaptive`, and `adaptive/` — but each provider only honors the subset listed above. Unsupported values either fail at request time (OpenAI) or are mapped/ignored as described per provider below. + +> `thinking_budget` is only applied by the providers listed above. Other OpenAI-compatible providers (xAI, Mistral, Ollama, …) currently ignore it — see [xAI and Mistral](#xai-grok-and-mistral). ## OpenAI @@ -47,33 +51,21 @@ models: | Level | Description | | --------- | -------------------------------------------------------- | -| `none` | Disable thinking (alias for `0`). Minimum output floor still applies. | +| `none` | Don't request extra reasoning (alias for `0`); the API's own default still applies. | | `minimal` | Fastest; lightest reasoning pass. | | `low` | Quick reasoning for straightforward tasks. | | `medium` | Balanced default. | | `high` | More thorough; recommended for complex tasks. | | `xhigh` | Near-maximum effort; slower but most accurate. | +These five effort levels (`minimal`–`xhigh`) are the **only** values accepted for OpenAI. Token counts, `max`, `adaptive`, and `adaptive/` are rejected with a configuration error at request time. Older models (o1, o3-mini) only accept `low`/`medium`/`high` — sending an unsupported level returns an API error. +
Tokens and max_tokens
-

OpenAI reasoning models always reason internally — even with thinking_budget: none there are hidden reasoning tokens that count against max_tokens. docker-agent automatically raises the output-token floor so hidden reasoning cannot starve visible text output.

+

OpenAI reasoning models always reason internally — even with thinking_budget: none there are hidden reasoning tokens that count against max_tokens. docker-agent automatically raises the output-token floor for its internal low-effort calls (e.g. title generation) so hidden reasoning cannot starve visible text output.

-### Adaptive thinking (OpenAI) - -Use `adaptive/` to let the model decide when to apply extended reasoning and scale effort dynamically: - -```yaml -models: - gpt-adaptive: - provider: openai - model: gpt-5-mini - thinking_budget: adaptive/medium # adaptive/low | adaptive/medium | adaptive/high | adaptive/xhigh | adaptive/max -``` - -> `max` is only valid inside the `adaptive/` prefix — `thinking_budget: max` (bare) is not accepted by OpenAI. - ## Anthropic Anthropic Claude supports two thinking modes: a **token budget** (older models) and **adaptive / effort-based** thinking (newer models). @@ -94,30 +86,43 @@ docker-agent auto-adjusts `max_tokens` when you set a thinking budget but leave ### Adaptive thinking (Claude Opus 4.6+) -Newer Claude models support adaptive thinking, where the model decides how much to think. Use `adaptive` or pair it with an effort level: +Newer Claude models support adaptive thinking, where the model decides how much to think. **Claude Opus 4.6, 4.7 and 4.8 only support adaptive thinking** — they reject token-based budgets. Use `adaptive`, `adaptive/`, or a bare effort level — on Anthropic, a bare effort level like `high` is shorthand for adaptive thinking at that effort: ```yaml models: claude-adaptive: provider: anthropic model: claude-opus-4-6 - thinking_budget: adaptive # model decides effort + thinking_budget: adaptive # model decides effort (defaults to high) claude-adaptive-low: provider: anthropic model: claude-opus-4-6 - thinking_budget: low # adaptive with low effort: low | medium | high | max + thinking_budget: low # same as adaptive/low + + claude-adaptive-max: + provider: anthropic + model: claude-opus-4-6 + thinking_budget: adaptive/max # adaptive/low | adaptive/medium | adaptive/high | adaptive/xhigh | adaptive/max ``` **Adaptive effort levels:** | Level | Description | | --------- | ------------------------------------------------- | +| `minimal` | Treated as `low` (bare form only). | | `low` | Minimal thinking; fastest adaptive mode. | | `medium` | Moderate effort. | | `high` | Thorough reasoning; default for `adaptive`. | +| `xhigh` | Very high effort (newer models, e.g. Opus 4.7+). | | `max` | Maximum effort. | +
+
Effort strings require adaptive-capable models +
+

Every string effort value on Anthropic is sent as adaptive thinking (output_config.effort), which only newer Claude models (Opus 4.6+) accept. For older models like Sonnet 4.5, use an integer token budget instead. Conversely, models that only support adaptive thinking (Opus 4.6, 4.7, 4.8) automatically have token budgets coerced to adaptive (a warning is logged).

+
+ ### Disabling thinking ```yaml @@ -223,7 +228,7 @@ models: ## AWS Bedrock (Claude) -Bedrock Claude uses a token budget like Anthropic, but only supports integer token values. String effort levels (`minimal`–`max`) are mapped automatically: +Bedrock Claude uses a token budget like Anthropic. String effort levels (`minimal`–`max`) are mapped automatically: | Effort level | Token budget | | ------------ | ------------ | @@ -251,30 +256,47 @@ models: # interleaved_thinking is auto-enabled when thinking_budget is set ``` +**Claude Opus 4.6+ on Bedrock requires adaptive thinking** — these models reject `thinking.type=enabled` (token budgets). Configure them with `adaptive` or `adaptive/`; docker-agent auto-coerces token budgets and effort levels on these models with a warning: + +```yaml +models: + bedrock-opus-adaptive: + provider: amazon-bedrock + model: global.anthropic.claude-opus-4-8 + thinking_budget: adaptive/high + provider_opts: + region: us-east-1 +``` +
Bedrock thinking requirements
-

Bedrock Claude requires thinking_budget to be ≥ 1024 and less than max_tokens. docker-agent logs a warning and ignores the budget if either condition is violated. Interleaved thinking requires the interleaved-thinking-2025-05-14 beta header, which docker-agent adds automatically; it is auto-enabled whenever a thinking budget is set on a Bedrock-hosted Claude model.

+

Bedrock Claude requires token-based thinking_budget values to be ≥ 1024 and less than max_tokens. docker-agent logs a warning and ignores the budget if either condition is violated. Interleaved thinking requires the interleaved-thinking-2025-05-14 beta header, which docker-agent adds automatically; it is auto-enabled whenever a token thinking budget is set on a Bedrock-hosted Claude model (adaptive thinking interleaves on its own).

-## xAI (Grok) and Mistral +## Docker Model Runner (local models) -Both providers use the OpenAI-compatible API and accept the same effort strings: +For local models, `thinking_budget` is forwarded to the inference engine. Both token counts and effort strings work; effort strings map to the same token scale as Bedrock (`minimal`=1024 … `xhigh`/`max`=32768), and `adaptive` means unlimited: ```yaml models: - grok-thinker: - provider: xai - model: grok-3 - thinking_budget: high # minimal | low | medium | high | xhigh | none - - mistral-thinker: - provider: mistral - model: mistral-large-latest - thinking_budget: medium # minimal | low | medium | high | xhigh | none + local: + provider: dmr + model: ai/qwen3 + thinking_budget: medium # llama.cpp: reasoning-budget=8192; vLLM: thinking_token_budget=8192 ``` -Thinking is **off by default** for these providers. Set `thinking_budget` explicitly to enable it. +- **llama.cpp**: sent as `reasoning-budget` at model-configure time. +- **vLLM**: sent as `thinking_token_budget` on each request. +- **MLX / SGLang**: no reasoning-budget knob; the value is silently ignored. + +See the [Docker Model Runner provider page]({{ '/providers/dmr/' | relative_url }}) for details. + +## xAI (Grok) and Mistral + +xAI and Mistral run through docker-agent's OpenAI-compatible client, but the `reasoning_effort` parameter is only sent for OpenAI reasoning model names (o-series, gpt-5). **Setting `thinking_budget` on Grok or Mistral models currently has no effect** — the value is accepted by config validation but never sent to the API. + +Grok and Mistral reasoning models (e.g. `grok-3-mini`, `magistral`) manage reasoning on their own; for non-reasoning models, consider the [think tool]({{ '/tools/think/' | relative_url }}) instead. ## Disabling Thinking @@ -293,6 +315,8 @@ models: thinking_budget: 0 ``` +`none` and `0` clear docker-agent's thinking configuration — no thinking parameter is sent. Models that always reason (OpenAI o-series, gpt-5, Gemini 3) then fall back to the API's default behavior and still reason internally; only models with optional thinking (Gemini 2.5, Claude, local models) are fully disabled. + ## Choosing an Effort Level | Task complexity | Recommended level | @@ -321,18 +345,18 @@ Define a provider with a default `thinking_budget` and all models that reference providers: deep-anthropic: provider: anthropic - thinking_budget: high + thinking_budget: adaptive/high max_tokens: 32768 models: claude-smart: provider: deep-anthropic - model: claude-sonnet-4-5 # inherits thinking_budget: high + model: claude-opus-4-6 # inherits thinking_budget: adaptive/high claude-faster: provider: deep-anthropic - model: claude-haiku-4-5 - thinking_budget: low # overrides to low + model: claude-opus-4-6 + thinking_budget: low # overrides to adaptive/low ``` ## Full Example diff --git a/docs/providers/anthropic/index.md b/docs/providers/anthropic/index.md index 3945275c90..c018d1e14c 100644 --- a/docs/providers/anthropic/index.md +++ b/docs/providers/anthropic/index.md @@ -104,7 +104,9 @@ models: ## Thinking Budget -Anthropic uses integer token budgets (1024–32768). Thinking is off unless you set `thinking_budget`; when set, interleaved thinking is auto-enabled: +Anthropic accepts either an integer token budget or a string effort value. Thinking is off unless you set `thinking_budget`; when set, interleaved thinking is auto-enabled. + +**Token budget** (1024–32768; works on all extended-thinking Claude models): ```yaml models: @@ -114,6 +116,23 @@ models: thinking_budget: 16384 # must be < max_tokens ``` +**Adaptive / effort-based** (Claude Opus 4.6+ only — every string value is sent as adaptive thinking via `output_config.effort`): + +```yaml +models: + opus-adaptive: + provider: anthropic + model: claude-opus-4-6 + thinking_budget: adaptive # model decides effort (defaults to high) + + opus-effort: + provider: anthropic + model: claude-opus-4-6 + thinking_budget: high # low | medium | high | xhigh | max (same as adaptive/) +``` + +On models that reject token-based thinking (Opus 4.6, 4.7, 4.8), an integer budget is automatically coerced to `adaptive` with a logged warning. See the [Thinking / Reasoning guide]({{ '/guides/thinking/' | relative_url }}) for the full cross-provider reference. + ## Interleaved Thinking Auto-enabled whenever a thinking budget is configured on a Claude model. Allows tool calls during model reasoning for more integrated problem-solving: diff --git a/docs/providers/bedrock/index.md b/docs/providers/bedrock/index.md index 95f093d3ae..59df9865c0 100644 --- a/docs/providers/bedrock/index.md +++ b/docs/providers/bedrock/index.md @@ -126,6 +126,22 @@ models: `thinking_budget` must be ≥ 1024 and less than `max_tokens`. Values outside this range are logged as a warning and ignored. +### Adaptive thinking (Opus 4.6+) + +Newer Claude Opus models (4.6, 4.7, 4.8) **reject token-based thinking** — Bedrock returns a `ValidationException` asking for `thinking.type=adaptive`. For these models, use adaptive thinking: + +```yaml +models: + bedrock-opus-adaptive: + provider: amazon-bedrock + model: global.anthropic.claude-opus-4-8 + thinking_budget: adaptive/high # adaptive | adaptive/low | adaptive/medium | adaptive/high | adaptive/xhigh | adaptive/max + provider_opts: + region: us-east-1 +``` + +docker-agent recognizes these models (including Bedrock-style IDs) and transparently coerces a configured token budget or effort level to adaptive thinking, logging a warning — so `thinking_budget: 32768` on Opus 4.8 won't fail, but `adaptive` or `adaptive/` is the recommended configuration. On older models that still use token-based thinking (e.g. Sonnet 4.5), `adaptive` is forwarded as-is and rejected by Bedrock — use a token count or effort level there instead. +
Temperature and top_p
diff --git a/docs/providers/custom/index.md b/docs/providers/custom/index.md index a4e6360b87..a409511f1f 100644 --- a/docs/providers/custom/index.md +++ b/docs/providers/custom/index.md @@ -53,18 +53,18 @@ providers: provider: anthropic token_key: MY_ANTHROPIC_KEY max_tokens: 16384 - thinking_budget: high + thinking_budget: 8192 models: claude_smart: provider: my_anthropic model: claude-sonnet-4-5 - # Inherits max_tokens: 16384, thinking_budget: high + # Inherits max_tokens: 16384, thinking_budget: 8192 claude_fast: provider: my_anthropic model: claude-haiku-4-5 - thinking_budget: low # Overrides provider default + thinking_budget: 1024 # Overrides provider default agents: root: diff --git a/docs/providers/mistral/index.md b/docs/providers/mistral/index.md index 56f27c763a..fa4f74f1b5 100644 --- a/docs/providers/mistral/index.md +++ b/docs/providers/mistral/index.md @@ -74,25 +74,9 @@ When you run `docker agent run` without specifying a config, docker-agent automa ## Extended Thinking -Mistral models support thinking mode through the OpenAI-compatible API. Thinking is **not enabled by default** — you must set `thinking_budget` explicitly on the model: +docker-agent's `thinking_budget` field is **not applied** to Mistral models: the underlying OpenAI-compatible client only sends `reasoning_effort` for OpenAI reasoning model names (o-series, gpt-5). Setting `thinking_budget` on a Mistral model passes config validation but has no effect on the request. -```yaml -models: - mistral: - provider: mistral - model: mistral-large-latest - thinking_budget: high # minimal, low, medium, high, xhigh, or none -``` - -To disable thinking (the default): - -```yaml -models: - mistral: - provider: mistral - model: mistral-large-latest - thinking_budget: none -``` +Mistral reasoning models (e.g. `magistral`) reason on their own without configuration. For non-reasoning models, use the [think tool]({{ '/tools/think/' | relative_url }}) instead. ## How It Works diff --git a/docs/providers/openai/index.md b/docs/providers/openai/index.md index 407979045f..122f99fcb9 100644 --- a/docs/providers/openai/index.md +++ b/docs/providers/openai/index.md @@ -63,33 +63,21 @@ models: | Level | Description | | --------- | -------------------------------------------------------- | -| `none` | Disable thinking (a minimum output floor still applies). | +| `none` | Don't request extra reasoning (alias for `0`); the API's own default still applies. | | `minimal` | Fastest; lightest reasoning pass. | | `low` | Quick reasoning for straightforward tasks. | | `medium` | Balanced default. | | `high` | More thorough; recommended for complex tasks. | | `xhigh` | Near-maximum effort; slower but most accurate. | +These are the **only** values OpenAI accepts — token counts, `max`, `adaptive`, and `adaptive/` are rejected with a configuration error at request time. Older models (o1, o3-mini) only accept `low`/`medium`/`high`. +
Hidden reasoning tokens
-

OpenAI reasoning models always produce hidden reasoning tokens that count against max_tokens — even with thinking_budget: none. docker-agent automatically raises the output-token floor so reasoning cannot starve visible text output.

+

OpenAI reasoning models always produce hidden reasoning tokens that count against max_tokens — even with thinking_budget: none. docker-agent automatically raises the output-token floor for its internal low-effort calls so reasoning cannot starve visible text output.

-### Adaptive Thinking - -Use `adaptive/` to let the model scale effort dynamically based on task complexity: - -```yaml -models: - gpt-adaptive: - provider: openai - model: gpt-5-mini - thinking_budget: adaptive/medium # adaptive/low | adaptive/medium | adaptive/high | adaptive/xhigh | adaptive/max -``` - -> `max` is only valid inside the `adaptive/` prefix — `thinking_budget: max` (bare) is not accepted by OpenAI. - See the [Thinking / Reasoning guide]({{ '/guides/thinking/' | relative_url }}) for a cross-provider overview.
diff --git a/docs/providers/xai/index.md b/docs/providers/xai/index.md index 79c1f596ed..f6f139a318 100644 --- a/docs/providers/xai/index.md +++ b/docs/providers/xai/index.md @@ -69,15 +69,9 @@ Check the [xAI documentation](https://docs.x.ai/docs) for the latest available m ## Extended Thinking -Grok models support thinking mode through the OpenAI-compatible API: +docker-agent's `thinking_budget` field is **not applied** to xAI models: the underlying OpenAI-compatible client only sends `reasoning_effort` for OpenAI reasoning model names (o-series, gpt-5). Setting `thinking_budget` on a Grok model passes config validation but has no effect on the request. -```yaml -models: - grok: - provider: xai - model: grok-3 - thinking_budget: high # minimal, low, medium, high, xhigh, max, none, or adaptive/ -``` +Grok reasoning models (e.g. `grok-3-mini`) reason on their own without configuration. For non-reasoning models, use the [think tool]({{ '/tools/think/' | relative_url }}) instead. ## How It Works diff --git a/examples/thinking_budget.yaml b/examples/thinking_budget.yaml index e0a96da1bf..5c665abddf 100644 --- a/examples/thinking_budget.yaml +++ b/examples/thinking_budget.yaml @@ -17,7 +17,7 @@ models: gpt-5-mini-min: provider: openai model: gpt-5-mini - thinking_budget: minimal # <- openai supports "minimal", "low", "medium", "high" + thinking_budget: minimal # <- openai supports "minimal", "low", "medium", "high", "xhigh" gpt-5-mini-high: provider: openai @@ -44,7 +44,7 @@ models: claude-opus-4-6-low: provider: anthropic model: claude-opus-4-6 - thinking_budget: low # <- adaptive thinking with low effort: "low", "medium", "high", "max" + thinking_budget: low # <- adaptive thinking with low effort: "low", "medium", "high", "xhigh", "max" (same as adaptive/) claude-opus-4-7-summarized: provider: anthropic @@ -72,3 +72,8 @@ models: provider: google model: gemini-2.5-flash thinking_budget: 24576 # <- google's maximum thinking budget for all models except Gemini 2.5 Pro (max 32768) + + bedrock-opus-4-8-adaptive: + provider: amazon-bedrock + model: global.anthropic.claude-opus-4-8 + thinking_budget: adaptive/high # <- Opus 4.6+ reject token budgets on Bedrock too; token values are auto-converted to adaptive diff --git a/pkg/model/provider/bedrock/client.go b/pkg/model/provider/bedrock/client.go index fb6bcce1e3..64c3678e7a 100644 --- a/pkg/model/provider/bedrock/client.go +++ b/pkg/model/provider/bedrock/client.go @@ -17,11 +17,13 @@ import ( "github.com/docker/docker-agent/pkg/chat" "github.com/docker/docker-agent/pkg/config/latest" + "github.com/docker/docker-agent/pkg/effort" "github.com/docker/docker-agent/pkg/environment" "github.com/docker/docker-agent/pkg/httpclient" "github.com/docker/docker-agent/pkg/model/provider/base" "github.com/docker/docker-agent/pkg/model/provider/options" "github.com/docker/docker-agent/pkg/model/provider/providerutil" + "github.com/docker/docker-agent/pkg/modelinfo" "github.com/docker/docker-agent/pkg/modelsdev" "github.com/docker/docker-agent/pkg/tools" ) @@ -323,6 +325,9 @@ func (c *Client) isThinkingEnabled() bool { if c.ModelConfig.ThinkingBudget == nil { return false } + if _, ok := c.adaptiveThinkingEffort(); ok { + return true + } tokens := c.ModelConfig.ThinkingBudget.Tokens if t, ok := c.ModelConfig.ThinkingBudget.EffortTokens(); ok { tokens = t @@ -336,6 +341,36 @@ func (c *Client) isThinkingEnabled() bool { return true } +// adaptiveThinkingEffort returns the `output_config.effort` value when the +// request must use adaptive thinking (`thinking.type=adaptive`) instead of a +// token budget. This happens when the user explicitly configured "adaptive" +// (or "adaptive/"), or when the model rejects token-based thinking +// (Claude Opus 4.6+) and the configured effort level or token budget is +// transparently coerced. +// +// It has no side effects so it can be shared by isThinkingEnabled and +// buildAdditionalModelRequestFields. +func (c *Client) adaptiveThinkingEffort() (string, bool) { + budget := c.ModelConfig.ThinkingBudget + if budget == nil || budget.IsDisabled() { + return "", false + } + if e, ok := budget.AdaptiveEffort(); ok { + return e, true + } + if !modelinfo.RejectsTokenThinking(c.ModelConfig.Model) { + return "", false + } + if l, ok := budget.EffortLevel(); ok { + return effort.ForAnthropic(l) + } + if budget.Tokens > 0 { + // Coerced token budget: adaptive's default effort. + return "high", true + } + return "", false +} + func (c *Client) promptCachingEnabled() bool { if getProviderOpt[bool](c.ModelConfig.ProviderOpts, "disable_prompt_caching") { return false @@ -356,36 +391,57 @@ func (c *Client) buildAdditionalModelRequestFields() document.Interface { // Configure thinking budget if present and valid if budget := c.ModelConfig.ThinkingBudget; budget != nil { - tokens := budget.Tokens - if t, ok := budget.EffortTokens(); ok { - tokens = t - } - - valid := tokens > 0 - if valid && tokens < 1024 { - slog.Warn("Bedrock thinking_budget below minimum (1024), ignoring", "tokens", tokens) - valid = false - } - if valid && c.ModelConfig.MaxTokens != nil && tokens >= int(*c.ModelConfig.MaxTokens) { - slog.Warn("Bedrock thinking_budget must be less than max_tokens, ignoring", - "thinking_budget", tokens, - "max_tokens", *c.ModelConfig.MaxTokens) - valid = false - } + if effortStr, ok := c.adaptiveThinkingEffort(); ok { + switch { + case budget.Tokens > 0: + slog.Warn("Bedrock: model rejects token-based thinking budgets; switching to adaptive thinking", + "model", c.ModelConfig.Model, + "thinking_budget_tokens", budget.Tokens, + "effort", effortStr) + case !budget.IsAdaptive(): + slog.Debug("Bedrock: model rejects token-based thinking; mapping effort level to adaptive thinking", + "model", c.ModelConfig.Model, + "thinking_budget", budget.Effort, + "effort", effortStr) + case !modelinfo.RejectsTokenThinking(c.ModelConfig.Model): + slog.Warn("Bedrock: adaptive thinking is only supported by Claude Opus 4.6+; the request may be rejected", + "model", c.ModelConfig.Model) + } + slog.Debug("Bedrock request using adaptive thinking", "effort", effortStr) + fields["thinking"] = map[string]any{"type": "adaptive"} + fields["output_config"] = map[string]any{"effort": effortStr} + } else { + tokens := budget.Tokens + if t, ok := budget.EffortTokens(); ok { + tokens = t + } - if valid { - slog.Debug("Bedrock request using thinking_budget", "budget_tokens", tokens) - fields["thinking"] = map[string]any{ - "type": "enabled", - "budget_tokens": tokens, + valid := tokens > 0 + if valid && tokens < 1024 { + slog.Warn("Bedrock thinking_budget below minimum (1024), ignoring", "tokens", tokens) + valid = false + } + if valid && c.ModelConfig.MaxTokens != nil && tokens >= int(*c.ModelConfig.MaxTokens) { + slog.Warn("Bedrock thinking_budget must be less than max_tokens, ignoring", + "thinking_budget", tokens, + "max_tokens", *c.ModelConfig.MaxTokens) + valid = false } - if c.interleavedThinkingEnabled() { - fields["anthropic_beta"] = []string{"interleaved-thinking-2025-05-14"} - slog.Debug("Bedrock request using interleaved thinking beta") - } else { - slog.Warn("Bedrock thinking_budget is set but interleaved_thinking is explicitly disabled; " + - "the anthropic_beta header will not be sent, which may cause the thinking budget to be ignored") + if valid { + slog.Debug("Bedrock request using thinking_budget", "budget_tokens", tokens) + fields["thinking"] = map[string]any{ + "type": "enabled", + "budget_tokens": tokens, + } + + if c.interleavedThinkingEnabled() { + fields["anthropic_beta"] = []string{"interleaved-thinking-2025-05-14"} + slog.Debug("Bedrock request using interleaved thinking beta") + } else { + slog.Warn("Bedrock thinking_budget is set but interleaved_thinking is explicitly disabled; " + + "the anthropic_beta header will not be sent, which may cause the thinking budget to be ignored") + } } } } diff --git a/pkg/model/provider/bedrock/client_test.go b/pkg/model/provider/bedrock/client_test.go index fda3fc3977..4e2d6daa89 100644 --- a/pkg/model/provider/bedrock/client_test.go +++ b/pkg/model/provider/bedrock/client_test.go @@ -2,10 +2,12 @@ package bedrock import ( "encoding/base64" + "encoding/json" "net/http" "net/http/httptest" "testing" + "github.com/aws/aws-sdk-go-v2/service/bedrockruntime/document" "github.com/aws/aws-sdk-go-v2/service/bedrockruntime/types" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -727,6 +729,152 @@ func TestBuildAdditionalModelRequestFields_NoMaxTokensSet(t *testing.T) { require.NotNil(t, result, "expected document when MaxTokens is nil") } +// decodeRequestFields unmarshals a lazy smithy document back into a map for +// assertions on its contents. +func decodeRequestFields(t *testing.T, doc document.Interface) map[string]any { + t.Helper() + raw, err := doc.MarshalSmithyDocument() + require.NoError(t, err) + var fields map[string]any + require.NoError(t, json.Unmarshal(raw, &fields)) + return fields +} + +func bedrockThinkingClient(model string, budget *latest.ThinkingBudget) *Client { + maxTokens := int64(64000) + return &Client{ + Config: base.Config{ + ModelConfig: latest.ModelConfig{ + Provider: "amazon-bedrock", + Model: model, + MaxTokens: &maxTokens, + ThinkingBudget: budget, + }, + }, + } +} + +func TestBuildAdditionalModelRequestFields_Adaptive(t *testing.T) { + t.Parallel() + + cases := []struct { + name string + model string + budget latest.ThinkingBudget + wantEffort string + }{ + { + name: "explicit adaptive defaults to high", + model: "global.anthropic.claude-opus-4-8-20260601-v1:0", + budget: latest.ThinkingBudget{Effort: "adaptive"}, + wantEffort: "high", + }, + { + name: "adaptive with explicit effort", + model: "global.anthropic.claude-opus-4-8-20260601-v1:0", + budget: latest.ThinkingBudget{Effort: "adaptive/low"}, + wantEffort: "low", + }, + { + name: "token budget coerced on adaptive-only model", + model: "global.anthropic.claude-opus-4-8-20260601-v1:0", + budget: latest.ThinkingBudget{Tokens: 32768}, + wantEffort: "high", + }, + { + name: "effort level coerced on adaptive-only model", + model: "anthropic.claude-opus-4-6-v1:0", + budget: latest.ThinkingBudget{Effort: "medium"}, + wantEffort: "medium", + }, + { + name: "minimal maps to low on adaptive-only model", + model: "global.anthropic.claude-opus-4-8", + budget: latest.ThinkingBudget{Effort: "minimal"}, + wantEffort: "low", + }, + { + // Explicit adaptive is forwarded as-is even on models that still + // use token-based thinking; Bedrock rejects it with a clear API + // error instead of docker-agent silently dropping it. + name: "explicit adaptive forwarded on token-thinking model", + model: "global.anthropic.claude-sonnet-4-5-20250929-v1:0", + budget: latest.ThinkingBudget{Effort: "adaptive"}, + wantEffort: "high", + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + client := bedrockThinkingClient(tc.model, &tc.budget) + + result := client.buildAdditionalModelRequestFields() + require.NotNil(t, result) + + fields := decodeRequestFields(t, result) + assert.Equal(t, map[string]any{"type": "adaptive"}, fields["thinking"]) + assert.Equal(t, map[string]any{"effort": tc.wantEffort}, fields["output_config"]) + assert.NotContains(t, fields, "anthropic_beta", "adaptive thinking needs no interleaved beta") + assert.True(t, client.isThinkingEnabled(), "adaptive thinking must suppress temperature/top_p") + }) + } +} + +func TestBuildAdditionalModelRequestFields_EffortMapsToTokens(t *testing.T) { + t.Parallel() + + // On models that still support token-based thinking, effort levels map + // to token budgets instead of adaptive thinking. + client := bedrockThinkingClient( + "global.anthropic.claude-sonnet-4-5-20250929-v1:0", + &latest.ThinkingBudget{Effort: "high"}, + ) + + result := client.buildAdditionalModelRequestFields() + require.NotNil(t, result) + + fields := decodeRequestFields(t, result) + assert.Equal(t, map[string]any{"type": "enabled", "budget_tokens": float64(16384)}, fields["thinking"]) + assert.NotContains(t, fields, "output_config") +} + +func TestBuildAdditionalModelRequestFields_DisabledOnAdaptiveOnlyModel(t *testing.T) { + t.Parallel() + + // Disabled budgets must stay disabled, never coerced to adaptive. + for name, budget := range map[string]latest.ThinkingBudget{ + "zero tokens": {Tokens: 0}, + "none": {Effort: "none"}, + } { + t.Run(name, func(t *testing.T) { + t.Parallel() + + client := bedrockThinkingClient("global.anthropic.claude-opus-4-8", &budget) + + assert.Nil(t, client.buildAdditionalModelRequestFields()) + assert.False(t, client.isThinkingEnabled()) + }) + } +} + +func TestBuildAdditionalModelRequestFields_AdaptiveWithInterleavedThinking(t *testing.T) { + t.Parallel() + + // interleaved_thinking is auto-enabled for Claude thinking budgets, but + // the token-thinking beta header must not leak into adaptive requests. + client := bedrockThinkingClient("global.anthropic.claude-opus-4-8", &latest.ThinkingBudget{Effort: "adaptive"}) + client.ModelConfig.ProviderOpts = map[string]any{"interleaved_thinking": true} + + result := client.buildAdditionalModelRequestFields() + require.NotNil(t, result) + + fields := decodeRequestFields(t, result) + assert.Equal(t, map[string]any{"type": "adaptive"}, fields["thinking"]) + assert.NotContains(t, fields, "anthropic_beta") +} + func TestBuildInferenceConfig_DisablesTempTopPWhenThinkingEnabled(t *testing.T) { t.Parallel() diff --git a/pkg/modelinfo/modelinfo.go b/pkg/modelinfo/modelinfo.go index 827657c3b4..194e256213 100644 --- a/pkg/modelinfo/modelinfo.go +++ b/pkg/modelinfo/modelinfo.go @@ -87,14 +87,19 @@ func AlwaysReasons(modelID string) bool { // `thinking.type=enabled` (token-based extended thinking) and instead requires // `thinking.type=adaptive`. // -// Currently Claude Opus 4.6 and 4.7 (and dated variants like -// claude-opus-4-7-20251101). For these models the agent transparently -// switches a token-based budget to adaptive thinking. +// Currently Claude Opus 4.6, 4.7 and 4.8 (and dated variants like +// claude-opus-4-7-20251101). Bedrock-style identifiers such as +// "global.anthropic.claude-opus-4-8" are recognised too. +// For these models the agent transparently switches a token-based budget to +// adaptive thinking. // // See https://platform.claude.com/docs/en/build-with-claude/adaptive-thinking func RejectsTokenThinking(modelID string) bool { m := normalize(modelID) - for _, prefix := range []string{"claude-opus-4-6", "claude-opus-4-7"} { + if bare, ok := bedrockClaudeModelName(m); ok { + m = bare + } + for _, prefix := range []string{"claude-opus-4-6", "claude-opus-4-7", "claude-opus-4-8"} { if m == prefix || strings.HasPrefix(m, prefix+"-") { return true } @@ -136,15 +141,26 @@ func UsesThinkingLevel(modelID string) bool { // Prefer [IsClaude] for cross-provider checks: this helper exists so callers // in the Bedrock path can avoid touching the models.dev store. func IsBedrockClaudeID(modelID string) bool { - m := normalize(modelID) - if strings.HasPrefix(m, "anthropic.claude-") { - return true + _, ok := bedrockClaudeModelName(normalize(modelID)) + return ok +} + +// bedrockClaudeModelName returns the bare Claude model name for a Bedrock-style +// identifier ("anthropic.claude-...", optionally preceded by a single regional +// inference profile such as "global." or "us."). The input must already be +// normalized. Returns ("", false) for non-Bedrock IDs; ARN-style identifiers +// are not handled. +func bedrockClaudeModelName(m string) (string, bool) { + if bare, ok := strings.CutPrefix(m, "anthropic."); ok && strings.HasPrefix(bare, "claude-") { + return bare, true } // Strip a single regional prefix (us., eu., apac., global., ...). if i := strings.IndexByte(m, '.'); i > 0 { - return strings.HasPrefix(m[i+1:], "anthropic.claude-") + if bare, ok := strings.CutPrefix(m[i+1:], "anthropic."); ok && strings.HasPrefix(bare, "claude-") { + return bare, true + } } - return false + return "", false } // IsClaude reports whether a model belongs to the Claude family, regardless diff --git a/pkg/modelinfo/modelinfo_test.go b/pkg/modelinfo/modelinfo_test.go index 5984591d48..428a19ffae 100644 --- a/pkg/modelinfo/modelinfo_test.go +++ b/pkg/modelinfo/modelinfo_test.go @@ -141,12 +141,18 @@ func TestRejectsTokenThinking(t *testing.T) { {" claude-opus-4-6 ", true}, // trims whitespace {"claude-opus-4-5", false}, {"claude-opus-4-5-20251015", false}, - {"claude-opus-4-8", false}, + {"claude-opus-4-8", true}, + {"claude-opus-4-8-20260601", true}, + {"anthropic.claude-opus-4-8-20260601-v1:0", true}, // Bedrock ID + {"global.anthropic.claude-opus-4-8-20260601-v1:0", true}, // Bedrock inference profile + {"us.anthropic.claude-opus-4-6-v1:0", true}, // regional profile + {"global.anthropic.claude-sonnet-4-5-20250929-v1:0", false}, // Bedrock Sonnet still token-based {"claude-sonnet-4-7", false}, {"claude-sonnet-4-5", false}, {"claude-haiku-4-5", false}, {"claude-opus-4-60", false}, // must not match {"claude-opus-4-70", false}, + {"claude-opus-4-80", false}, {"", false}, } for _, tc := range cases {