From 8838e0aced2faa086efd5dca594d9678afff2caa Mon Sep 17 00:00:00 2001 From: toki Date: Fri, 3 Jul 2026 12:57:19 +0900 Subject: [PATCH] =?UTF-8?q?fix(openai):=20reasoning=20alias=EB=A5=BC=20?= =?UTF-8?q?=EC=B2=98=EB=A6=AC=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../adapters/openai_compat/openai_compat.go | 33 +++++++++++------ .../openai_compat/openai_compat_test.go | 35 +++++++++++++++++++ 2 files changed, 57 insertions(+), 11 deletions(-) diff --git a/apps/node/internal/adapters/openai_compat/openai_compat.go b/apps/node/internal/adapters/openai_compat/openai_compat.go index d62124e..9418ae4 100644 --- a/apps/node/internal/adapters/openai_compat/openai_compat.go +++ b/apps/node/internal/adapters/openai_compat/openai_compat.go @@ -253,6 +253,7 @@ streamResponse: if len(choice.Delta.ToolCalls) > 0 { toolCalls.AddDelta(choice.Delta.ToolCalls) } + reasoning := choice.Delta.ReasoningText() if traceStream { a.logger.Info("openai_compat provider stream parsed chunk", zap.String("run_id", spec.RunID), @@ -263,13 +264,13 @@ streamResponse: zap.Bool("content_has_open_think", hasOpenThinkTag(choice.Delta.Content)), zap.Bool("content_has_close_think", hasCloseThinkTag(choice.Delta.Content)), zap.String("content_preview", tracePreview(choice.Delta.Content, 2000)), - zap.Int("reasoning_len", len(choice.Delta.ReasoningContent)), - zap.Bool("reasoning_has_open_think", hasOpenThinkTag(choice.Delta.ReasoningContent)), - zap.Bool("reasoning_has_close_think", hasCloseThinkTag(choice.Delta.ReasoningContent)), - zap.String("reasoning_preview", tracePreview(choice.Delta.ReasoningContent, 2000)), + zap.Int("reasoning_len", len(reasoning)), + zap.Bool("reasoning_has_open_think", hasOpenThinkTag(reasoning)), + zap.Bool("reasoning_has_close_think", hasCloseThinkTag(reasoning)), + zap.String("reasoning_preview", tracePreview(reasoning, 2000)), ) } - if reasoning := choice.Delta.ReasoningContent; reasoning != "" { + if reasoning != "" { if err := sink.Emit(ctx, runtime.RuntimeEvent{ RunID: spec.RunID, Type: runtime.EventTypeReasoningDelta, @@ -754,12 +755,8 @@ type chatMessage struct { type chatChunk struct { Choices []struct { - Delta struct { - Content string `json:"content"` - ReasoningContent string `json:"reasoning_content"` - ToolCalls []any `json:"tool_calls"` - } `json:"delta"` - FinishReason *string `json:"finish_reason"` + Delta chatChunkDelta `json:"delta"` + FinishReason *string `json:"finish_reason"` } `json:"choices"` Usage *struct { PromptTokens int `json:"prompt_tokens"` @@ -767,6 +764,20 @@ type chatChunk struct { } `json:"usage"` } +type chatChunkDelta struct { + Content string `json:"content"` + ReasoningContent string `json:"reasoning_content"` + Reasoning string `json:"reasoning"` + ToolCalls []any `json:"tool_calls"` +} + +func (d chatChunkDelta) ReasoningText() string { + if d.ReasoningContent != "" { + return d.ReasoningContent + } + return d.Reasoning +} + type openAIToolCallAccumulator struct { calls map[int]map[string]any order []int diff --git a/apps/node/internal/adapters/openai_compat/openai_compat_test.go b/apps/node/internal/adapters/openai_compat/openai_compat_test.go index 46a2301..300b3e6 100644 --- a/apps/node/internal/adapters/openai_compat/openai_compat_test.go +++ b/apps/node/internal/adapters/openai_compat/openai_compat_test.go @@ -209,6 +209,41 @@ func TestOpenAICompatExecuteStreamsDeltasAndFinishReason(t *testing.T) { } } +func TestOpenAICompatExecuteStreamsReasoningAlias(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path != "/v1/chat/completions" { + t.Fatalf("unexpected path %s", r.URL.Path) + } + w.Header().Set("Content-Type", "text/event-stream") + _, _ = fmt.Fprintf(w, "data: %s\n\n", `{"choices":[{"delta":{"reasoning":"think alias "}}]}`) + _, _ = fmt.Fprintf(w, "data: %s\n\n", `{"choices":[{"delta":{"content":"answer"}}]}`) + _, _ = fmt.Fprintf(w, "data: [DONE]\n\n") + })) + defer server.Close() + + adapter := New(config.OpenAICompatConf{Endpoint: server.URL}, zap.NewNop()) + sink := &fakeSink{} + err := adapter.Execute(context.Background(), runtime.ExecutionSpec{ + RunID: "run-1", + Target: "qwen-model", + Input: map[string]any{"prompt": "say hello"}, + }, sink) + if err != nil { + t.Fatalf("Execute failed: %v", err) + } + + events := sink.all() + if len(events) != 4 { + t.Fatalf("expected 4 events, got %d: %+v", len(events), events) + } + if events[1].Type != runtime.EventTypeReasoningDelta || events[1].Delta != "think alias " { + t.Fatalf("expected reasoning alias delta, got %+v", events[1]) + } + if events[2].Type != runtime.EventTypeDelta || events[2].Delta != "answer" { + t.Fatalf("expected answer delta, got %+v", events[2]) + } +} + func TestOpenAICompatExecuteSendsHeaders(t *testing.T) { var gotAuth, gotContentType string server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {