detached ea10cba

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 {
109109 Name string `json:"name,omitempty"`
110110 ToolCalls []oaiToolCall `json:"tool_calls,omitempty"`
111111 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"`
112117 }
113118
114119 type oaiTool struct {
@@ -186,6 +191,7 @@ func (c *openaiClient) buildRequest(req Request) (*oaiRequest, error) {
186191 case RoleAssistant:
187192 am := oaiMessage{Role: "assistant"}
188193 var text strings.Builder
194+ var reasoning strings.Builder
189195 for _, b := range msg.Content {
190196 switch v := b.(type) {
191197 case TextBlock:
@@ -195,7 +201,7 @@ func (c *openaiClient) buildRequest(req Request) (*oaiRequest, error) {
195201 text.WriteString(v.Text)
196202 case ToolCallBlock:
197203 args := v.Arguments
198- if len(args) == 0 {
204+ if len(args) == 0 || !json.Valid(args) {
199205 args = json.RawMessage("{}")
200206 }
201207 am.ToolCalls = append(am.ToolCalls, oaiToolCall{
@@ -206,11 +212,21 @@ func (c *openaiClient) buildRequest(req Request) (*oaiRequest, error) {
206212 Arguments: string(args),
207213 },
208214 })
215+ case ReasoningBlock:
216+ if v.Summary != "" {
217+ if reasoning.Len() > 0 {
218+ reasoning.WriteString("\n")
219+ }
220+ reasoning.WriteString(v.Summary)
221+ }
209222 }
210223 }
211224 if text.Len() > 0 {
212225 am.Content = text.String()
213226 }
227+ if reasoning.Len() > 0 && len(am.ToolCalls) > 0 {
228+ am.ReasoningContent = reasoning.String()
229+ }
214230 out.Messages = append(out.Messages, am)
215231 case RoleTool:
216232 // Each ToolResultBlock becomes its own tool message. Preserve
@@ -377,12 +393,13 @@ func (c *openaiClient) runStream(ctx context.Context, resp *http.Response, req R
377393 announced bool
378394 }
379395 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
386403 )
387404
388405 appendText := func(delta string) {
@@ -416,7 +433,7 @@ func (c *openaiClient) runStream(ctx context.Context, resp *http.Response, req R
416433 }
417434 case "tool_use":
418435 args := b.toolArgs.String()
419- if args == "" {
436+ if args == "" || !json.Valid([]byte(args)) {
420437 args = "{}"
421438 }
422439 content = append(content, ToolCallBlock{
@@ -424,6 +441,9 @@ func (c *openaiClient) runStream(ctx context.Context, resp *http.Response, req R
424441 })
425442 }
426443 }
444+ if reasoningBuf.Len() > 0 {
445+ content = append(content, ReasoningBlock{Summary: reasoningBuf.String()})
446+ }
427447 return Message{Role: RoleAssistant, Content: content, Time: time.Now()}
428448 }
429449
@@ -453,8 +473,9 @@ func (c *openaiClient) runStream(ctx context.Context, resp *http.Response, req R
453473 Choices []struct {
454474 Index int `json:"index"`
455475 Delta struct {
456- Content string `json:"content"`
457- ToolCalls []struct {
476+ Content string `json:"content"`
477+ ReasoningContent string `json:"reasoning_content"`
478+ ToolCalls []struct {
458479 Index int `json:"index"`
459480 ID string `json:"id"`
460481 Type string `json:"type"`
@@ -496,6 +517,9 @@ func (c *openaiClient) runStream(ctx context.Context, resp *http.Response, req R
496517 usage.CacheReadTokens = chunk.Usage.PromptTokensDetails.CachedTokens
497518 }
498519 for _, ch := range chunk.Choices {
520+ if ch.Delta.ReasoningContent != "" {
521+ reasoningBuf.WriteString(ch.Delta.ReasoningContent)
522+ }
499523 if ch.Delta.Content != "" {
500524 appendText(ch.Delta.Content)
501525 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.Builder193 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.Arguments203 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. Preserve232 // 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 bool393 announced bool
378 }394 }
379 var (395 var (
380- blocks []*blockEntry396+ blocks []*blockEntry
381- currentText *blockEntry // most-recent text block, nil if none397+ currentText *blockEntry // most-recent text block, nil if none
382- toolByIdx = map[int]*blockEntry{} // openai tool_call index -> block398+ toolByIdx = map[int]*blockEntry{} // openai tool_call index -> block
383- usage Usage399+ reasoningBuf strings.Builder
384- stop StopReason = StopEnd400+ usage Usage
385- finalErr error401+ 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.CachedTokens517 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}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 1

Tags 476

↑↓ moveenter openesc close