Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion agent-schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -1756,7 +1756,7 @@
"additionalProperties": true
}
},
"description": "Provider-specific options. Anthropic: thinking_prefix_mismatch (error or drop_block; defaults to drop_block on Fable/Mythos 5.1), strict_tools (true for eligible schemas or a tool-name array requiring strict mode), cache_diagnostics (boolean; Claude API only), native_compaction (boolean; synchronous whole-history signed compaction on Claude API), cache_preserving_updates (boolean; native system/tool/effort changes with turn-scoped reminders on supported models), thinking_display: updates (progress updates beta on Fable 5+ and Mythos 5.1). Sampling parameters: top_k (integer, supported by anthropic, google, amazon-bedrock, and custom OpenAI-compatible providers like vLLM/Ollama), repetition_penalty (float, forwarded to custom OpenAI-compatible providers), min_p (float, forwarded to custom providers), seed (integer, forwarded to OpenAI). Lifecycle: unload_api (string) overrides the unload endpoint inherited from the provider config (relative path resolved against base_url's scheme+host, or an absolute URL); used by the runtime's `unload` on_agent_switch builtin hook to release model resources between agent switches. Infrastructure options: http_headers (map of string to string, adds custom HTTP headers to every request; used for OpenAI-compatible providers like github-copilot which requires Copilot-Integration-Id). dmr: runtime_flags; supports_images (boolean) and supports_pdf (boolean) declare which document attachment types the local model accepts. DMR-hosted models are not in the models.dev catalog, so attachment capabilities cannot be detected automatically and default to text-only; set supports_images: true for a vision model so image attachments are forwarded instead of silently dropped. anthropic/amazon-bedrock (Claude): interleaved_thinking (boolean, default true). anthropic: thinking_display ('summarized', 'omitted', 'updates', or 'display') controls whether thinking blocks are returned in responses when thinking is enabled. Claude Opus 4.7 hides thinking by default ('omitted'); set thinking_display: summarized to receive thinking blocks. 'display' (full thinking blocks) is only accepted by pre-4.6 token-thinking models (e.g. Sonnet 4.5, Haiku 4.5); models from the adaptive-thinking generation onward (Opus/Sonnet 4.6+, Sonnet 5, Fable 5) reject it, and docker-agent fails fast with a configuration error (fallbacks models are validated too); fallbacks (array of model ID strings, in priority order; enables the server-side-fallback beta so requests refused by safety classifiers, e.g. on Claude Fable 5, are retried with the listed models in a single round trip). OpenAI-compatible APIs: service_tier (string, forwarded unchanged to Chat Completions and Responses, including WebSocket and reranking; 'fast' or 'priority' enables Fast mode at premium pricing on supported models; omitted or empty leaves the API default, non-string values are ignored). openai/chatgpt: preserve_reasoning (boolean, default false) preserves ordered Responses output and encrypted reasoning for stateless continuation; native_tool_search also enables output replay. openai: cache_diagnostics (boolean, default false) logs Responses prompt-cache comparison results with --debug on GPT-5.6+; comparisons are scoped to the current session and resolved model; ignored for chatgpt and custom base_url endpoints. transport ('sse' or 'websocket') to choose between SSE and WebSocket streaming for the Responses API; native_tool_search (boolean, default false; also enables ordered output replay) declares the agent's deferred tools with defer_loading and a server-executed tool_search tool so OpenAI's hosted tool search discovers them (only openai models supporting deferred tools, gpt-5.4+; ignored on chatgpt, OpenAI-compatible endpoints including a custom base_url, and Chat Completions requests). openai/anthropic/google: rerank_prompt (string) to fully override the system prompt used for RAG reranking (advanced - prefer using results.reranking.criteria for domain-specific guidance). Google: google_search (boolean) enables Google Search grounding, google_maps (boolean) enables Google Maps grounding, code_execution (boolean) enables server-side code execution, url_context (boolean) enables reading public URLs during generation, output_dimensionality (positive integer) truncates Gemini embedding vectors to that size when the model is used as a RAG embedding_model; service_tier (string, forwarded unchanged as the top-level serviceTier of every generateContent request, including reranking, title generation, and compaction, on the Gemini API, Vertex AI, and the Docker models gateway; 'flex' for discounted, queued processing that may wait up to 15 minutes before the first byte, 'standard' for the default tier, 'priority' for premium processing; omitted or empty leaves the API default, non-string values are ignored; cost estimates are not tier-adjusted, set the model's cost override).",
"description": "Provider-specific options. Anthropic: thinking_prefix_mismatch (error or drop_block; defaults to drop_block on Fable/Mythos 5.1), strict_tools (true for eligible schemas or a tool-name array requiring strict mode), cache_diagnostics (boolean; Claude API only), native_compaction (boolean; synchronous whole-history signed compaction on Claude API), cache_preserving_updates (boolean; native system/tool/effort changes with turn-scoped reminders on supported models), thinking_display: updates (progress updates beta on Fable 5+ and Mythos 5.1). Sampling parameters: top_k (integer, supported by anthropic, google, amazon-bedrock, and custom OpenAI-compatible providers like vLLM/Ollama), repetition_penalty (float, forwarded to custom OpenAI-compatible providers), min_p (float, forwarded to custom providers), seed (integer, forwarded to OpenAI). Lifecycle: unload_api (string) overrides the unload endpoint inherited from the provider config (relative path resolved against base_url's scheme+host, or an absolute URL); used by the runtime's `unload` on_agent_switch builtin hook to release model resources between agent switches. Infrastructure options: http_headers (map of string to string, adds custom HTTP headers to every request; used for OpenAI-compatible providers like github-copilot which requires Copilot-Integration-Id). dmr: runtime_flags; supports_images (boolean) and supports_pdf (boolean) declare which document attachment types the local model accepts. DMR-hosted models are not in the models.dev catalog, so attachment capabilities cannot be detected automatically and default to text-only; set supports_images: true for a vision model so image attachments are forwarded instead of silently dropped. anthropic/amazon-bedrock (Claude): interleaved_thinking (boolean, default true). anthropic: thinking_display ('summarized', 'omitted', 'updates', or 'display') controls whether thinking blocks are returned in responses when thinking is enabled. Claude Opus 4.7 hides thinking by default ('omitted'); set thinking_display: summarized to receive thinking blocks. 'display' (full thinking blocks) is only accepted by pre-4.6 token-thinking models (e.g. Sonnet 4.5, Haiku 4.5); models from the adaptive-thinking generation onward (Opus/Sonnet 4.6+, Sonnet 5, Fable 5) reject it, and docker-agent fails fast with a configuration error (fallbacks models are validated too); fallbacks (array of model ID strings, in priority order; enables the server-side-fallback beta so requests refused by safety classifiers, e.g. on Claude Fable 5, are retried with the listed models in a single round trip). OpenAI-compatible APIs: service_tier (string, forwarded unchanged to Chat Completions and Responses, including WebSocket and reranking; 'fast' or 'priority' enables Fast mode at premium pricing on supported models; omitted or empty leaves the API default, non-string values are ignored). openai/chatgpt: preserve_reasoning (boolean, default false) preserves ordered Responses output and encrypted reasoning for stateless continuation; native_tool_search also enables output replay. Any OpenAI-compatible destination: replay_reasoning (boolean) replays the reasoning stored on assistant turns back as a reasoning_content field on later Chat Completions requests, so a reasoning model sees its own earlier reasoning instead of re-deriving it after each tool call (distinct from preserve_reasoning, which replays encrypted reasoning items on the Responses API and does not apply to Chat Completions); default true for Chat Completions; set replay_reasoning to false for an endpoint that rejects or must not receive the non-standard field; a non-boolean value also disables replay. openai: cache_diagnostics (boolean, default false) logs Responses prompt-cache comparison results with --debug on GPT-5.6+; comparisons are scoped to the current session and resolved model; ignored for chatgpt and custom base_url endpoints. transport ('sse' or 'websocket') to choose between SSE and WebSocket streaming for the Responses API; native_tool_search (boolean, default false; also enables ordered output replay) declares the agent's deferred tools with defer_loading and a server-executed tool_search tool so OpenAI's hosted tool search discovers them (only openai models supporting deferred tools, gpt-5.4+; ignored on chatgpt, OpenAI-compatible endpoints including a custom base_url, and Chat Completions requests). openai/anthropic/google: rerank_prompt (string) to fully override the system prompt used for RAG reranking (advanced - prefer using results.reranking.criteria for domain-specific guidance). Google: google_search (boolean) enables Google Search grounding, google_maps (boolean) enables Google Maps grounding, code_execution (boolean) enables server-side code execution, url_context (boolean) enables reading public URLs during generation, output_dimensionality (positive integer) truncates Gemini embedding vectors to that size when the model is used as a RAG embedding_model; service_tier (string, forwarded unchanged as the top-level serviceTier of every generateContent request, including reranking, title generation, and compaction, on the Gemini API, Vertex AI, and the Docker models gateway; 'flex' for discounted, queued processing that may wait up to 15 minutes before the first byte, 'standard' for the default tier, 'priority' for premium processing; omitted or empty leaves the API default, non-string values are ignored; cost estimates are not tier-adjusted, set the model's cost override).",
"additionalProperties": true
},
"track_usage": {
Expand Down
40 changes: 40 additions & 0 deletions docs/providers/custom/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,46 @@ agents:
>
> Models that stream reasoning under `delta.reasoning` (e.g. Qwen3 served via OVHcloud AI Endpoints, OpenRouter, or a self-hosted vLLM / SGLang deployment) are fully supported. Docker Agent reads both the `delta.reasoning_content` and `delta.reasoning` fields from the stream, so thinking blocks are captured and shown in the TUI regardless of which field the server uses.

### Replaying reasoning on later turns

Captured reasoning is sent back to the model on subsequent requests as a `reasoning_content` field on the assistant message. Servers that understand the field read it back, so the model sees its own earlier reasoning instead of re-deriving it after every tool call. Reasoning that is only useful to the model is not free: replaying it can multiply the size of a long request, so it is limited to turns the session keeps anyway (those with content or a tool call).

Chat Completions reasoning replay defaults on. Because `reasoning_content` is a
non-standard extension, set `provider_opts.replay_reasoning: false` for a model
or provider whose endpoint rejects or must not receive it. An invalid,
non-boolean value also fails closed and disables replay.

Set the opt-out on a model, or once on a provider and let its models inherit it:

```yaml
providers:
local_llm:
base_url: http://localhost:8000/v1
provider_opts:
replay_reasoning: false # opt out every local_llm model

models:
qwen_default:
provider: local_llm
model: qwen3
provider_opts:
replay_reasoning: true # optional: replay is already the default

agents:
root:
model: qwen_default
```

This option is specific to the Chat Completions API and its `reasoning_content`
field. It is not the same as `preserve_reasoning`, which replays the encrypted
reasoning items of first-party OpenAI Responses sessions; neither option
applies to the other API. See [Reasoning state preservation](../openai/index.md#reasoning-state-preservation).

The field is worth a look if you switch models mid-session or use fallbacks:
Docker Agent stores reasoning from every provider in one place, so a later Chat
Completions model may otherwise receive reasoning captured from another vendor.
Set `replay_reasoning: false` on that destination when this is undesirable.

### API Router (Requesty, LiteLLM)

```yaml
Expand Down
12 changes: 9 additions & 3 deletions docs/providers/openai/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -218,9 +218,9 @@ generated values. This option does not apply to the Responses API.

## Reasoning State Preservation

Reasoning replay is opt-in, so existing configurations keep their request and
session-storage behavior. Enable it for first-party OpenAI Responses sessions,
including the `chatgpt` provider:
Responses reasoning replay is opt-in, so existing configurations keep their
request and session-storage behavior. Enable it for first-party OpenAI
Responses sessions, including the `chatgpt` provider:

```yaml
provider_opts:
Expand All @@ -247,3 +247,9 @@ WebSocket requests replay the full history without also chaining it through `pre

See [OpenAI's reasoning guide](https://developers.openai.com/api/docs/guides/reasoning)
and [cache diagnostics guide](https://developers.openai.com/api/docs/guides/prompt-caching/diagnostics).

`preserve_reasoning` is unrelated to `provider_opts.replay_reasoning`, which
replays plain reasoning text as a `reasoning_content` field on Chat
Completions requests to OpenAI-compatible servers. It does not apply to the
Responses API, and `preserve_reasoning` does not apply to Chat Completions.
See [Replaying reasoning on later turns](../custom/index.md#replaying-reasoning-on-later-turns).
52 changes: 52 additions & 0 deletions examples/reasoning_replay.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
# Example: Replaying reasoning to an OpenAI-compatible model
#
# Docker Agent captures reasoning from reasoning models (delta.reasoning_content
# or delta.reasoning on OpenAI-compatible streams) and replays it on later turns,
# so the model sees its own earlier reasoning instead of re-deriving it after
# every tool call. The replay is sent as a reasoning_content field on the
# assistant message.
#
# Replay defaults on for Chat Completions. Set
# provider_opts.replay_reasoning: false when an endpoint rejects or must not
# receive the non-standard field. Replayed reasoning counts against the context
# window, so a model that reasons at length is also the case where opting out
# saves the most tokens.
#
# This option is Chat Completions only. The Responses API has a separate
# preserve_reasoning option for its encrypted reasoning items; neither option
# applies to the other API.

providers:
# A local server fronting an open-weight reasoning model. Replay is already
# on for this destination; the explicit setting is shown for contrast.
local_vllm:
provider: openai
api_type: openai_chatcompletions
base_url: http://localhost:8000/v1

models:
qwen_replay:
provider: local_vllm
model: qwen3-30b-a3b
provider_opts:
replay_reasoning: true

# Same server, but this model should never receive stored reasoning.
qwen_no_replay:
provider: local_vllm
model: qwen3-30b-a3b
provider_opts:
replay_reasoning: false

agents:
root:
model: qwen_replay
description: Main assistant, replaying its own reasoning
instruction: |
You are a helpful AI assistant. Be concise and helpful in your responses.

analyst:
model: qwen_no_replay
description: Sub-agent that starts each turn from scratch
instruction: |
You are a specialized assistant for specific tasks.
15 changes: 11 additions & 4 deletions pkg/chat/chat.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,10 +56,17 @@ type Message struct {
Content string `json:"content"`
MultiContent []MessagePart `json:"multi_content,omitempty"`

// This property is used for the "reasoning" feature supported by deepseek-reasoner
// which is not in the official documentation.
// the doc from deepseek:
// - https://api-docs.deepseek.com/api/create-chat-completion#responses
// ReasoningContent carries the reasoning text of a reasoning model. It is a
// generic internal field, not one wire field: it is filled from
// delta.reasoning_content / delta.reasoning on OpenAI-compatible streams
// (DeepSeek, Qwen, vLLM, llama.cpp, ...), from Anthropic and Bedrock thinking
// blocks, from Gemini reasoning parts, and from the OpenAI Responses API.
// Providers that keep reasoning signed or encrypted of their own store that
// alongside in ThinkingSignature or ThoughtSignature.
//
// Replaying it on later turns stops a reasoning model from re-deriving work it
// already produced. Chat Completions replay defaults on and can be disabled
// with provider_opts.replay_reasoning for incompatible endpoints.
ReasoningContent string `json:"reasoning_content,omitempty"`

// ThinkingSignature is used for Anthropic's extended thinking feature
Expand Down
5 changes: 5 additions & 0 deletions pkg/model/provider/dmr/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,11 @@ func (c *Client) convertMessages(ctx context.Context, messages []chat.Message) [
if override := c.CapsOverride(); override != nil {
caps = modelinfo.CapsWith(override.Image, override.PDF, override.Audio, override.Video)
}
// Chat Completions reasoning replay defaults on;
// provider_opts.replay_reasoning: false opts out.
if !oaistream.ReplayReasoning(c.ModelConfig.ProviderOpts) {
messages = oaistream.WithoutReplayedReasoning(messages)
}
openaiMessages := oaistream.ConvertMessagesWithCaps(ctx, messages, caps)
return oaistream.MergeConsecutiveMessages(openaiMessages)
}
Expand Down
73 changes: 73 additions & 0 deletions pkg/model/provider/dmr/reasoning_replay_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
package dmr

import (
"encoding/json"
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"

"github.com/docker/docker-agent/pkg/chat"
"github.com/docker/docker-agent/pkg/config/latest"
"github.com/docker/docker-agent/pkg/model/provider/base"
"github.com/docker/docker-agent/pkg/tools"
)

// Chat Completions reasoning replay defaults on and
// provider_opts.replay_reasoning: false turns it off.
func TestConvertMessages_ReasoningReplay(t *testing.T) {
t.Parallel()

tests := []struct {
name string
opts map[string]any
wantSent bool
}{
{name: "on by default", wantSent: true},
{name: "explicit opt-out", opts: map[string]any{"replay_reasoning": false}, wantSent: false},
{name: "explicit true keeps replay enabled", opts: map[string]any{"replay_reasoning": true}, wantSent: true},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()

c := &Client{Config: base.Config{ModelConfig: latest.ModelConfig{
Provider: "dmr",
Model: "ai/qwen3",
ProviderOpts: tt.opts,
}}}

converted := c.convertMessages(t.Context(), []chat.Message{
{Role: chat.MessageRoleUser, Content: "weather?"},
{
Role: chat.MessageRoleAssistant,
Content: "checking",
ReasoningContent: "I should call the weather tool",
ToolCalls: []tools.ToolCall{{ID: "c1", Function: tools.FunctionCall{Name: "weather", Arguments: `{}`}}},
},
})

raw, err := json.Marshal(converted)
require.NoError(t, err)

var msgs []map[string]any
require.NoError(t, json.Unmarshal(raw, &msgs))

var assistant map[string]any
for _, m := range msgs {
if m["role"] == "assistant" {
assistant = m
}
}
require.NotNil(t, assistant, "assistant turn must survive conversion: %s", raw)

if tt.wantSent {
assert.Equal(t, "I should call the weather tool", assistant["reasoning_content"])
} else {
assert.NotContains(t, assistant, "reasoning_content")
}
assert.Len(t, assistant["tool_calls"], 1)
})
}
}
Loading