diff --git a/internal/e2e/e2e_test.go b/internal/e2e/e2e_test.go index 6bcfcc64..e1e4cc1e 100644 --- a/internal/e2e/e2e_test.go +++ b/internal/e2e/e2e_test.go @@ -232,7 +232,9 @@ func loadDotEnv(t testing.TB) { } parent := filepath.Dir(dir) if parent == dir { - t.Log(".env.test not found — relying on OS env vars") + if t != nil { + t.Log(".env.test not found — relying on OS env vars") + } return } dir = parent diff --git a/internal/e2e/openai_response_e2e_test.go b/internal/e2e/openai_response_e2e_test.go index 57b3359d..9e42d97b 100644 --- a/internal/e2e/openai_response_e2e_test.go +++ b/internal/e2e/openai_response_e2e_test.go @@ -5,9 +5,12 @@ package e2e_test import ( "context" "encoding/json" + "net/http" + "net/http/httptest" "testing" "moonbridge/internal/format" + "moonbridge/internal/protocol/anthropic" "moonbridge/internal/protocol/openai" ) @@ -404,6 +407,131 @@ func TestOpenAIResponsePassthroughE2E_Streaming(t *testing.T) { } } +// TestOpenAIResponseAnthropicE2E_ReasoningToolReplay verifies the full +// streaming path from an Anthropic thinking/tool-use response through the +// OpenAI Responses stream, then back to an Anthropic continuation request. +func TestOpenAIResponseAnthropicE2E_ReasoningToolReplay(t *testing.T) { + ctx := context.Background() + cfg := e2eMinimalConfig() + hooks := format.CorePluginHooks{}.WithDefaults() + reg := newTestRegistry(t, cfg, hooks) + + client, ok := reg.GetClient(configOpenAIResponse) + if !ok { + t.Fatal("OpenAI Responses client adapter not found") + } + clientStream, ok := reg.GetClientStream(configOpenAIResponse) + if !ok { + t.Fatal("OpenAI Responses stream adapter not found") + } + provider, ok := reg.GetProvider(configAnthropic) + if !ok { + t.Fatal("Anthropic provider adapter not found") + } + providerStream, ok := reg.GetProviderStream(configAnthropic) + if !ok { + t.Fatal("Anthropic provider stream adapter not found") + } + + mockSrv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.Header().Set("Content-Type", "text/event-stream") + w.WriteHeader(http.StatusOK) + writeSSE(w, "message_start", `{"type":"message_start","message":{"id":"msg_reasoning_001","type":"message","role":"assistant","content":[],"model":"deepseek-v4","usage":{"input_tokens":5,"output_tokens":0}}}`) + writeSSE(w, "content_block_start", `{"type":"content_block_start","index":0,"content_block":{"type":"thinking","thinking":""}}`) + writeSSE(w, "content_block_delta", `{"type":"content_block_delta","index":0,"delta":{"type":"thinking_delta","thinking":"check repository state"}}`) + writeSSE(w, "content_block_delta", `{"type":"content_block_delta","index":0,"delta":{"type":"signature_delta","signature":"sig_replay_001"}}`) + writeSSE(w, "content_block_stop", `{"type":"content_block_stop","index":0}`) + writeSSE(w, "content_block_start", `{"type":"content_block_start","index":1,"content_block":{"type":"tool_use","id":"call_1","name":"lookup","input":{}}}`) + writeSSE(w, "content_block_delta", `{"type":"content_block_delta","index":1,"delta":{"type":"input_json_delta","partial_json":"{\"query\":\"moon\"}"}}`) + writeSSE(w, "content_block_stop", `{"type":"content_block_stop","index":1}`) + writeSSE(w, "message_delta", `{"type":"message_delta","delta":{"stop_reason":"tool_use"},"usage":{"input_tokens":5,"output_tokens":8}}`) + writeSSE(w, "message_stop", `{"type":"message_stop"}`) + })) + defer mockSrv.Close() + + firstCore, err := client.ToCoreRequest(ctx, &openai.ResponsesRequest{ + Model: "deepseek-v4", + Input: json.RawMessage(`"Use lookup"`), + Stream: true, + }) + if err != nil { + t.Fatalf("first ToCoreRequest: %v", err) + } + upstreamAny, err := provider.FromCoreRequest(ctx, firstCore) + if err != nil { + t.Fatalf("first FromCoreRequest: %v", err) + } + stream, err := anthropic.NewClient(anthropic.ClientConfig{BaseURL: mockSrv.URL, APIKey: "test-key", Client: mockSrv.Client()}).StreamMessage(ctx, *upstreamAny.(*anthropic.MessageRequest)) + if err != nil { + t.Fatalf("StreamMessage: %v", err) + } + defer stream.Close() + coreStream, err := providerStream.ToCoreStream(ctx, stream) + if err != nil { + t.Fatalf("ToCoreStream: %v", err) + } + streamAny, err := clientStream.FromCoreStream(ctx, firstCore, coreStream.Events) + if err != nil { + t.Fatalf("FromCoreStream: %v", err) + } + + var response openai.Response + var createdID string + for event := range streamAny.(*openai.OpenAIStreamResult).Chan() { + switch event.Event { + case "response.created": + createdID = event.Data.(openai.ResponseLifecycleEvent).Response.ID + case "response.completed": + response = event.Data.(openai.ResponseLifecycleEvent).Response + } + } + if createdID != "msg_reasoning_001" { + t.Fatalf("response.created ID = %q, want msg_reasoning_001", createdID) + } + if len(response.Output) != 2 { + t.Fatalf("stream output = %+v, want reasoning and function_call", response.Output) + } + + input, err := json.Marshal(response.Output) + if err != nil { + t.Fatal(err) + } + var continuation []map[string]any + if err := json.Unmarshal(input, &continuation); err != nil { + t.Fatal(err) + } + continuation = append(continuation, map[string]any{"type": "function_call_output", "call_id": "call_1", "output": "found it"}) + input, err = json.Marshal(continuation) + if err != nil { + t.Fatal(err) + } + secondCore, err := client.ToCoreRequest(ctx, &openai.ResponsesRequest{Model: "deepseek-v4", Input: input}) + if err != nil { + t.Fatalf("continuation ToCoreRequest: %v", err) + } + secondAny, err := provider.FromCoreRequest(ctx, secondCore) + if err != nil { + t.Fatalf("continuation FromCoreRequest: %v", err) + } + second := secondAny.(*anthropic.MessageRequest) + if len(second.Messages) != 3 { + t.Fatalf("continuation messages = %+v", second.Messages) + } + assistant := second.Messages[1] + if assistant.Role != "assistant" || len(assistant.Content) != 2 { + t.Fatalf("assistant replay = %+v", assistant) + } + if got := assistant.Content[0]; got.Type != "thinking" || got.Thinking != "check repository state" || got.Signature != "sig_replay_001" { + t.Fatalf("replayed thinking = %+v", got) + } + if got := assistant.Content[1]; got.Type != "tool_use" || got.ID != "call_1" || got.Name != "lookup" { + t.Fatalf("replayed tool use = %+v", got) + } + if got := second.Messages[2]; got.Role != "user" || len(got.Content) != 1 || got.Content[0].Type != "tool_result" || got.Content[0].ToolUseID != "call_1" { + t.Fatalf("replayed tool result = %+v", got) + } +} + // ============================================================================ // TestOpenAIResponsePassthroughE2E_ErrorResponse // ============================================================================ diff --git a/internal/protocol/anthropic/adapter.go b/internal/protocol/anthropic/adapter.go index dffd2456..06a86e78 100644 --- a/internal/protocol/anthropic/adapter.go +++ b/internal/protocol/anthropic/adapter.go @@ -596,6 +596,7 @@ func (s *streamConverterState) convertEvent(events chan<- format.CoreStreamEvent s.emit(events, format.CoreStreamEvent{ Type: format.CoreEventCreated, + ItemID: s.msgID, Status: "in_progress", Model: s.model, }) @@ -687,6 +688,11 @@ func (s *streamConverterState) convertEvent(events chan<- format.CoreStreamEvent Delta: ev.Delta.PartialJSON, }) + case ev.Delta.Type == "signature_delta": + if sig := ev.Delta.Signature; sig != "" { + s.blockSignatures[index] = sig + } + case ev.Delta.Type == "thinking_delta" || blockType == "thinking": s.emit(events, format.CoreStreamEvent{ Type: format.CoreTextDelta, @@ -696,11 +702,6 @@ func (s *streamConverterState) convertEvent(events chan<- format.CoreStreamEvent Type: "reasoning", }, }) - - case ev.Delta.Type == "signature_delta": - if sig := ev.Delta.Signature; sig != "" { - s.blockSignatures[index] = sig - } } case "content_block_stop": diff --git a/internal/protocol/anthropic/adapter_test.go b/internal/protocol/anthropic/adapter_test.go index 88d0e353..680261a9 100644 --- a/internal/protocol/anthropic/adapter_test.go +++ b/internal/protocol/anthropic/adapter_test.go @@ -3,12 +3,29 @@ package anthropic_test import ( "context" "encoding/json" + "io" "testing" "moonbridge/internal/format" "moonbridge/internal/protocol/anthropic" ) +type fixtureStream struct { + events []anthropic.StreamEvent + index int +} + +func (s *fixtureStream) Next() (anthropic.StreamEvent, error) { + if s.index >= len(s.events) { + return anthropic.StreamEvent{}, io.EOF + } + event := s.events[s.index] + s.index++ + return event, nil +} + +func (s *fixtureStream) Close() error { return nil } + // --------------------------------------------------------------------------- // noopCacheManager — no-op implementation of anthropic.CacheManager // --------------------------------------------------------------------------- @@ -66,6 +83,29 @@ func TestFromCoreRequest_BasicTextMessage(t *testing.T) { } } +func TestToCoreStream_CreatedEventCarriesResponseID(t *testing.T) { + adapter := newTestAdapter() + stream := &fixtureStream{events: []anthropic.StreamEvent{ + { + Type: "message_start", + Message: &anthropic.MessageResponse{ + ID: "msg_provider_1", + Model: "deepseek-v4-flash", + }, + }, + {Type: "message_stop"}, + }} + + result, err := adapter.ToCoreStream(context.Background(), stream) + if err != nil { + t.Fatal(err) + } + first := <-result.Events + if first.Type != format.CoreEventCreated || first.ItemID != "msg_provider_1" { + t.Fatalf("created event = %+v, want response ID", first) + } +} + func TestFromCoreRequest_SystemField(t *testing.T) { adapter := newTestAdapter() diff --git a/internal/protocol/openai/adapter.go b/internal/protocol/openai/adapter.go index 2dab9d1a..57dfc2d0 100644 --- a/internal/protocol/openai/adapter.go +++ b/internal/protocol/openai/adapter.go @@ -487,10 +487,13 @@ func (a *OpenAIAdapter) streamLoopWithBuf(ctx context.Context, coreReq *format.C io := len(response.Output) outputIndexes[index] = io response.Output = append(response.Output, OutputItem{ - Type: "reasoning", - ID: id, - Status: "in_progress", - Summary: []ReasoningItemSummary{}, + Type: "reasoning", + ID: id, + Status: "in_progress", + // The Responses schema requires a summary array on a reasoning + // item. Codex uses this initial part to register the active item + // before it receives reasoning-summary delta events. + Summary: []ReasoningItemSummary{{Type: "summary_text", Text: ""}}, }) send(StreamEvent{ Event: "response.output_item.added", @@ -509,6 +512,10 @@ func (a *OpenAIAdapter) streamLoopWithBuf(ctx context.Context, coreReq *format.C ItemID: id, OutputIndex: io, SummaryIndex: 0, + Part: ReasoningItemSummary{ + Type: "summary_text", + Text: "", + }, }, }) contentText[index] = "" @@ -932,11 +939,26 @@ func (a *OpenAIAdapter) streamLoopWithBuf(ctx context.Context, coreReq *format.C sig = event.ContentBlock.ReasoningSignature } response.Output[idx].Summary = []ReasoningItemSummary{{ - Type: "text", + Type: "summary_text", Text: contentText[index], Signature: sig, }} } + part := ReasoningItemSummary{Type: "summary_text", Text: contentText[index]} + if idx, ok := outputIndexes[index]; ok && idx < len(response.Output) && len(response.Output[idx].Summary) > 0 { + part = response.Output[idx].Summary[0] + } + send(StreamEvent{ + Event: "response.reasoning_summary_text.done", + Data: ReasoningSummaryTextDoneEvent{ + Type: "response.reasoning_summary_text.done", + SequenceNumber: next(), + ItemID: itemIDs[index], + OutputIndex: outputIndexes[index], + SummaryIndex: 0, + Text: part.Text, + }, + }) send(StreamEvent{ Event: "response.reasoning_summary_part.done", Data: ReasoningSummaryPartDoneEvent{ @@ -945,8 +967,20 @@ func (a *OpenAIAdapter) streamLoopWithBuf(ctx context.Context, coreReq *format.C ItemID: itemIDs[index], OutputIndex: outputIndexes[index], SummaryIndex: 0, + Part: part, }, }) + if idx, ok := outputIndexes[index]; ok && idx < len(response.Output) { + send(StreamEvent{ + Event: "response.output_item.done", + Data: OutputItemEvent{ + Type: "response.output_item.done", + SequenceNumber: next(), + OutputIndex: idx, + Item: response.Output[idx], + }, + }) + } delete(contentText, index) delete(itemIDs, index) delete(outputIndexes, index) diff --git a/internal/protocol/openai/adapter_test.go b/internal/protocol/openai/adapter_test.go index 5b223314..7d314180 100644 --- a/internal/protocol/openai/adapter_test.go +++ b/internal/protocol/openai/adapter_test.go @@ -7,9 +7,18 @@ import ( "testing" "moonbridge/internal/format" + "moonbridge/internal/protocol/anthropic" "moonbridge/internal/protocol/openai" ) +type noOpCacheManager struct{} + +func (noOpCacheManager) PlanAndInject(_ context.Context, _ *anthropic.MessageRequest, _ *format.CoreRequest) (string, string) { + return "", "" +} + +func (noOpCacheManager) UpdateRegistry(_ context.Context, _, _ string, _ anthropic.Usage) {} + func TestToCoreRequest_BasicText(t *testing.T) { adapter := openai.NewOpenAIAdapter(format.CorePluginHooks{}) @@ -291,7 +300,7 @@ func TestToCoreRequest_KeepsToolUseAdjacentToToolResultWhenReasoningPrecedesOutp Model: "gpt-5.4", Input: json.RawMessage(`[ {"type":"function_call","id":"fc_1","call_id":"call_1","name":"tool_a","arguments":"{\"a\":1}"}, - {"type":"reasoning","summary":[{"type":"text","text":"thinking after tool call"}]}, + {"type":"reasoning","summary":[{"type":"summary_text","text":"thinking after tool call","signature":"sig_1"}]}, {"type":"function_call_output","call_id":"call_1","output":"ok"} ]`), } @@ -311,7 +320,7 @@ func TestToCoreRequest_KeepsToolUseAdjacentToToolResultWhenReasoningPrecedesOutp if len(assistant.Content) != 2 { t.Fatalf("assistant content len=%d, want 2; got %+v", len(assistant.Content), assistant.Content) } - if assistant.Content[0].Type != "reasoning" || assistant.Content[0].ReasoningText != "thinking after tool call" { + if assistant.Content[0].Type != "reasoning" || assistant.Content[0].ReasoningText != "thinking after tool call" || assistant.Content[0].ReasoningSignature != "sig_1" { t.Fatalf("assistant.Content[0]=%+v, want merged reasoning", assistant.Content[0]) } if assistant.Content[1].Type != "tool_use" || assistant.Content[1].ToolUseID != "call_1" { @@ -438,3 +447,153 @@ func TestFromCoreStream_NoDuplicateDoneForToolUse(t *testing.T) { t.Fatalf("output_item.done (tool) count=%d, want 1", itemDone) } } + +func TestFromCoreStream_ReasoningLifecycleUsesResponsesSchema(t *testing.T) { + adapter := openai.NewOpenAIAdapter(format.CorePluginHooks{}) + coreReq := &format.CoreRequest{Model: "deepseek-v4-flash"} + evCh := make(chan format.CoreStreamEvent, 5) + evCh <- format.CoreStreamEvent{Type: format.CoreEventCreated, ItemID: "resp_1"} + evCh <- format.CoreStreamEvent{ + Type: format.CoreContentBlockStarted, + Index: 0, + ContentBlock: &format.CoreContentBlock{ + Type: "reasoning", + }, + } + evCh <- format.CoreStreamEvent{Type: format.CoreTextDelta, Index: 0, Delta: "inspect stream"} + evCh <- format.CoreStreamEvent{ + Type: format.CoreContentBlockDone, + Index: 0, + ContentBlock: &format.CoreContentBlock{ + Type: "reasoning", + ReasoningSignature: "sig_1", + }, + } + evCh <- format.CoreStreamEvent{Type: format.CoreEventCompleted} + close(evCh) + + streamAny, err := adapter.FromCoreStream(context.Background(), coreReq, evCh) + if err != nil { + t.Fatal(err) + } + result := streamAny.(*openai.OpenAIStreamResult) + + var added, textDone, partDone, itemDone bool + for ev := range result.Chan() { + switch ev.Event { + case "response.reasoning_summary_part.added": + data := ev.Data.(openai.ReasoningSummaryPartAddedEvent) + if data.Part.Type != "summary_text" || data.Part.Text != "" { + t.Fatalf("added part = %+v, want empty summary_text", data.Part) + } + added = true + case "response.output_item.added": + data := ev.Data.(openai.OutputItemEvent) + if data.Item.Type == "reasoning" && (len(data.Item.Summary) != 1 || data.Item.Summary[0].Type != "summary_text" || data.Item.Summary[0].Text != "") { + t.Fatalf("initial reasoning item = %+v, want one empty summary_text part", data.Item) + } + case "response.reasoning_summary_text.done": + data := ev.Data.(openai.ReasoningSummaryTextDoneEvent) + if data.Text != "inspect stream" { + t.Fatalf("done text = %q", data.Text) + } + textDone = true + case "response.reasoning_summary_part.done": + data := ev.Data.(openai.ReasoningSummaryPartDoneEvent) + if data.Part.Type != "summary_text" || data.Part.Text != "inspect stream" || data.Part.Signature != "sig_1" { + t.Fatalf("done part = %+v", data.Part) + } + partDone = true + case "response.output_item.done": + data := ev.Data.(openai.OutputItemEvent) + if data.Item.Type == "reasoning" { + if data.Item.Status != "completed" || len(data.Item.Summary) != 1 || data.Item.Summary[0].Type != "summary_text" { + t.Fatalf("completed reasoning item = %+v", data.Item) + } + itemDone = true + } + } + } + if !added || !textDone || !partDone || !itemDone { + t.Fatalf("reasoning lifecycle incomplete: added=%t textDone=%t partDone=%t itemDone=%t", added, textDone, partDone, itemDone) + } +} + +func TestTwoTurnStream_ReplaysReasoningBeforeToolUseWithoutSessionState(t *testing.T) { + client := openai.NewOpenAIAdapter(format.CorePluginHooks{}) + firstReq := &format.CoreRequest{Model: "deepseek-v4-flash"} + events := make(chan format.CoreStreamEvent, 8) + events <- format.CoreStreamEvent{Type: format.CoreEventCreated, ItemID: "resp_1"} + events <- format.CoreStreamEvent{Type: format.CoreContentBlockStarted, Index: 0, ContentBlock: &format.CoreContentBlock{Type: "reasoning"}} + events <- format.CoreStreamEvent{Type: format.CoreTextDelta, Index: 0, Delta: "exact provider thinking"} + events <- format.CoreStreamEvent{Type: format.CoreContentBlockDone, Index: 0, ContentBlock: &format.CoreContentBlock{Type: "reasoning", ReasoningSignature: "sig_exact"}} + events <- format.CoreStreamEvent{Type: format.CoreContentBlockStarted, Index: 1, ContentBlock: &format.CoreContentBlock{Type: "tool_use", ToolUseID: "call_1", ToolName: "lookup"}} + events <- format.CoreStreamEvent{Type: format.CoreToolCallArgsDone, Index: 1, Delta: `{"query":"moon"}`} + events <- format.CoreStreamEvent{Type: format.CoreEventCompleted} + close(events) + + streamAny, err := client.FromCoreStream(context.Background(), firstReq, events) + if err != nil { + t.Fatal(err) + } + var firstResp openai.Response + for event := range streamAny.(*openai.OpenAIStreamResult).Chan() { + if event.Event == "response.completed" { + firstResp = event.Data.(openai.ResponseLifecycleEvent).Response + } + } + if len(firstResp.Output) != 2 { + t.Fatalf("first output = %+v, want reasoning and tool call", firstResp.Output) + } + + var secondInput []map[string]any + encodedOutput, err := json.Marshal(firstResp.Output) + if err != nil { + t.Fatal(err) + } + if err := json.Unmarshal(encodedOutput, &secondInput); err != nil { + t.Fatal(err) + } + secondInput = append(secondInput, map[string]any{ + "type": "function_call_output", + "call_id": "call_1", + "output": "tool result", + }) + rawInput, err := json.Marshal(secondInput) + if err != nil { + t.Fatal(err) + } + + secondCore, err := client.ToCoreRequest(context.Background(), &openai.ResponsesRequest{ + Model: "deepseek-v4-flash", + Input: rawInput, + }) + if err != nil { + t.Fatal(err) + } + provider := anthropic.NewAnthropicProviderAdapter(0, noOpCacheManager{}, format.CorePluginHooks{}) + converted, err := provider.FromCoreRequest(context.Background(), secondCore) + if err != nil { + t.Fatal(err) + } + secondAnthropic := converted.(*anthropic.MessageRequest) + if len(secondAnthropic.Messages) != 3 { + t.Fatalf("second messages = %+v", secondAnthropic.Messages) + } + if got := secondAnthropic.Messages[0]; got.Role != "user" || len(got.Content) != 1 || got.Content[0].Type != "text" || got.Content[0].Text != "_" { + t.Fatalf("Anthropic placeholder = %+v", got) + } + assistant := secondAnthropic.Messages[1] + if assistant.Role != "assistant" || len(assistant.Content) != 2 { + t.Fatalf("assistant replay = %+v", assistant) + } + if got := assistant.Content[0]; got.Type != "thinking" || got.Thinking != "exact provider thinking" || got.Signature != "sig_exact" { + t.Fatalf("replayed thinking = %+v", got) + } + if got := assistant.Content[1]; got.Type != "tool_use" || got.ID != "call_1" || got.Name != "lookup" { + t.Fatalf("replayed tool use = %+v", got) + } + if got := secondAnthropic.Messages[2]; got.Role != "user" || len(got.Content) != 1 || got.Content[0].Type != "tool_result" || got.Content[0].ToolUseID != "call_1" { + t.Fatalf("tool result = %+v", got) + } +} diff --git a/internal/protocol/openai/types.go b/internal/protocol/openai/types.go index 6f82371a..21dbded1 100644 --- a/internal/protocol/openai/types.go +++ b/internal/protocol/openai/types.go @@ -255,11 +255,12 @@ type ReasoningItemSummary struct { // ReasoningSummaryPartAddedEvent is emitted when a reasoning summary part is added. type ReasoningSummaryPartAddedEvent struct { - Type string `json:"type"` - SequenceNumber int64 `json:"sequence_number"` - ItemID string `json:"item_id"` - OutputIndex int `json:"output_index"` - SummaryIndex int `json:"summary_index"` + Type string `json:"type"` + SequenceNumber int64 `json:"sequence_number"` + ItemID string `json:"item_id"` + OutputIndex int `json:"output_index"` + SummaryIndex int `json:"summary_index"` + Part ReasoningItemSummary `json:"part"` } // ReasoningSummaryTextDeltaEvent is emitted for reasoning text deltas. @@ -274,9 +275,20 @@ type ReasoningSummaryTextDeltaEvent struct { // ReasoningSummaryPartDoneEvent is emitted when a reasoning summary part is complete. type ReasoningSummaryPartDoneEvent struct { + Type string `json:"type"` + SequenceNumber int64 `json:"sequence_number"` + ItemID string `json:"item_id"` + OutputIndex int `json:"output_index"` + SummaryIndex int `json:"summary_index"` + Part ReasoningItemSummary `json:"part"` +} + +// ReasoningSummaryTextDoneEvent is emitted when a reasoning summary text is complete. +type ReasoningSummaryTextDoneEvent struct { Type string `json:"type"` SequenceNumber int64 `json:"sequence_number"` ItemID string `json:"item_id"` OutputIndex int `json:"output_index"` SummaryIndex int `json:"summary_index"` + Text string `json:"text"` } diff --git a/internal/service/server/candidate_routing_test.go b/internal/service/server/candidate_routing_test.go index b8cc95b4..1d2f7fc6 100644 --- a/internal/service/server/candidate_routing_test.go +++ b/internal/service/server/candidate_routing_test.go @@ -312,7 +312,7 @@ func TestRememberStreamResponseContentCachesDeepSeekThinkingForLaterReplay(t *te Type: "reasoning", Status: "completed", Summary: []openai.ReasoningItemSummary{{ - Type: "text", + Type: "summary_text", Text: "trace stream reasoning", Signature: "sig-trace-stream", }},