openai: replay reasoning_content for chat completions
- commit
- ea10cbad79826f684359ab9de2d2c7cc39a4f9a1
- author
- patriceckhart · · 2026-05-05 13:15 UTC
- parent
- 7134fb7
- signature
- unsigned
- refs
- tag: v0.1.55
1 file · +34 -10
M internal/provider/openai.go +34 -10
| @@ -109,6 +109,11 @@ type oaiMessage struct { | |||
| 109 | 109 | Name string `json:"name,omitempty"` | |
| 110 | 110 | ToolCalls []oaiToolCall `json:"tool_calls,omitempty"` | |
| 111 | 111 | ToolCallID string `json:"tool_call_id,omitempty"` | |
| 112 | + | // ReasoningContent carries the model's chain-of-thought summary | |
| 113 | + | // alongside an assistant tool-call message. Required by Kimi's | |
| 114 | + | // chat completions endpoint when thinking is enabled and the | |
| 115 | + | // assistant message contains a tool call; OpenAI ignores it. | |
| 116 | + | ReasoningContent string `json:"reasoning_content,omitempty"` | |
| 112 | 117 | } | |
| 113 | 118 | ||
| 114 | 119 | type oaiTool struct { | |
| @@ -186,6 +191,7 @@ func (c *openaiClient) buildRequest(req Request) (*oaiRequest, error) { | |||
| 186 | 191 | case RoleAssistant: | |
| 187 | 192 | am := oaiMessage{Role: "assistant"} | |
| 188 | 193 | var text strings.Builder | |
| 194 | + | var reasoning strings.Builder | |
| 189 | 195 | for _, b := range msg.Content { | |
| 190 | 196 | switch v := b.(type) { | |
| 191 | 197 | case TextBlock: | |
| @@ -195,7 +201,7 @@ func (c *openaiClient) buildRequest(req Request) (*oaiRequest, error) { | |||
| 195 | 201 | text.WriteString(v.Text) | |
| 196 | 202 | case ToolCallBlock: | |
| 197 | 203 | args := v.Arguments | |
| 198 | - | if len(args) == 0 { | |
| 204 | + | if len(args) == 0 || !json.Valid(args) { | |
| 199 | 205 | args = json.RawMessage("{}") | |
| 200 | 206 | } | |
| 201 | 207 | am.ToolCalls = append(am.ToolCalls, oaiToolCall{ | |
| @@ -206,11 +212,21 @@ func (c *openaiClient) buildRequest(req Request) (*oaiRequest, error) { | |||
| 206 | 212 | Arguments: string(args), | |
| 207 | 213 | }, | |
| 208 | 214 | }) | |
| 215 | + | case ReasoningBlock: | |
| 216 | + | if v.Summary != "" { | |
| 217 | + | if reasoning.Len() > 0 { | |
| 218 | + | reasoning.WriteString("\n") | |
| 219 | + | } | |
| 220 | + | reasoning.WriteString(v.Summary) | |
| 221 | + | } | |
| 209 | 222 | } | |
| 210 | 223 | } | |
| 211 | 224 | if text.Len() > 0 { | |
| 212 | 225 | am.Content = text.String() | |
| 213 | 226 | } | |
| 227 | + | if reasoning.Len() > 0 && len(am.ToolCalls) > 0 { | |
| 228 | + | am.ReasoningContent = reasoning.String() | |
| 229 | + | } | |
| 214 | 230 | out.Messages = append(out.Messages, am) | |
| 215 | 231 | case RoleTool: | |
| 216 | 232 | // Each ToolResultBlock becomes its own tool message. Preserve | |
| @@ -377,12 +393,13 @@ func (c *openaiClient) runStream(ctx context.Context, resp *http.Response, req R | |||
| 377 | 393 | announced bool | |
| 378 | 394 | } | |
| 379 | 395 | var ( | |
| 380 | - | blocks []*blockEntry | |
| 381 | - | currentText *blockEntry // most-recent text block, nil if none | |
| 382 | - | toolByIdx = map[int]*blockEntry{} // openai tool_call index -> block | |
| 383 | - | usage Usage | |
| 384 | - | stop StopReason = StopEnd | |
| 385 | - | finalErr error | |
| 396 | + | blocks []*blockEntry | |
| 397 | + | currentText *blockEntry // most-recent text block, nil if none | |
| 398 | + | toolByIdx = map[int]*blockEntry{} // openai tool_call index -> block | |
| 399 | + | reasoningBuf strings.Builder | |
| 400 | + | usage Usage | |
| 401 | + | stop StopReason = StopEnd | |
| 402 | + | finalErr error | |
| 386 | 403 | ) | |
| 387 | 404 | ||
| 388 | 405 | appendText := func(delta string) { | |
| @@ -416,7 +433,7 @@ func (c *openaiClient) runStream(ctx context.Context, resp *http.Response, req R | |||
| 416 | 433 | } | |
| 417 | 434 | case "tool_use": | |
| 418 | 435 | args := b.toolArgs.String() | |
| 419 | - | if args == "" { | |
| 436 | + | if args == "" || !json.Valid([]byte(args)) { | |
| 420 | 437 | args = "{}" | |
| 421 | 438 | } | |
| 422 | 439 | content = append(content, ToolCallBlock{ | |
| @@ -424,6 +441,9 @@ func (c *openaiClient) runStream(ctx context.Context, resp *http.Response, req R | |||
| 424 | 441 | }) | |
| 425 | 442 | } | |
| 426 | 443 | } | |
| 444 | + | if reasoningBuf.Len() > 0 { | |
| 445 | + | content = append(content, ReasoningBlock{Summary: reasoningBuf.String()}) | |
| 446 | + | } | |
| 427 | 447 | return Message{Role: RoleAssistant, Content: content, Time: time.Now()} | |
| 428 | 448 | } | |
| 429 | 449 | ||
| @@ -453,8 +473,9 @@ func (c *openaiClient) runStream(ctx context.Context, resp *http.Response, req R | |||
| 453 | 473 | Choices []struct { | |
| 454 | 474 | Index int `json:"index"` | |
| 455 | 475 | Delta struct { | |
| 456 | - | Content string `json:"content"` | |
| 457 | - | ToolCalls []struct { | |
| 476 | + | Content string `json:"content"` | |
| 477 | + | ReasoningContent string `json:"reasoning_content"` | |
| 478 | + | ToolCalls []struct { | |
| 458 | 479 | Index int `json:"index"` | |
| 459 | 480 | ID string `json:"id"` | |
| 460 | 481 | Type string `json:"type"` | |
| @@ -496,6 +517,9 @@ func (c *openaiClient) runStream(ctx context.Context, resp *http.Response, req R | |||
| 496 | 517 | usage.CacheReadTokens = chunk.Usage.PromptTokensDetails.CachedTokens | |
| 497 | 518 | } | |
| 498 | 519 | for _, ch := range chunk.Choices { | |
| 520 | + | if ch.Delta.ReasoningContent != "" { | |
| 521 | + | reasoningBuf.WriteString(ch.Delta.ReasoningContent) | |
| 522 | + | } | |
| 499 | 523 | if ch.Delta.Content != "" { | |
| 500 | 524 | appendText(ch.Delta.Content) | |
| 501 | 525 | out <- EventTextDelta{Delta: ch.Delta.Content} | |
| @@ -109,6 +109,11 @@ type oaiMessage struct { | |||||
| 109 | Name string `json:"name,omitempty"` | 109 | Name string `json:"name,omitempty"` | ||
| 110 | ToolCalls []oaiToolCall `json:"tool_calls,omitempty"` | 110 | ToolCalls []oaiToolCall `json:"tool_calls,omitempty"` | ||
| 111 | ToolCallID string `json:"tool_call_id,omitempty"` | 111 | ToolCallID string `json:"tool_call_id,omitempty"` | ||
| 112 | + | // ReasoningContent carries the model's chain-of-thought summary | |||
| 113 | + | // alongside an assistant tool-call message. Required by Kimi's | |||
| 114 | + | // chat completions endpoint when thinking is enabled and the | |||
| 115 | + | // assistant message contains a tool call; OpenAI ignores it. | |||
| 116 | + | ReasoningContent string `json:"reasoning_content,omitempty"` | |||
| 112 | } | 117 | } | ||
| 113 | 118 | ||||
| 114 | type oaiTool struct { | 119 | type oaiTool struct { | ||
| @@ -186,6 +191,7 @@ func (c *openaiClient) buildRequest(req Request) (*oaiRequest, error) { | |||||
| 186 | case RoleAssistant: | 191 | case RoleAssistant: | ||
| 187 | am := oaiMessage{Role: "assistant"} | 192 | am := oaiMessage{Role: "assistant"} | ||
| 188 | var text strings.Builder | 193 | var text strings.Builder | ||
| 194 | + | var reasoning strings.Builder | |||
| 189 | for _, b := range msg.Content { | 195 | for _, b := range msg.Content { | ||
| 190 | switch v := b.(type) { | 196 | switch v := b.(type) { | ||
| 191 | case TextBlock: | 197 | case TextBlock: | ||
| @@ -195,7 +201,7 @@ func (c *openaiClient) buildRequest(req Request) (*oaiRequest, error) { | |||||
| 195 | text.WriteString(v.Text) | 201 | text.WriteString(v.Text) | ||
| 196 | case ToolCallBlock: | 202 | case ToolCallBlock: | ||
| 197 | args := v.Arguments | 203 | args := v.Arguments | ||
| 198 | - | if len(args) == 0 { | 204 | + | if len(args) == 0 || !json.Valid(args) { |
| 199 | args = json.RawMessage("{}") | 205 | args = json.RawMessage("{}") | ||
| 200 | } | 206 | } | ||
| 201 | am.ToolCalls = append(am.ToolCalls, oaiToolCall{ | 207 | am.ToolCalls = append(am.ToolCalls, oaiToolCall{ | ||
| @@ -206,11 +212,21 @@ func (c *openaiClient) buildRequest(req Request) (*oaiRequest, error) { | |||||
| 206 | Arguments: string(args), | 212 | Arguments: string(args), | ||
| 207 | }, | 213 | }, | ||
| 208 | }) | 214 | }) | ||
| 215 | + | case ReasoningBlock: | |||
| 216 | + | if v.Summary != "" { | |||
| 217 | + | if reasoning.Len() > 0 { | |||
| 218 | + | reasoning.WriteString("\n") | |||
| 219 | + | } | |||
| 220 | + | reasoning.WriteString(v.Summary) | |||
| 221 | + | } | |||
| 209 | } | 222 | } | ||
| 210 | } | 223 | } | ||
| 211 | if text.Len() > 0 { | 224 | if text.Len() > 0 { | ||
| 212 | am.Content = text.String() | 225 | am.Content = text.String() | ||
| 213 | } | 226 | } | ||
| 227 | + | if reasoning.Len() > 0 && len(am.ToolCalls) > 0 { | |||
| 228 | + | am.ReasoningContent = reasoning.String() | |||
| 229 | + | } | |||
| 214 | out.Messages = append(out.Messages, am) | 230 | out.Messages = append(out.Messages, am) | ||
| 215 | case RoleTool: | 231 | case RoleTool: | ||
| 216 | // Each ToolResultBlock becomes its own tool message. Preserve | 232 | // Each ToolResultBlock becomes its own tool message. Preserve | ||
| @@ -377,12 +393,13 @@ func (c *openaiClient) runStream(ctx context.Context, resp *http.Response, req R | |||||
| 377 | announced bool | 393 | announced bool | ||
| 378 | } | 394 | } | ||
| 379 | var ( | 395 | var ( | ||
| 380 | - | blocks []*blockEntry | 396 | + | blocks []*blockEntry |
| 381 | - | currentText *blockEntry // most-recent text block, nil if none | 397 | + | currentText *blockEntry // most-recent text block, nil if none |
| 382 | - | toolByIdx = map[int]*blockEntry{} // openai tool_call index -> block | 398 | + | toolByIdx = map[int]*blockEntry{} // openai tool_call index -> block |
| 383 | - | usage Usage | 399 | + | reasoningBuf strings.Builder |
| 384 | - | stop StopReason = StopEnd | 400 | + | usage Usage |
| 385 | - | finalErr error | 401 | + | stop StopReason = StopEnd |
| 402 | + | finalErr error | |||
| 386 | ) | 403 | ) | ||
| 387 | 404 | ||||
| 388 | appendText := func(delta string) { | 405 | appendText := func(delta string) { | ||
| @@ -416,7 +433,7 @@ func (c *openaiClient) runStream(ctx context.Context, resp *http.Response, req R | |||||
| 416 | } | 433 | } | ||
| 417 | case "tool_use": | 434 | case "tool_use": | ||
| 418 | args := b.toolArgs.String() | 435 | args := b.toolArgs.String() | ||
| 419 | - | if args == "" { | 436 | + | if args == "" || !json.Valid([]byte(args)) { |
| 420 | args = "{}" | 437 | args = "{}" | ||
| 421 | } | 438 | } | ||
| 422 | content = append(content, ToolCallBlock{ | 439 | content = append(content, ToolCallBlock{ | ||
| @@ -424,6 +441,9 @@ func (c *openaiClient) runStream(ctx context.Context, resp *http.Response, req R | |||||
| 424 | }) | 441 | }) | ||
| 425 | } | 442 | } | ||
| 426 | } | 443 | } | ||
| 444 | + | if reasoningBuf.Len() > 0 { | |||
| 445 | + | content = append(content, ReasoningBlock{Summary: reasoningBuf.String()}) | |||
| 446 | + | } | |||
| 427 | return Message{Role: RoleAssistant, Content: content, Time: time.Now()} | 447 | return Message{Role: RoleAssistant, Content: content, Time: time.Now()} | ||
| 428 | } | 448 | } | ||
| 429 | 449 | ||||
| @@ -453,8 +473,9 @@ func (c *openaiClient) runStream(ctx context.Context, resp *http.Response, req R | |||||
| 453 | Choices []struct { | 473 | Choices []struct { | ||
| 454 | Index int `json:"index"` | 474 | Index int `json:"index"` | ||
| 455 | Delta struct { | 475 | Delta struct { | ||
| 456 | - | Content string `json:"content"` | 476 | + | Content string `json:"content"` |
| 457 | - | ToolCalls []struct { | 477 | + | ReasoningContent string `json:"reasoning_content"` |
| 478 | + | ToolCalls []struct { | |||
| 458 | Index int `json:"index"` | 479 | Index int `json:"index"` | ||
| 459 | ID string `json:"id"` | 480 | ID string `json:"id"` | ||
| 460 | Type string `json:"type"` | 481 | Type string `json:"type"` | ||
| @@ -496,6 +517,9 @@ func (c *openaiClient) runStream(ctx context.Context, resp *http.Response, req R | |||||
| 496 | usage.CacheReadTokens = chunk.Usage.PromptTokensDetails.CachedTokens | 517 | usage.CacheReadTokens = chunk.Usage.PromptTokensDetails.CachedTokens | ||
| 497 | } | 518 | } | ||
| 498 | for _, ch := range chunk.Choices { | 519 | for _, ch := range chunk.Choices { | ||
| 520 | + | if ch.Delta.ReasoningContent != "" { | |||
| 521 | + | reasoningBuf.WriteString(ch.Delta.ReasoningContent) | |||
| 522 | + | } | |||
| 499 | if ch.Delta.Content != "" { | 523 | if ch.Delta.Content != "" { | ||
| 500 | appendText(ch.Delta.Content) | 524 | appendText(ch.Delta.Content) | ||
| 501 | out <- EventTextDelta{Delta: ch.Delta.Content} | 525 | out <- EventTextDelta{Delta: ch.Delta.Content} | ||