| 1 | package responses |
| 2 | |
| 3 | import ( |
| 4 | "testing" |
| 5 | |
| 6 | "reasonix/internal/provider" |
| 7 | ) |
| 8 | |
| 9 | func TestEmptyReasoningFallbackIsScopedToThinkingStatelessVendors(t *testing.T) { |
| 10 | deepseek := New(Config{Name: "deepseek", BaseURL: "https://api.deepseek.com", Model: "deepseek-v4-flash"}) |
| 11 | if !provider.AllowsEmptyReasoningFallback(deepseek) { |
| 12 | t.Fatal("DeepSeek Responses provider must accept tool turns whose output omitted reasoning") |
| 13 | } |
| 14 | mimo := New(Config{Name: "mimo", BaseURL: "https://api.xiaomimimo.com/v1", Model: "mimo-v2.5-pro"}) |
| 15 | if !provider.AllowsEmptyReasoningFallback(mimo) { |
| 16 | t.Fatal("MiMo Responses provider must accept its optional tool-call reasoning") |
| 17 | } |
| 18 | disabled := New(Config{Name: "deepseek", BaseURL: "https://api.deepseek.com", Model: "deepseek-v4-pro", Effort: "none"}) |
| 19 | if provider.RequiresToolCallReasoning(disabled) || provider.WarnOnMissingToolCallReasoning(disabled) { |
| 20 | t.Fatal("reasoning-disabled Responses provider must not require or diagnose missing reasoning") |
| 21 | } |
| 22 | unknown := New(Config{Name: "other", BaseURL: "https://example.com", Model: "m"}) |
| 23 | if provider.AllowsEmptyReasoningFallback(unknown) { |
| 24 | t.Fatal("unknown Responses endpoint must not inherit a vendor-specific empty fallback") |
| 25 | } |
| 26 | } |
| 27 | |
| 28 | func TestStatelessRequestReplaysToolPairWithoutFabricatingReasoning(t *testing.T) { |
| 29 | client := New(Config{Name: "deepseek", BaseURL: "https://api.deepseek.com", Model: "deepseek-v4-flash"}).(*client) |
| 30 | body, _, _ := client.buildRequestBody(provider.Request{Messages: []provider.Message{ |
| 31 | {Role: provider.RoleUser, Content: "run"}, |
| 32 | {Role: provider.RoleAssistant, ToolCalls: []provider.ToolCall{{ID: "call_1", Name: "echo", Arguments: `{"text":"hi"}`}}}, |
| 33 | {Role: provider.RoleTool, ToolCallID: "call_1", Name: "echo", Content: "hi"}, |
| 34 | }}) |
| 35 | |
| 36 | items := body["input"].([]map[string]any) |
| 37 | if len(items) != 3 || items[1]["type"] != "function_call" || items[2]["type"] != "function_call_output" { |
| 38 | t.Fatalf("input = %#v, want user/call/output", items) |
| 39 | } |
| 40 | for _, item := range items { |
| 41 | if item["type"] == "reasoning" { |
| 42 | t.Fatalf("missing provider reasoning was fabricated: %#v", item) |
| 43 | } |
| 44 | } |
| 45 | } |
| 46 | |
| 47 | func TestExplicitGatewayReplayContractWithoutModelGuess(t *testing.T) { |
| 48 | message := provider.Message{Role: provider.RoleAssistant, ToolCalls: []provider.ToolCall{{ID: "c", Name: "echo", Arguments: `{}`}}} |
| 49 | history := []provider.Message{{Role: provider.RoleUser, Content: "go"}, message, {Role: provider.RoleTool, Name: "echo", ToolCallID: "c", Content: "done"}} |
| 50 | for _, explicit := range []bool{false, true} { |
| 51 | cfg := Config{BaseURL: "https://gateway.example/v1", Model: "deepseek-v4-flash", Mode: "stateless", Effort: "high"} |
| 52 | if explicit { |
| 53 | cfg.Extra = map[string]any{"reasoning_protocol": "deepseek"} |
| 54 | } |
| 55 | p := New(cfg) |
| 56 | if provider.RequiresToolCallReasoning(p) != explicit { |
| 57 | t.Fatal("explicit contract not honored or model name inferred strictness") |
| 58 | } |
| 59 | if explicit { |
| 60 | if !provider.CanReplayAssistantMessage(p, message) { |
| 61 | t.Fatal("healthy missing-item fallback changed") |
| 62 | } |
| 63 | projected, changed := provider.ProjectReasoningStrippedMessages(p, history) |
| 64 | if !changed || len(projected) != 1 { |
| 65 | t.Fatalf("rejected missing proof cannot recover: %+v", projected) |
| 66 | } |
| 67 | } |
| 68 | } |
| 69 | } |
| 70 |