diff --git a/internal/adapter/claude/handler_stream_test.go b/internal/adapter/claude/handler_stream_test.go index 42358aa..ebce879 100644 --- a/internal/adapter/claude/handler_stream_test.go +++ b/internal/adapter/claude/handler_stream_test.go @@ -209,6 +209,40 @@ func TestHandleClaudeStreamRealtimeToolDetectionFromThinkingFallback(t *testing. } } +func TestHandleClaudeStreamRealtimeSkipsThinkingFallbackWhenFinalTextExists(t *testing.T) { + h := &Handler{} + resp := makeClaudeSSEHTTPResponse( + `data: {"p":"response/thinking_content","v":"{\"tool_calls\":[{\"name\":\"search\""}`, + `data: {"p":"response/thinking_content","v":",\"input\":{\"q\":\"go\"}}]}"}`, + `data: {"p":"response/content","v":"normal answer"}`, + `data: [DONE]`, + ) + rec := httptest.NewRecorder() + req := httptest.NewRequest(http.MethodPost, "/anthropic/v1/messages", nil) + + h.handleClaudeStreamRealtime(rec, req, resp, "claude-sonnet-4-5", []any{map[string]any{"role": "user", "content": "use tool"}}, true, false, []string{"search"}) + + frames := parseClaudeFrames(t, rec.Body.String()) + for _, f := range findClaudeFrames(frames, "content_block_start") { + contentBlock, _ := f.Payload["content_block"].(map[string]any) + if contentBlock["type"] == "tool_use" { + t.Fatalf("unexpected tool_use block when final text exists, body=%s", rec.Body.String()) + } + } + + foundEndTurn := false + for _, f := range findClaudeFrames(frames, "message_delta") { + delta, _ := f.Payload["delta"].(map[string]any) + if delta["stop_reason"] == "end_turn" { + foundEndTurn = true + break + } + } + if !foundEndTurn { + t.Fatalf("expected stop_reason=end_turn, body=%s", rec.Body.String()) + } +} + func TestHandleClaudeStreamRealtimeUpstreamErrorEvent(t *testing.T) { h := &Handler{} resp := makeClaudeSSEHTTPResponse( diff --git a/internal/adapter/claude/stream_runtime_finalize.go b/internal/adapter/claude/stream_runtime_finalize.go index 12d9510..18a9e2d 100644 --- a/internal/adapter/claude/stream_runtime_finalize.go +++ b/internal/adapter/claude/stream_runtime_finalize.go @@ -46,7 +46,7 @@ func (s *claudeStreamRuntime) finalize(stopReason string) { if s.bufferToolContent { detected := util.ParseToolCalls(finalText, s.toolNames) - if len(detected) == 0 && finalThinking != "" { + if len(detected) == 0 && finalText == "" && finalThinking != "" { detected = util.ParseToolCalls(finalThinking, s.toolNames) } if len(detected) > 0 { diff --git a/internal/format/claude/render.go b/internal/format/claude/render.go index 4675398..d1a486b 100644 --- a/internal/format/claude/render.go +++ b/internal/format/claude/render.go @@ -9,7 +9,7 @@ import ( func BuildMessageResponse(messageID, model string, normalizedMessages []any, finalThinking, finalText string, toolNames []string) map[string]any { detected := util.ParseToolCalls(finalText, toolNames) - if len(detected) == 0 && finalThinking != "" { + if len(detected) == 0 && finalText == "" && finalThinking != "" { detected = util.ParseToolCalls(finalThinking, toolNames) } content := make([]map[string]any, 0, 4) diff --git a/internal/format/claude/render_test.go b/internal/format/claude/render_test.go index 389eaee..38668d4 100644 --- a/internal/format/claude/render_test.go +++ b/internal/format/claude/render_test.go @@ -27,3 +27,36 @@ func TestBuildMessageResponseDetectsToolCallsFromThinkingFallback(t *testing.T) t.Fatalf("expected tool name search, got=%#v", last["name"]) } } + +func TestBuildMessageResponseSkipsThinkingFallbackWhenFinalTextExists(t *testing.T) { + resp := BuildMessageResponse( + "msg_1", + "claude-sonnet-4-5", + []any{map[string]any{"role": "user", "content": "hi"}}, + `{"tool_calls":[{"name":"search","input":{"q":"go"}}]}`, + "normal answer", + []string{"search"}, + ) + + if resp["stop_reason"] != "end_turn" { + t.Fatalf("expected stop_reason=end_turn, got=%#v", resp["stop_reason"]) + } + + content, _ := resp["content"].([]map[string]any) + foundText := false + foundTool := false + for _, block := range content { + if block["type"] == "text" && block["text"] == "normal answer" { + foundText = true + } + if block["type"] == "tool_use" { + foundTool = true + } + } + if !foundText { + t.Fatalf("expected text block with finalText, got=%#v", resp["content"]) + } + if foundTool { + t.Fatalf("unexpected tool_use block when finalText exists, got=%#v", resp["content"]) + } +}