1package provider
2
3import (
4 "context"
5 "io"
6 "net/http"
7 "net/http/httptest"
8 "strings"
9 "sync"
10 "testing"
11)
12
13func TestOpenCodeGoRetriesKeepRequestIdentityStable(t *testing.T) {
14 const apiKey = "synthetic-key"
15 models := []Model{
16 {Provider: ProviderOpenCodeGo, ID: "retry-responses", API: APIResponses, MaxOutput: 1024},
17 {Provider: ProviderOpenCodeGo, ID: "retry-messages", API: APIAnthropicMessages, MaxOutput: 1024},
18 {Provider: ProviderOpenCodeGo, ID: "retry-chat", API: APICompletions, MaxOutput: 1024},
19 }
20 var (
21 mu sync.Mutex
22 calls = make(map[string]int)
23 seen = make(map[string][]http.Header)
24 )
25 server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
26 switch r.URL.Path {
27 case "/v1/responses", "/v1/messages", "/v1/chat/completions":
28 default:
29 http.NotFound(w, r)
30 return
31 }
32 mu.Lock()
33 calls[r.URL.Path]++
34 attempt := calls[r.URL.Path]
35 seen[r.URL.Path] = append(seen[r.URL.Path], r.Header.Clone())
36 mu.Unlock()
37 if attempt == 1 {
38 http.Error(w, "temporary gateway failure", http.StatusInternalServerError)
39 return
40 }
41 writeTextResponse(w, r.URL.Path)
42 }))
43 defer server.Close()
44
45 client := NewOpenCodeGoWithModels(apiKey, server.URL+"/v1", models)
46 for _, model := range models {
47 req := Request{
48 Model: model.ID,
49 Context: RequestContext{
50 CacheSessionID: "cache-root",
51 ThreadID: "thread-stable",
52 TurnID: "turn-stable",
53 },
54 Messages: []Message{{Role: RoleUser, Content: []Content{TextBlock{Text: "hello"}}}},
55 }
56 if _, err := consumeTextStream(client, req); err != nil {
57 t.Fatalf("%s: %v", model.ID, err)
58 }
59 }
60
61 for _, model := range models {
62 path := map[string]string{
63 APIResponses: "/v1/responses",
64 APIAnthropicMessages: "/v1/messages",
65 APICompletions: "/v1/chat/completions",
66 }[model.API]
67 mu.Lock()
68 got := append([]http.Header(nil), seen[path]...)
69 mu.Unlock()
70 if len(got) != 2 {
71 t.Fatalf("%s attempts = %d, want initial failure and retry", path, len(got))
72 }
73 for i, headers := range got {
74 if headers.Get("x-opencode-client") != "zut" || headers.Get("user-agent") != openCodeGoUserAgent {
75 t.Fatalf("%s attempt %d identity = client %q user-agent %q", path, i+1, headers.Get("x-opencode-client"), headers.Get("user-agent"))
76 }
77 if headers.Get("x-opencode-session") != "thread-stable" || headers.Get("x-opencode-request") != "turn-stable" {
78 t.Fatalf("%s attempt %d correlation = session %q request %q", path, i+1, headers.Get("x-opencode-session"), headers.Get("x-opencode-request"))
79 }
80 }
81 }
82}
83
84func TestOpenCodeGoHeaderHelperScopesIdentity(t *testing.T) {
85 openCodeHeaders := make(http.Header)
86 setOpenCodeGoHeaders(openCodeHeaders, ProviderOpenCodeGo, RequestContext{CacheSessionID: "cache", TurnID: "turn"})
87 if openCodeHeaders.Get("x-opencode-session") != "cache" || openCodeHeaders.Get("x-opencode-request") != "turn" {
88 t.Fatalf("fallback identity = session %q request %q", openCodeHeaders.Get("x-opencode-session"), openCodeHeaders.Get("x-opencode-request"))
89 }
90 if openCodeHeaders.Get("x-opencode-client") != "zut" || openCodeHeaders.Get("user-agent") != openCodeGoUserAgent {
91 t.Fatalf("OpenCode Go identity = %v", openCodeHeaders)
92 }
93
94 ordinaryHeaders := make(http.Header)
95 setOpenCodeGoHeaders(ordinaryHeaders, ProviderOpenAI, RequestContext{ThreadID: "thread", TurnID: "turn"})
96 if ordinaryHeaders.Get("x-opencode-session") != "" || ordinaryHeaders.Get("x-opencode-request") != "" || ordinaryHeaders.Get("x-opencode-client") != "" || ordinaryHeaders.Get("user-agent") != "" {
97 t.Fatalf("ordinary provider received OpenCode headers: %v", ordinaryHeaders)
98 }
99
100 emptyHeaders := make(http.Header)
101 setOpenCodeGoHeaders(emptyHeaders, ProviderOpenCodeGo, RequestContext{})
102 if emptyHeaders.Get("x-opencode-session") != "" || emptyHeaders.Get("x-opencode-request") != "" {
103 t.Fatalf("empty context got correlation headers: %v", emptyHeaders)
104 }
105 if emptyHeaders.Get("x-opencode-client") != "zut" || emptyHeaders.Get("user-agent") != openCodeGoUserAgent {
106 t.Fatalf("empty context lost product identity: %v", emptyHeaders)
107 }
108
109 firstSession := make(http.Header)
110 secondSession := make(http.Header)
111 setOpenCodeGoHeaders(firstSession, ProviderOpenCodeGo, RequestContext{ThreadID: "thread-a"})
112 setOpenCodeGoHeaders(secondSession, ProviderOpenCodeGo, RequestContext{ThreadID: "thread-b"})
113 if firstSession.Get("x-opencode-session") == secondSession.Get("x-opencode-session") {
114 t.Fatalf("distinct conversations shared session identity %q", firstSession.Get("x-opencode-session"))
115 }
116}
117
118func TestOpenCodeGoResponsesRetainsIdentityThroughPublicTransport(t *testing.T) {
119 named := NewOpenAIResponsesNamed("synthetic-key", "https://example.test/v1", ProviderOpenCodeGo).(*renamedClient)
120 client := named.inner.(*codexClient)
121 client.SetModelMetadata(Model{Provider: ProviderOpenCodeGo, ID: "responses-model", MaxOutput: 1024})
122
123 var got *http.Request
124 client.http = openaiResponsesHTTPClient(&http.Client{Transport: roundTripFunc(func(r *http.Request) (*http.Response, error) {
125 got = r
126 return &http.Response{
127 StatusCode: http.StatusOK,
128 Header: http.Header{"Content-Type": []string{"text/event-stream"}},
129 Body: io.NopCloser(strings.NewReader("")),
130 }, nil
131 })})
132
133 stream, err := client.Stream(context.Background(), Request{
134 Model: "responses-model",
135 Context: RequestContext{ThreadID: "thread", TurnID: "turn"},
136 })
137 if err != nil {
138 t.Fatal(err)
139 }
140 for range stream {
141 }
142 if got == nil {
143 t.Fatal("request was not sent")
144 }
145 if got.Header.Get("x-opencode-client") != "zut" || got.Header.Get("user-agent") != openCodeGoUserAgent {
146 t.Fatalf("identity stripped by public Responses transport: %v", got.Header)
147 }
148 if got.Header.Get("x-opencode-session") != "thread" || got.Header.Get("x-opencode-request") != "turn" {
149 t.Fatalf("correlation stripped by public Responses transport: %v", got.Header)
150 }
151 if got.Header.Get("chatgpt-account-id") != "" || got.Header.Get("originator") != "" || got.Header.Get("openai-beta") != "" {
152 t.Fatalf("Codex identity leaked through public Responses transport: %v", got.Header)
153 }
154}