fix: round-trip reasoning_content for DeepSeek thinking models
DeepSeek requires reasoning_content to be echoed back on assistant messages. Store reasoning from stream events on Message and serialize it as reasoning_content in the OpenAI wire format.
This commit is contained in:
parent
10cbded305
commit
bc7a536ced
|
|
@ -101,11 +101,13 @@ func run(ctx context.Context, cfg agentConfig, state state) error {
|
||||||
// Stream the assistant's response, retrying on rate limits, transient
|
// Stream the assistant's response, retrying on rate limits, transient
|
||||||
// backend errors (5xx, network), and mid-stream drops.
|
// backend errors (5xx, network), and mid-stream drops.
|
||||||
var content strings.Builder
|
var content strings.Builder
|
||||||
|
var reasoning strings.Builder
|
||||||
var toolCalls []backend.ToolCall
|
var toolCalls []backend.ToolCall
|
||||||
var stopReason string
|
var stopReason string
|
||||||
|
|
||||||
for attempt := range maxTransientRetries + 1 {
|
for attempt := range maxTransientRetries + 1 {
|
||||||
content.Reset()
|
content.Reset()
|
||||||
|
reasoning.Reset()
|
||||||
toolCalls = nil
|
toolCalls = nil
|
||||||
|
|
||||||
ch, err := cfg.Backend.ChatStream(ctx, history, cfg.Tools, cfg.GenerationParams)
|
ch, err := cfg.Backend.ChatStream(ctx, history, cfg.Tools, cfg.GenerationParams)
|
||||||
|
|
@ -144,6 +146,7 @@ func run(ctx context.Context, cfg agentConfig, state state) error {
|
||||||
emit(cfg, Event{Role: "reasoning", Content: "<think>\n"})
|
emit(cfg, Event{Role: "reasoning", Content: "<think>\n"})
|
||||||
hadReasoning = true
|
hadReasoning = true
|
||||||
}
|
}
|
||||||
|
reasoning.WriteString(ev.Reasoning)
|
||||||
emit(cfg, Event{Role: "reasoning", Content: ev.Reasoning})
|
emit(cfg, Event{Role: "reasoning", Content: ev.Reasoning})
|
||||||
}
|
}
|
||||||
if ev.Content != "" {
|
if ev.Content != "" {
|
||||||
|
|
@ -194,7 +197,7 @@ func run(ctx context.Context, cfg agentConfig, state state) error {
|
||||||
if hadReasoning {
|
if hadReasoning {
|
||||||
emit(cfg, Event{Role: "reasoning", Content: "\n</think>\n"})
|
emit(cfg, Event{Role: "reasoning", Content: "\n</think>\n"})
|
||||||
}
|
}
|
||||||
msg := backend.Message{Role: "assistant", Content: content.String(), ToolCalls: toolCalls}
|
msg := backend.Message{Role: "assistant", Content: content.String(), Reasoning: reasoning.String(), ToolCalls: toolCalls}
|
||||||
var results []toolResult
|
var results []toolResult
|
||||||
for _, tc := range toolCalls {
|
for _, tc := range toolCalls {
|
||||||
results = append(results, toolResult{
|
results = append(results, toolResult{
|
||||||
|
|
@ -241,7 +244,7 @@ func run(ctx context.Context, cfg agentConfig, state state) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Execute tool calls, running consecutive parallel-read-safe tools concurrently.
|
// Execute tool calls, running consecutive parallel-read-safe tools concurrently.
|
||||||
msg := backend.Message{Role: "assistant", Content: content.String(), ToolCalls: toolCalls}
|
msg := backend.Message{Role: "assistant", Content: content.String(), Reasoning: reasoning.String(), ToolCalls: toolCalls}
|
||||||
results := make([]toolResult, 0, len(toolCalls))
|
results := make([]toolResult, 0, len(toolCalls))
|
||||||
interrupted := false
|
interrupted := false
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -32,6 +32,7 @@ type ImageSource struct {
|
||||||
type Message struct {
|
type Message struct {
|
||||||
Role string `json:"role"` // "system" | "user" | "assistant" | "tool"
|
Role string `json:"role"` // "system" | "user" | "assistant" | "tool"
|
||||||
Content string `json:"content"`
|
Content string `json:"content"`
|
||||||
|
Reasoning string `json:"reasoning,omitempty"` // thinking/reasoning content (DeepSeek, OpenAI o-series)
|
||||||
ContentBlocks []ContentBlock `json:"content_blocks,omitempty"` // when set, overrides Content
|
ContentBlocks []ContentBlock `json:"content_blocks,omitempty"` // when set, overrides Content
|
||||||
ToolCalls []ToolCall `json:"tool_calls,omitempty"` // set by assistant when calling tools
|
ToolCalls []ToolCall `json:"tool_calls,omitempty"` // set by assistant when calling tools
|
||||||
ToolCallID string `json:"tool_call_id,omitempty"` // set on role=tool replies (required by OpenAI)
|
ToolCallID string `json:"tool_call_id,omitempty"` // set on role=tool replies (required by OpenAI)
|
||||||
|
|
|
||||||
|
|
@ -132,11 +132,12 @@ type openAIContentBlock struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
type openAIMessage struct {
|
type openAIMessage struct {
|
||||||
Role string `json:"role"`
|
Role string `json:"role"`
|
||||||
Content *string `json:"content"`
|
Content *string `json:"content"`
|
||||||
ContentBlocks []openAIContentBlock `json:"-"` // non-nil overrides Content during marshaling
|
ReasoningContent *string `json:"reasoning_content,omitempty"`
|
||||||
ToolCalls []openAIToolCall `json:"tool_calls,omitempty"`
|
ContentBlocks []openAIContentBlock `json:"-"` // non-nil overrides Content during marshaling
|
||||||
ToolCallID string `json:"tool_call_id,omitempty"`
|
ToolCalls []openAIToolCall `json:"tool_calls,omitempty"`
|
||||||
|
ToolCallID string `json:"tool_call_id,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// MarshalJSON encodes content as an array of blocks when ContentBlocks is set
|
// MarshalJSON encodes content as an array of blocks when ContentBlocks is set
|
||||||
|
|
@ -145,11 +146,12 @@ type openAIMessage struct {
|
||||||
func (m openAIMessage) MarshalJSON() ([]byte, error) {
|
func (m openAIMessage) MarshalJSON() ([]byte, error) {
|
||||||
if len(m.ContentBlocks) > 0 {
|
if len(m.ContentBlocks) > 0 {
|
||||||
return json.Marshal(struct {
|
return json.Marshal(struct {
|
||||||
Role string `json:"role"`
|
Role string `json:"role"`
|
||||||
Content []openAIContentBlock `json:"content"`
|
ReasoningContent *string `json:"reasoning_content,omitempty"`
|
||||||
ToolCalls []openAIToolCall `json:"tool_calls,omitempty"`
|
Content []openAIContentBlock `json:"content"`
|
||||||
ToolCallID string `json:"tool_call_id,omitempty"`
|
ToolCalls []openAIToolCall `json:"tool_calls,omitempty"`
|
||||||
}{m.Role, m.ContentBlocks, m.ToolCalls, m.ToolCallID})
|
ToolCallID string `json:"tool_call_id,omitempty"`
|
||||||
|
}{m.Role, m.ReasoningContent, m.ContentBlocks, m.ToolCalls, m.ToolCallID})
|
||||||
}
|
}
|
||||||
type plain openAIMessage
|
type plain openAIMessage
|
||||||
return json.Marshal(plain(m))
|
return json.Marshal(plain(m))
|
||||||
|
|
@ -243,6 +245,9 @@ func encodeOpenAIMessages(messages []Message) []openAIMessage {
|
||||||
Role: m.Role,
|
Role: m.Role,
|
||||||
ToolCallID: m.ToolCallID,
|
ToolCallID: m.ToolCallID,
|
||||||
}
|
}
|
||||||
|
if m.Reasoning != "" {
|
||||||
|
wm.ReasoningContent = &m.Reasoning
|
||||||
|
}
|
||||||
for _, tc := range m.ToolCalls {
|
for _, tc := range m.ToolCalls {
|
||||||
wm.ToolCalls = append(wm.ToolCalls, openAIToolCall{
|
wm.ToolCalls = append(wm.ToolCalls, openAIToolCall{
|
||||||
ID: tc.ID,
|
ID: tc.ID,
|
||||||
|
|
|
||||||
Reference in New Issue