返回 DeepSeek-Reasonix
commandcode_retry_test.go
根目录 / internal / provider / responses / commandcode_retry_test.go
1 package responses
2
3 import (
4 "context"
5 "io"
6 "net/http"
7 "strings"
8 "testing"
9
10 "reasonix/internal/provider"
11 )
12
13 type commandCodeRoundTripFunc func(*http.Request) (*http.Response, error)
14
15 func (f commandCodeRoundTripFunc) RoundTrip(req *http.Request) (*http.Response, error) {
16 return f(req)
17 }
18
19 func TestCommandCodeTransientResponsesBadRequestRetriesTheSameBodyOnce(t *testing.T) {
20 var calls int
21 var bodies []string
22 transport := commandCodeRoundTripFunc(func(req *http.Request) (*http.Response, error) {
23 calls++
24 body, err := io.ReadAll(req.Body)
25 if err != nil {
26 t.Fatal(err)
27 }
28 bodies = append(bodies, string(body))
29 if calls == 1 {
30 return &http.Response{
31 StatusCode: http.StatusBadRequest,
32 Status: "400 Bad Request",
33 Header: make(http.Header),
34 Body: io.NopCloser(strings.NewReader(
35 `{"message":"{\"type\":\"invalid_request_error\",\"code\":\"\",\"message\":\"invalid request error trace_id: 4f8d\"}\n","type":"invalid_request_error"}`,
36 )),
37 Request: req,
38 }, nil
39 }
40 return &http.Response{
41 StatusCode: http.StatusOK,
42 Header: http.Header{"Content-Type": []string{"text/event-stream"}},
43 Body: io.NopCloser(strings.NewReader(
44 "data: {\"type\":\"response.completed\",\"response\":{\"id\":\"resp_1\",\"usage\":{\"input_tokens\":1,\"output_tokens\":1,\"total_tokens\":2}}}\n\n",
45 )),
46 Request: req,
47 }, nil
48 })
49 p := New(Config{
50 Name: "Command Code", APIKey: "key",
51 BaseURL: "https://api.commandcode.ai/provider/v1",
52 Model: "deepseek/deepseek-v4.1-flash",
53 }).(*client)
54 p.http = &http.Client{Transport: transport}
55
56 chunks := collect(t, p, provider.Request{Messages: []provider.Message{{Role: provider.RoleUser, Content: "continue"}}})
57 if calls != 2 {
58 t.Fatalf("requests = %d, want one transparent retry", calls)
59 }
60 if len(bodies) != 2 || bodies[0] != bodies[1] {
61 t.Fatalf("retry body changed:\nfirst=%s\nsecond=%s", bodies[0], bodies[1])
62 }
63 if len(chunks) == 0 || chunks[len(chunks)-1].Type != provider.ChunkDone {
64 t.Fatalf("chunks = %+v, want completed stream", chunks)
65 }
66 }
67
68 func TestCommandCodeTransientRetryRequiresTheCommandCodeResponsesHost(t *testing.T) {
69 var calls int
70 transport := commandCodeRoundTripFunc(func(req *http.Request) (*http.Response, error) {
71 calls++
72 return &http.Response{
73 StatusCode: http.StatusBadRequest,
74 Status: "400 Bad Request",
75 Header: make(http.Header),
76 Body: io.NopCloser(strings.NewReader(
77 `{"error":{"message":"invalid request error trace_id: 4f8d"}}`,
78 )),
79 Request: req,
80 }, nil
81 })
82 p := New(Config{
83 Name: "relay", APIKey: "key", BaseURL: "https://relay.example/v1", Model: "model",
84 }).(*client)
85 p.http = &http.Client{Transport: transport}
86
87 _, err := p.Stream(context.Background(), provider.Request{Messages: []provider.Message{{Role: provider.RoleUser, Content: "continue"}}})
88 if err == nil {
89 t.Fatal("transient-shaped 400 from another host must stay terminal")
90 }
91 if calls != 1 {
92 t.Fatalf("requests = %d, want no retry outside Command Code", calls)
93 }
94 }
95
96 func TestCommandCodeTransientRetryDoesNotBroadenToOtherBadRequests(t *testing.T) {
97 var calls int
98 transport := commandCodeRoundTripFunc(func(req *http.Request) (*http.Response, error) {
99 calls++
100 return &http.Response{
101 StatusCode: http.StatusBadRequest,
102 Status: "400 Bad Request",
103 Header: make(http.Header),
104 Body: io.NopCloser(strings.NewReader(
105 `{"error":{"message":"Invalid 'summary': summary is required and must be a list for reasoning."}}`,
106 )),
107 Request: req,
108 }, nil
109 })
110 p := New(Config{
111 Name: "Command Code", APIKey: "key",
112 BaseURL: "https://api.commandcode.ai/provider/v1",
113 Model: "deepseek/deepseek-v4.1-flash",
114 }).(*client)
115 p.http = &http.Client{Transport: transport}
116
117 _, err := p.Stream(context.Background(), provider.Request{Messages: []provider.Message{{Role: provider.RoleUser, Content: "continue"}}})
118 if err == nil {
119 t.Fatal("deterministic 400 must stay terminal")
120 }
121 if calls != 1 {
122 t.Fatalf("requests = %d, want no retry for a named schema error", calls)
123 }
124 }
125
125 lines GO