返回 DeepSeek-Reasonix
providerconv_test.go
根目录 / internal / extension / providerconv / providerconv_test.go
1 package providerconv
2
3 import (
4 "strings"
5 "testing"
6
7 "reasonix/internal/extension/protocol"
8 "reasonix/internal/provider"
9 )
10
11 // Round trips through the wire DTOs must preserve every provider-visible
12 // field and drop nothing the extension side needs.
13 func TestRequestRoundTripPreservesProviderVisibleFields(t *testing.T) {
14 temperature := 0.25
15 req := provider.Request{
16 Messages: []provider.Message{
17 {Role: provider.RoleSystem, Content: "sys"},
18 {Role: provider.RoleUser, Content: "hi", Images: []string{"data:image/png;base64,AA=="}},
19 {
20 Role: provider.RoleAssistant, Content: "prev",
21 ReasoningContent: "because", ReasoningSignature: "sig",
22 ToolCalls: []provider.ToolCall{{ID: "c1", Name: "bash", Arguments: `{"cmd":"ls"}`, ThoughtSignature: "ts"}},
23 },
24 {Role: provider.RoleTool, ToolCallID: "c1", Name: "bash", Content: "ok"},
25 },
26 Tools: []provider.ToolSchema{{
27 Name: "bash", Description: "run", Parameters: []byte(`{"type":"object"}`),
28 }},
29 Temperature: &temperature,
30 MaxTokens: 64,
31 ResponseFormat: &provider.ResponseFormat{Type: "json_object"},
32 }
33
34 back := RequestFromProtocol(RequestToProtocol(req))
35 if len(back.Messages) != len(req.Messages) || len(back.Tools) != 1 {
36 t.Fatalf("round trip = %+v", back)
37 }
38 assistant := back.Messages[2]
39 if assistant.ReasoningContent != "because" || assistant.ReasoningSignature != "sig" {
40 t.Fatalf("assistant reasoning = %+v", assistant)
41 }
42 if len(assistant.ToolCalls) != 1 || assistant.ToolCalls[0].ThoughtSignature != "ts" {
43 t.Fatalf("assistant tool calls = %+v", assistant.ToolCalls)
44 }
45 if back.Messages[1].Images[0] != "data:image/png;base64,AA==" {
46 t.Fatalf("images = %+v", back.Messages[1].Images)
47 }
48 if back.Tools[0].Name != "bash" || string(back.Tools[0].Parameters) != `{"type":"object"}` {
49 t.Fatalf("tools = %+v", back.Tools)
50 }
51 if back.Temperature == nil || *back.Temperature != temperature || back.MaxTokens != 64 {
52 t.Fatalf("scalars = %+v", back)
53 }
54 if back.ResponseFormat == nil || back.ResponseFormat.Type != "json_object" {
55 t.Fatalf("response format = %+v", back.ResponseFormat)
56 }
57 if RequestFromProtocol(RequestToProtocol(provider.Request{})).ResponseFormat != nil {
58 t.Fatal("nil response format must stay nil")
59 }
60 }
61
62 func TestUsageRoundTrip(t *testing.T) {
63 usage := &provider.Usage{
64 PromptTokens: 1, CompletionTokens: 2, TotalTokens: 3,
65 CacheHitTokens: 4, CacheMissTokens: 5, ReasoningTokens: 6, FinishReason: "stop",
66 }
67 back := UsageFromProtocol(UsageToProtocol(usage))
68 if *back != *usage {
69 t.Fatalf("usage round trip = %+v, want %+v", back, usage)
70 }
71 if UsageToProtocol(nil) != nil || UsageFromProtocol(nil) != nil {
72 t.Fatal("nil usage must stay nil")
73 }
74 }
75
76 func TestChunkFromProtocolMapsEveryType(t *testing.T) {
77 cases := []struct {
78 wire protocol.ProviderChunkType
79 want provider.ChunkType
80 }{
81 {protocol.ChunkText, provider.ChunkText},
82 {protocol.ChunkReasoning, provider.ChunkReasoning},
83 {protocol.ChunkToolCallStart, provider.ChunkToolCallStart},
84 {protocol.ChunkToolCallDelta, provider.ChunkToolCallArgsDelta},
85 {protocol.ChunkToolCall, provider.ChunkToolCall},
86 {protocol.ChunkUsage, provider.ChunkUsage},
87 {protocol.ChunkDone, provider.ChunkDone},
88 {protocol.ChunkError, provider.ChunkError},
89 }
90 for _, tc := range cases {
91 got := ChunkFromProtocol(protocol.ProviderChunk{Type: tc.wire}).Type
92 if got != tc.want {
93 t.Fatalf("type %q mapped to %v, want %v", tc.wire, got, tc.want)
94 }
95 }
96 }
97
98 func TestChunkFromProtocolErrorCodes(t *testing.T) {
99 const secret = "sk-abcdef1234567890SECRETKEY"
100 failed := ChunkFromProtocol(protocol.ProviderChunk{
101 Type: protocol.ChunkError,
102 Error: &protocol.ProviderError{Code: protocol.ProviderFailed, Message: "provider rejected api_key=" + secret},
103 })
104 if failed.Err == nil || strings.Contains(failed.Err.Error(), secret) || provider.IsStreamInterrupted(failed.Err) {
105 t.Fatalf("failed chunk = %+v", failed)
106 }
107 if !strings.Contains(failed.Err.Error(), "provider rejected api_key=") {
108 t.Fatalf("failed error lost diagnostic context: %q", failed.Err)
109 }
110 interrupted := ChunkFromProtocol(protocol.ProviderChunk{
111 Type: protocol.ChunkError,
112 Error: &protocol.ProviderError{Code: protocol.ProviderInterrupted, Message: "provider interrupted token=" + secret},
113 })
114 if !provider.IsStreamInterrupted(interrupted.Err) {
115 t.Fatalf("interrupted chunk = %+v", interrupted)
116 }
117 if strings.Contains(interrupted.Err.Error(), secret) {
118 t.Fatalf("interrupted error leaked credential: %q", interrupted.Err)
119 }
120 }
121
122 func TestDescriptorFromProtocolCopiesFields(t *testing.T) {
123 wire := protocol.ProviderDescriptor{
124 Ref: "plugin/demo/fake/x", DisplayName: "Demo", Model: "x",
125 ContextWindow: 128_000, PricingCurrency: "$",
126 CacheHitPerMillion: 0.1, InputPerMillion: 1.0, OutputPerMillion: 2.0,
127 Vision: true, Tools: true, Reasoning: true,
128 Efforts: []string{"low", "high"}, DefaultEffort: "low",
129 ToolCallReasoning: true, ReasoningRoundTrip: true, WarnOnMissingToolCallReasoning: true,
130 }
131 d := DescriptorFromProtocol(wire)
132 if d.Ref != wire.Ref || d.DisplayName != wire.DisplayName || d.Model != wire.Model ||
133 d.ContextWindow != wire.ContextWindow || d.PricingCurrency != wire.PricingCurrency ||
134 d.CacheHitPerMillion != wire.CacheHitPerMillion || d.InputPerMillion != wire.InputPerMillion ||
135 d.OutputPerMillion != wire.OutputPerMillion || d.Vision != wire.Vision || d.Tools != wire.Tools ||
136 d.Reasoning != wire.Reasoning || d.DefaultEffort != wire.DefaultEffort ||
137 d.ToolCallReasoning != wire.ToolCallReasoning || d.ReasoningRoundTrip != wire.ReasoningRoundTrip ||
138 d.WarnOnMissingToolCallReasoning != wire.WarnOnMissingToolCallReasoning ||
139 len(d.Efforts) != 2 || d.Efforts[1] != "high" {
140 t.Fatalf("descriptor = %+v", d)
141 }
142 }
143
143 lines GO