package openai import ( "encoding/json" "strings" "testing" "ds2api/internal/toolcall" "ds2api/internal/util" ) func TestBuildResponseObjectKeepsFencedToolPayloadAsText(t *testing.T) { obj := BuildResponseObject( "resp_test", "gpt-4o", "prompt", "", "```json\n{\"tool_calls\":[{\"name\":\"search\",\"input\":{\"q\":\"golang\"}}]}\n```", []string{"search"}, nil, ) outputText, _ := obj["output_text"].(string) if !strings.Contains(outputText, "\"tool_calls\"") { t.Fatalf("expected output_text to preserve fenced tool payload, got %q", outputText) } output, _ := obj["output"].([]any) if len(output) != 1 { t.Fatalf("expected one message output item, got %#v", obj["output"]) } first, _ := output[0].(map[string]any) if first["type"] != "message" { t.Fatalf("expected message output type, got %#v", first["type"]) } } // Backward-compatible alias for historical test name used in CI logs. func TestBuildResponseObjectPromotesFencedToolPayloadToFunctionCall(t *testing.T) { TestBuildResponseObjectKeepsFencedToolPayloadAsText(t) } func TestBuildResponseObjectReasoningOnlyFallsBackToOutputText(t *testing.T) { obj := BuildResponseObject( "resp_test", "gpt-4o", "prompt", "internal thinking content", "", nil, nil, ) outputText, _ := obj["output_text"].(string) if outputText == "" { t.Fatalf("expected output_text fallback from reasoning when final text is empty") } output, _ := obj["output"].([]any) if len(output) != 1 { t.Fatalf("expected one output item, got %#v", obj["output"]) } first, _ := output[0].(map[string]any) if first["type"] != "message" { t.Fatalf("expected output type message, got %#v", first["type"]) } content, _ := first["content"].([]any) if len(content) == 0 { t.Fatalf("expected reasoning content, got %#v", first["content"]) } block0, _ := content[0].(map[string]any) if block0["type"] != "reasoning" { t.Fatalf("expected first content block reasoning, got %#v", block0["type"]) } } func TestBuildResponseObjectPromotesToolCallFromThinkingWhenTextEmpty(t *testing.T) { obj := BuildResponseObject( "resp_test", "gpt-4o", "prompt", `from-thinking`, "", []string{"search"}, nil, ) output, _ := obj["output"].([]any) if len(output) != 1 { t.Fatalf("expected one output item, got %#v", obj["output"]) } first, _ := output[0].(map[string]any) if first["type"] != "function_call" { t.Fatalf("expected function_call output, got %#v", first["type"]) } } func TestBuildChatCompletionWithToolCallsCoercesSchemaDeclaredStringArguments(t *testing.T) { toolsRaw := []any{ map[string]any{ "type": "function", "function": map[string]any{ "name": "Write", "parameters": map[string]any{ "type": "object", "properties": map[string]any{ "content": map[string]any{"type": "string"}, "taskId": map[string]any{"type": "string"}, }, }, }, }, } obj := BuildChatCompletionWithToolCalls( "chat_test", "gpt-4o", "prompt", "", "", []toolcall.ParsedToolCall{{ Name: "Write", Input: map[string]any{ "content": map[string]any{"message": "hi"}, "taskId": 1, }, }}, toolsRaw, ) choices, _ := obj["choices"].([]map[string]any) message, _ := choices[0]["message"].(map[string]any) toolCalls, _ := message["tool_calls"].([]map[string]any) fn, _ := toolCalls[0]["function"].(map[string]any) args := map[string]any{} if err := json.Unmarshal([]byte(fn["arguments"].(string)), &args); err != nil { t.Fatalf("decode arguments failed: %v", err) } if args["content"] != `{"message":"hi"}` { t.Fatalf("expected content stringified by schema, got %#v", args["content"]) } if args["taskId"] != "1" { t.Fatalf("expected taskId stringified by schema, got %#v", args["taskId"]) } } func TestBuildResponseObjectWithToolCallsCoercesSchemaDeclaredStringArguments(t *testing.T) { toolsRaw := []any{ map[string]any{ "type": "function", "function": map[string]any{ "name": "Write", "parameters": map[string]any{ "type": "object", "properties": map[string]any{ "content": map[string]any{"type": "string"}, }, }, }, }, } obj := BuildResponseObjectWithToolCalls( "resp_test", "gpt-4o", "prompt", "", "", []toolcall.ParsedToolCall{{ Name: "Write", Input: map[string]any{"content": []any{"a", 1}}, }}, toolsRaw, ) output, _ := obj["output"].([]any) first, _ := output[0].(map[string]any) args := map[string]any{} if err := json.Unmarshal([]byte(first["arguments"].(string)), &args); err != nil { t.Fatalf("decode response arguments failed: %v", err) } if args["content"] != `["a",1]` { t.Fatalf("expected response content stringified by schema, got %#v", args["content"]) } } func TestBuildChatUsageForModelUsesConservativePromptCount(t *testing.T) { prompt := strings.Repeat("上下文token ", 40) usage := BuildChatUsageForModel("deepseek-v4-flash", prompt, "", "ok", 0) promptTokens, _ := usage["prompt_tokens"].(int) if promptTokens <= util.EstimateTokens(prompt) { t.Fatalf("expected conservative prompt token count > rough estimate, got=%d estimate=%d", promptTokens, util.EstimateTokens(prompt)) } totalTokens, _ := usage["total_tokens"].(int) completionTokens, _ := usage["completion_tokens"].(int) if totalTokens != promptTokens+completionTokens { t.Fatalf("expected total tokens to add up, got usage=%#v", usage) } }