Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 48 additions & 0 deletions internal/providers/deepseek/deepseek_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,54 @@ func TestResponses_TranslatesToChatCompletions(t *testing.T) {
}
}

func TestResponses_ReplaysReasoningContentForToolCall(t *testing.T) {
var gotBody map[string]any
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if err := json.NewDecoder(r.Body).Decode(&gotBody); err != nil {
http.Error(w, "decode error", http.StatusBadRequest)
return
}
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write([]byte(`{
"id":"chatcmpl-deepseek",
"created":1,
"model":"deepseek-v4-pro",
"choices":[{"index":0,"message":{"role":"assistant","content":"done"},"finish_reason":"stop"}],
"usage":{"prompt_tokens":1,"completion_tokens":1,"total_tokens":2}
}`))
}))
defer server.Close()

var req core.ResponsesRequest
if err := json.Unmarshal([]byte(`{
"model":"deepseek-v4-pro",
"input":[
{"type":"reasoning","summary":[],"content":[{"type":"reasoning_text","text":"Need the weather."}]},
{"type":"function_call","call_id":"call_1","name":"lookup","arguments":"{}"},
{"type":"function_call_output","call_id":"call_1","output":"sunny"}
]
}`), &req); err != nil {
t.Fatalf("json.Unmarshal() error = %v", err)
}

provider := NewWithHTTPClient("deepseek-key", server.URL, server.Client(), llmclient.Hooks{})
if _, err := provider.Responses(context.Background(), &req); err != nil {
t.Fatalf("Responses() error = %v", err)
}

messages, _ := gotBody["messages"].([]any)
if len(messages) != 2 {
t.Fatalf("messages = %#v, want assistant call and tool result", gotBody["messages"])
}
assistant, _ := messages[0].(map[string]any)
if assistant["role"] != "assistant" || assistant["reasoning_content"] != "Need the weather." {
t.Fatalf("assistant = %#v", assistant)
}
if calls, _ := assistant["tool_calls"].([]any); len(calls) != 1 {
t.Fatalf("assistant tool_calls = %#v", assistant["tool_calls"])
}
}

func TestStreamResponses_TranslatesToChatCompletions(t *testing.T) {
var gotPath string
var gotBody map[string]any
Expand Down
124 changes: 121 additions & 3 deletions internal/providers/responses_adapter_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -731,7 +731,7 @@ func TestConvertResponsesRequestToChat_RejectsUnknownInputItemTypes(t *testing.T
var req core.ResponsesRequest
if err := json.Unmarshal([]byte(`{
"model":"test-model",
"input":[{"type":"reasoning","id":"rs_123","summary":[]}]
"input":[{"type":"computer_call","id":"cc_123"}]
}`), &req); err != nil {
t.Fatalf("json.Unmarshal() error = %v", err)
}
Expand All @@ -740,8 +740,97 @@ func TestConvertResponsesRequestToChat_RejectsUnknownInputItemTypes(t *testing.T
if err == nil {
t.Fatal("expected error, got nil")
}
if !strings.Contains(err.Error(), `unsupported input item type "reasoning"`) {
t.Fatalf("error = %v, want unsupported reasoning item", err)
if !strings.Contains(err.Error(), `unsupported input item type "computer_call"`) {
t.Fatalf("error = %v, want unsupported computer_call item", err)
}
}

// Reasoning from an ordinary assistant turn is accepted but omitted because
// chat providers do not need it on the following user turn.
func TestConvertResponsesRequestToChat_DropsReasoningWithoutToolCall(t *testing.T) {
var req core.ResponsesRequest
if err := json.Unmarshal([]byte(`{
"model":"test-model",
"input":[
{"type":"message","role":"user","content":"hello"},
{"type":"reasoning","id":"rs_123","summary":[{"type":"summary_text","text":"thinking..."}]},
{"type":"message","role":"assistant","content":"hi there"}
]
}`), &req); err != nil {
t.Fatalf("json.Unmarshal() error = %v", err)
}

chatReq, err := ConvertResponsesRequestToChat(&req)
if err != nil {
t.Fatalf("ConvertResponsesRequestToChat() error = %v", err)
}
if len(chatReq.Messages) != 2 {
t.Fatalf("Messages = %#v, want exactly the user and assistant messages (reasoning dropped)", chatReq.Messages)
}
if chatReq.Messages[0].Role != "user" || chatReq.Messages[1].Role != "assistant" {
t.Fatalf("Messages = %#v, want [user, assistant]", chatReq.Messages)
}
if got := chatReq.Messages[1].ExtraFields.Lookup("reasoning_content"); got != nil {
t.Fatalf("reasoning_content = %s, want omitted without a tool call", got)
}
}

// DeepSeek requires reasoning_content to be replayed on the assistant message
// that made a tool call. Codex echoes Responses output items back as input, so
// the reasoning item and function-call item must be reassembled here.
func TestConvertResponsesRequestToChat_ReplaysReasoningWithToolCall(t *testing.T) {
var req core.ResponsesRequest
if err := json.Unmarshal([]byte(`{
"model":"deepseek-v4-pro",
"input":[
{"type":"message","role":"user","content":"weather?"},
{"type":"reasoning","id":"rs_123","summary":[],"content":[{"type":"reasoning_text","text":"Need to check the weather."}]},
{"type":"message","id":"msg_123","role":"assistant","content":"I'll check."},
{"type":"function_call","call_id":"call_123","name":"lookup_weather","arguments":"{\"city\":\"Warsaw\"}"},
{"type":"function_call_output","call_id":"call_123","output":"sunny"}
]
}`), &req); err != nil {
t.Fatalf("json.Unmarshal() error = %v", err)
}

chatReq, err := ConvertResponsesRequestToChat(&req)
if err != nil {
t.Fatalf("ConvertResponsesRequestToChat() error = %v", err)
}
if len(chatReq.Messages) != 3 {
t.Fatalf("Messages = %#v, want user, assistant tool call, and tool result", chatReq.Messages)
}
assistant := chatReq.Messages[1]
if assistant.Role != "assistant" || core.ExtractTextContent(assistant.Content) != "I'll check." || len(assistant.ToolCalls) != 1 {
t.Fatalf("assistant message = %#v, want merged text and tool call", assistant)
}
var reasoning string
if err := json.Unmarshal(assistant.ExtraFields.Lookup("reasoning_content"), &reasoning); err != nil {
t.Fatalf("reasoning_content decode error = %v", err)
}
if reasoning != "Need to check the weather." {
t.Fatalf("reasoning_content = %q", reasoning)
}
if chatReq.Messages[2].Role != "tool" || chatReq.Messages[2].ToolCallID != "call_123" {
t.Fatalf("tool message = %#v", chatReq.Messages[2])
}
}

func TestConvertResponsesRequestToChat_NormalizesDeveloperRole(t *testing.T) {
tests := map[string]any{
"typed": []core.ResponsesInputElement{{Type: "message", Role: "developer", Content: "Be concise."}},
"map": []any{map[string]any{"type": "message", "role": "developer", "content": "Be concise."}},
}
for name, input := range tests {
t.Run(name, func(t *testing.T) {
chatReq, err := ConvertResponsesRequestToChat(&core.ResponsesRequest{Model: "test-model", Input: input})
if err != nil {
t.Fatalf("ConvertResponsesRequestToChat() error = %v", err)
}
if len(chatReq.Messages) != 1 || chatReq.Messages[0].Role != "system" {
t.Fatalf("Messages = %#v, want one system message", chatReq.Messages)
}
})
}
}

Expand Down Expand Up @@ -1051,6 +1140,35 @@ func TestConvertChatResponseToResponses(t *testing.T) {
}
}

func TestConvertChatResponseToResponses_PreservesRawReasoning(t *testing.T) {
resp := &core.ChatResponse{
ID: "chatcmpl-reasoning",
Model: "deepseek-v4-pro",
Created: 1,
Choices: []core.Choice{{
Message: core.ResponseMessage{
Role: "assistant",
Content: "done",
ExtraFields: core.UnknownJSONFieldsFromMap(map[string]json.RawMessage{
"reasoning_content": json.RawMessage(`"raw trace"`),
}),
},
}},
}

result := ConvertChatResponseToResponses(resp)
if len(result.Output) != 2 || result.Output[0].Type != "reasoning" || result.Output[1].Type != "message" {
t.Fatalf("Output = %#v, want reasoning then message", result.Output)
}
reasoning := result.Output[0]
if len(reasoning.Content) != 1 || reasoning.Content[0].Type != "reasoning_text" || reasoning.Content[0].Text != "raw trace" {
t.Fatalf("reasoning content = %#v", reasoning.Content)
}
if reasoning.ExtraFields.Lookup("summary") == nil {
t.Fatal("reasoning summary array missing")
}
}

func TestConvertChatResponseToResponses_PreservesStructuredAssistantContent(t *testing.T) {
resp := &core.ChatResponse{
ID: "chatcmpl-structured",
Expand Down
Loading