Files
magpie/internal/gateway/codex_client_headers_test.go

108 lines
4.1 KiB
Go

package gateway
import (
"net/http"
"net/http/httptest"
"slices"
"strings"
"testing"
"github.com/yetone/magpie/internal/provider"
)
// A relay that serves only Codex (Discord: "This account only allows Codex
// official clients", sub2api's codex_cli_only: an official User-Agent or
// originator, and an x-codex- header) gets Codex's own headers as it sent
// them, the provider's key in place of magpie's, on the Responses API and
// on Chat Completions, relayed or translated
func TestCodexHeadersReachKeyedRelay(t *testing.T) {
codex := http.Header{
"User-Agent": {"codex_exec/0.159.2 (Mac OS 26.6.0; arm64) kitty (codex_exec; 0.159.2)"},
"Originator": {"codex_exec"},
"Session-Id": {"ses-1"},
"Thread-Id": {"ses-1"},
"Session_id": {"ses-1"},
"Version": {"0.159.2"},
"X-Client-Request-Id": {"ses-1"},
"X-Codex-Window-Id": {"ses-1:0"},
"X-Codex-Beta-Features": {"remote_compaction_v2"},
"X-Codex-Turn-Metadata": {`{"session_id":"ses-1","turn_id":"t1"}`},
"X-Openai-Subagent": {"review"},
"Authorization": {"Bearer " + Token},
"X-Oai-Attestation": {"att"},
"Chatgpt-Account-Id": {"acct"},
}
secret := []string{"Authorization", "X-Oai-Attestation", "Chatgpt-Account-Id"}
for _, c := range []struct {
proto provider.Protocol
path, body string
ctype string
reply string
}{
{provider.Responses, "/v1/responses", `{"model":"fake/m1","input":"hi","stream":false}`,
"application/json", `{"id":"r","object":"response","status":"completed","output":[]}`},
// Codex's default web_search makes magpie build the request again
{provider.Responses, "/v1/responses", `{"model":"fake/m1","input":"hi","stream":false,"tools":[{"type":"web_search","external_web_access":false}]}`,
"", "data: {\"type\":\"response.completed\",\"response\":{\"id\":\"r\",\"status\":\"completed\",\"output\":[]}}\n\n"},
{provider.Chat, "/v1/responses", `{"model":"fake/m1","input":"hi","stream":false}`,
"", "data: {\"id\":\"c1\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\"OK\"},\"finish_reason\":\"stop\"}]}\n\ndata: [DONE]\n\n"},
} {
f := &fake{t: t, ctype: c.ctype, reply: c.reply}
setup(t, c.proto, f)
send := func(h http.Header) {
t.Helper()
rec := httptest.NewRecorder()
req := httptest.NewRequest("POST", c.path, strings.NewReader(c.body))
for k, v := range h {
req.Header[k] = v
}
New().Handler().ServeHTTP(rec, req)
if rec.Code != 200 || f.calls == 0 {
t.Fatalf("%s %s: %d %s", c.proto, c.body, rec.Code, rec.Body.String())
}
}
send(codex)
for k, v := range codex {
if slices.Contains(secret, k) {
continue
}
if got := f.head.Get(k); got != v[0] {
t.Errorf("%s: %s: got %q, want %q", c.proto, k, got, v[0])
}
}
if a := f.head.Get("Authorization"); a != "Bearer k" {
t.Errorf("%s: auth %q", c.proto, a)
}
for _, k := range secret[1:] {
if f.head.Get(k) != "" {
t.Errorf("%s: %s leaked", c.proto, k)
}
}
// a provider with no key of its own still never sees magpie's
p, _ := provider.Find("fake")
p.Key = ""
provider.Save(*p)
send(codex)
if a := f.head.Get("Authorization"); a != "" {
t.Errorf("%s: magpie's key leaked: %q", c.proto, a)
}
if f.head.Get("Originator") != "codex_exec" {
t.Errorf("%s: originator: %v", c.proto, f.head)
}
// the desktop app is told by its originator as well
send(http.Header{"User-Agent": {"Codex Desktop/0.162.3 (Windows 10.0.26100; x86_64) unknown"}, "Originator": {"Codex Desktop"}, "X-Codex-Window-Id": {"w"}})
if f.head.Get("Originator") != "Codex Desktop" || !strings.HasPrefix(f.head.Get("User-Agent"), "Codex Desktop/") || f.head.Get("X-Codex-Window-Id") != "w" {
t.Errorf("%s: desktop: %v", c.proto, f.head)
}
// another agent's request goes as magpie's
send(http.Header{"User-Agent": {"opencode/1.0"}, "X-Codex-Window-Id": {"w"}, "Session-Id": {"s"}})
if ua := f.head.Get("User-Agent"); !strings.HasPrefix(ua, "magpie/") || f.head.Get("X-Codex-Window-Id") != "" || f.head.Get("Session-Id") != "" {
t.Errorf("%s: other agent: %q %v", c.proto, ua, f.head)
}
}
}