diff --git a/changelog/unreleased/upstream-qoder-codex-align.md b/changelog/unreleased/upstream-qoder-codex-align.md new file mode 100644 index 00000000..3e4aeb55 --- /dev/null +++ b/changelog/unreleased/upstream-qoder-codex-align.md @@ -0,0 +1,11 @@ +### English + +- Align Qoder Cosy fingerprint with hub v1.7.3: CLI 1.1.75, desktop Cosy-Version 0.4.8, undici User-Agent, business/scene/clientip headers, lowercase `agree` data-policy; machineos on all COSY requests, hostname SSE-only. +- Bump Codex outbound cloaking User-Agent to official stable `0.164.0`. +- Pass through inbound `X-Codex-Turn-Metadata`, `X-Codex-Turn-State`, and `X-Codex-Window-Id` on Codex OAuth (non-SIWC) upstream requests, matching CLIProxyAPI’s narrow EnsureHeader set. + +### 中文 + +- Qoder Cosy 指纹对齐 hub v1.7.3:CLI 1.1.75、桌面签到 Cosy-Version 0.4.8、undici User-Agent、business/scene/clientip 头、data-policy 小写 `agree`;machineos 全 COSY 请求携带,hostname 仅 SSE。 +- Codex 出站伪装 User-Agent 升至官方稳定版 `0.164.0`。 +- Codex OAuth(非 SIWC)上游透传入站 `X-Codex-Turn-Metadata` / `X-Codex-Turn-State` / `X-Codex-Window-Id`,范围与 CLIProxyAPI EnsureHeader 一致、不扩大。 diff --git a/internal/executor/client_ua.go b/internal/executor/client_ua.go index 415cd8ee..ce9654b3 100644 --- a/internal/executor/client_ua.go +++ b/internal/executor/client_ua.go @@ -2,6 +2,7 @@ package executor import ( "context" + "net/http" "github.com/caigee-cmd/cli2api/internal/providers" ) @@ -15,3 +16,14 @@ func WithClientUserAgent(ctx context.Context, ua string) context.Context { func ClientUserAgentFromContext(ctx context.Context) string { return providers.ClientUserAgentFromContext(ctx) } + +// WithCodexClientHeaders records inbound Codex turn/window headers for the +// Codex provider to pass through on OAuth (non-SIWC) upstream requests. +func WithCodexClientHeaders(ctx context.Context, h http.Header) context.Context { + return providers.WithCodexClientHeaders(ctx, h) +} + +// CodexClientHeadersFromContext returns inbound Codex passthrough headers. +func CodexClientHeadersFromContext(ctx context.Context) http.Header { + return providers.CodexClientHeadersFromContext(ctx) +} diff --git a/internal/gateway/prepare.go b/internal/gateway/prepare.go index cbf4a8ce..ca802ef7 100644 --- a/internal/gateway/prepare.go +++ b/internal/gateway/prepare.go @@ -51,6 +51,7 @@ func (h *Handler) prepareExecution(r *http.Request, request translate.ChatReques sessionHeader = r.Header.Get("X-CLI2API-Session") prefer = h.requestedAccount(r) ctx = executor.WithClientUserAgent(r.Context(), r.Header.Get("User-Agent")) + ctx = executor.WithCodexClientHeaders(ctx, r.Header) } var logs executor.RequestStarter if h != nil && h.Logs != nil { diff --git a/internal/providers/codex/client.go b/internal/providers/codex/client.go index 8fb7c2ff..a68bdadd 100644 --- a/internal/providers/codex/client.go +++ b/internal/providers/codex/client.go @@ -771,6 +771,7 @@ func (c *Client) chatRequest(ctx context.Context, credential Credential, req tra SetChatHeaders(httpReq.Header, credential, sessionID, true) if !credential.UsesChatGPT() { applyCodexRequestHeaders(httpReq.Header, req.Model, usesResponsesLite(req.Model)) + applyCodexPassthroughHeaders(httpReq.Header, providers.CodexClientHeadersFromContext(ctx)) } return httpReq, resolved, nil } diff --git a/internal/providers/codex/credential.go b/internal/providers/codex/credential.go index c0da9fb0..07d3899c 100644 --- a/internal/providers/codex/credential.go +++ b/internal/providers/codex/credential.go @@ -53,7 +53,7 @@ const ( pathWhamUsage = "/wham/usage" // Cloaking: upstream gates on the official codex CLI UA/originator. - UserAgent = "codex-tui/0.154.0 (Mac OS 26.5.2; arm64) iTerm.app/3.6.11 (codex-tui; 0.154.0)" + UserAgent = "codex-tui/0.164.0 (Mac OS 26.5.2; arm64) iTerm.app/3.6.11 (codex-tui; 0.164.0)" Originator = "codex-tui" QuotaUnit = "rate_limit_window" diff --git a/internal/providers/codex/headers.go b/internal/providers/codex/headers.go index b346803b..87bff896 100644 --- a/internal/providers/codex/headers.go +++ b/internal/providers/codex/headers.go @@ -3,6 +3,8 @@ package codex import ( "net/http" "strings" + + "github.com/caigee-cmd/cli2api/internal/providers" ) // SetChatHeaders applies the upstream headers the ChatGPT codex backend expects. @@ -59,3 +61,17 @@ func applyCodexRequestHeaders(h http.Header, model string, lite bool) { h.Set(codexResponsesLiteHeader, "true") } } + +// applyCodexPassthroughHeaders copies inbound turn/window headers onto the +// upstream request (CLIProxyAPI EnsureHeader for Turn-Metadata/State/Window-Id). +// Empty values are skipped. Do not call on the SIWC path. +func applyCodexPassthroughHeaders(dst, src http.Header) { + if dst == nil || src == nil { + return + } + for _, name := range providers.CodexPassthroughHeaderNames { + if v := strings.TrimSpace(src.Get(name)); v != "" { + dst.Set(name, v) + } + } +} diff --git a/internal/providers/codex/headers_passthrough_test.go b/internal/providers/codex/headers_passthrough_test.go new file mode 100644 index 00000000..8f2f68c0 --- /dev/null +++ b/internal/providers/codex/headers_passthrough_test.go @@ -0,0 +1,64 @@ +package codex + +import ( + "net/http" + "strings" + "testing" + + "github.com/caigee-cmd/cli2api/internal/providers" +) + +func TestApplyCodexPassthroughHeaders(t *testing.T) { + src := http.Header{} + src.Set("X-Codex-Turn-State", " opaque-state ") + src.Set("X-Codex-Turn-Metadata", `{"turn_id":"t1"}`) + src.Set("X-Codex-Window-Id", "win-1") + src.Set("X-Codex-Routing-Hint", "must-not-passthrough") + src.Set("Authorization", "secret") + + dst := http.Header{} + applyCodexPassthroughHeaders(dst, src) + + if got := dst.Get("X-Codex-Turn-State"); got != "opaque-state" { + t.Fatalf("Turn-State=%q", got) + } + if got := dst.Get("X-Codex-Turn-Metadata"); got != `{"turn_id":"t1"}` { + t.Fatalf("Turn-Metadata=%q", got) + } + if got := dst.Get("X-Codex-Window-Id"); got != "win-1" { + t.Fatalf("Window-Id=%q", got) + } + if dst.Get("X-Codex-Routing-Hint") != "" || dst.Get("Authorization") != "" { + t.Fatal("must not widen passthrough scope") + } +} + +func TestApplyCodexPassthroughHeadersSkipsEmpty(t *testing.T) { + dst := http.Header{} + applyCodexPassthroughHeaders(dst, http.Header{"X-Codex-Turn-State": {" "}}) + if dst.Get("X-Codex-Turn-State") != "" { + t.Fatal("blank values must be skipped") + } + applyCodexPassthroughHeaders(dst, nil) + applyCodexPassthroughHeaders(nil, http.Header{"X-Codex-Turn-State": {"x"}}) +} + +func TestWithCodexClientHeadersFilters(t *testing.T) { + src := http.Header{} + src.Set("X-Codex-Turn-State", "s") + src.Set("X-Other", "nope") + ctx := providers.WithCodexClientHeaders(nil, src) + got := providers.CodexClientHeadersFromContext(ctx) + if got.Get("X-Codex-Turn-State") != "s" || got.Get("X-Other") != "" { + t.Fatalf("got=%v", got) + } + if providers.CodexClientHeadersFromContext(nil) != nil { + t.Fatal("nil ctx") + } +} + +func TestUserAgentPinnedToOfficialStable(t *testing.T) { + if !strings.Contains(UserAgent, "0.164.0") { + t.Fatalf("UserAgent=%q want 0.164.0", UserAgent) + } +} diff --git a/internal/providers/codex/native.go b/internal/providers/codex/native.go index b9b05009..c86fe0ff 100644 --- a/internal/providers/codex/native.go +++ b/internal/providers/codex/native.go @@ -44,6 +44,7 @@ func (c *Client) ResponsesStream(ctx context.Context, accountID string, req *tra SetChatHeaders(httpReq.Header, credential, sessionID, true) if !credential.UsesChatGPT() { applyCodexRequestHeaders(httpReq.Header, model, usesResponsesLite(model)) + applyCodexPassthroughHeaders(httpReq.Header, providers.CodexClientHeadersFromContext(ctx)) } client, err := c.httpClient(ctx, accountID) if err != nil { diff --git a/internal/providers/codex_headers.go b/internal/providers/codex_headers.go new file mode 100644 index 00000000..798897a1 --- /dev/null +++ b/internal/providers/codex_headers.go @@ -0,0 +1,50 @@ +package providers + +import ( + "context" + "net/http" + "strings" +) + +type codexClientHeadersKey struct{} + +// CodexPassthroughHeaderNames are inbound headers CLIProxyAPI EnsureHeader +// forwards onto ChatGPT Codex OAuth requests for turn continuity. Kept +// narrow: Routing-Hint / Responses-Lite are set by the adapter itself; +// Session-Id is managed locally; SIWC does not receive these. +var CodexPassthroughHeaderNames = []string{ + "X-Codex-Turn-Metadata", + "X-Codex-Turn-State", + "X-Codex-Window-Id", +} + +// WithCodexClientHeaders records selected inbound Codex headers so the +// Codex adapter can pass them through on non-SIWC upstream requests. +func WithCodexClientHeaders(ctx context.Context, src http.Header) context.Context { + if ctx == nil { + ctx = context.Background() + } + if src == nil { + return ctx + } + out := http.Header{} + for _, name := range CodexPassthroughHeaderNames { + if v := strings.TrimSpace(src.Get(name)); v != "" { + out.Set(name, v) + } + } + if len(out) == 0 { + return ctx + } + return context.WithValue(ctx, codexClientHeadersKey{}, out) +} + +// CodexClientHeadersFromContext returns the inbound Codex passthrough +// headers set by WithCodexClientHeaders, or nil when unset. +func CodexClientHeadersFromContext(ctx context.Context) http.Header { + if ctx == nil { + return nil + } + h, _ := ctx.Value(codexClientHeadersKey{}).(http.Header) + return h +} diff --git a/internal/providers/qoder/checkin.go b/internal/providers/qoder/checkin.go index 01cf7dfd..43c79aab 100644 --- a/internal/providers/qoder/checkin.go +++ b/internal/providers/qoder/checkin.go @@ -140,7 +140,7 @@ func (b *Bridge) Checkin(ctx context.Context) (providers.CheckinResult, error) { req.Header.Set("Content-Type", "application/json") req.Header.Set("User-Agent", "Qoder") req.Header.Set("Cosy-ClientType", "10") - req.Header.Set("Cosy-Version", "0.3.4") + req.Header.Set("Cosy-Version", qauth.DesktopCosyVersion) req.Header.Set("Cosy-MachineId", machineID) req.Header.Set("Cosy-MachineToken", machineToken) req.Header.Set("Cosy-MachineType", machineType) diff --git a/internal/providers/qoder/checkin_inprocess_test.go b/internal/providers/qoder/checkin_inprocess_test.go index ad12c634..f7e04be0 100644 --- a/internal/providers/qoder/checkin_inprocess_test.go +++ b/internal/providers/qoder/checkin_inprocess_test.go @@ -105,8 +105,8 @@ func TestBridgeCheckinHappyPath(t *testing.T) { if got := gotHeaders.Get("Cosy-ClientType"); got != "10" { t.Fatalf("Cosy-ClientType = %q want 10", got) } - if got := gotHeaders.Get("Cosy-Version"); got != "0.3.4" { - t.Fatalf("Cosy-Version = %q want 0.3.4", got) + if got := gotHeaders.Get("Cosy-Version"); got != "0.4.8" { + t.Fatalf("Cosy-Version = %q want 0.4.8", got) } for _, key := range []string{"Cosy-Machineid", "Cosy-Machinetoken", "Cosy-Machinetype", "Cosy-Machinecode"} { if gotHeaders.Get(key) == "" { diff --git a/internal/providers/qoder/cosy.go b/internal/providers/qoder/cosy.go index 47bcb678..8442ffac 100644 --- a/internal/providers/qoder/cosy.go +++ b/internal/providers/qoder/cosy.go @@ -164,26 +164,30 @@ func (s *Session) Sign(body, rawURL string, now time.Time) (headers map[string]s sum := md5.Sum([]byte(raw)) bearer := "Bearer COSY." + payload + "." + hex.EncodeToString(sum[:]) return map[string]string{ - "Accept": "text/event-stream", - "Authorization": bearer, - "Cache-Control": "no-cache", - "Content-Type": "application/json", - "Cosy-ClientType": "5", - "Cosy-Data-Policy": "AGREE", - "Cosy-Date": date, - "Cosy-Key": s.cosyKey, - "Cosy-MachineId": s.MachineID, - "Cosy-MachineToken": s.MachineToken, - "Cosy-MachineType": s.MachineType, - "Cosy-User": s.User.uid(), - "Cosy-Version": COSYVersion, - "Login-Version": "v2", - "Accept-Encoding": "identity", - // Sign is only used for inference SSE (direct chat), where qodercli - // also sends machine OS/hostname. - "User-Agent": qauth.UserAgentForURL(rawURL), - "Cosy-MachineOS": qauth.CosyMachineOS, - "Cosy-MachineHostname": qauth.MachineHostname(s.User.uid()), + "Accept": "text/event-stream", + "Authorization": bearer, + "Cache-Control": "no-cache", + "Content-Type": "application/json", + "Cosy-ClientType": "5", + "Cosy-Data-Policy": "agree", + "Cosy-Date": date, + "Cosy-Key": s.cosyKey, + "Cosy-MachineId": s.MachineID, + "Cosy-MachineToken": s.MachineToken, + "Cosy-MachineType": s.MachineType, + "Cosy-User": s.User.uid(), + "Cosy-Version": COSYVersion, + "Login-Version": "v2", + "Accept-Encoding": "identity", + "Cosy-Business-Product": "cli", + "Cosy-Business-Type": "agent", + "Cosy-Scene": "assistant", + "Cosy-Clientip": qauth.CosyClientIP, + // Inference SSE: undici UA + machine OS always; hostname SSE-only + // (applyClientFingerprint mirrors this on the bearer path). + "User-Agent": qauth.CosyUserAgent, + "Cosy-MachineOS": qauth.CosyMachineOS, + "Cosy-MachineHostname": qauth.MachineHostname(s.User.uid()), }, nil } diff --git a/internal/providers/qoder/fingerprint_test.go b/internal/providers/qoder/fingerprint_test.go index 880fc366..19e2a7a7 100644 --- a/internal/providers/qoder/fingerprint_test.go +++ b/internal/providers/qoder/fingerprint_test.go @@ -15,16 +15,23 @@ func TestDirectSignCarriesClientFingerprint(t *testing.T) { if err != nil { t.Fatal(err) } - for _, tc := range []struct{ region, ua string }{{"global", "Qoder/"}, {"cn", "QoderWork/"}} { - h, err := s.Sign("b", ChatEndpoint(tc.region), time.Unix(1, 0)) + for _, region := range []string{"global", "cn"} { + h, err := s.Sign("b", ChatEndpoint(region), time.Unix(1, 0)) if err != nil { t.Fatal(err) } - if h["User-Agent"] != tc.ua+qauth.CosyVersion || h["Cosy-Version"] != qauth.CosyVersion { - t.Fatalf("%s ua=%q ver=%q", tc.region, h["User-Agent"], h["Cosy-Version"]) + if h["User-Agent"] != qauth.CosyUserAgent || h["Cosy-Version"] != qauth.CosyVersion { + t.Fatalf("%s ua=%q ver=%q", region, h["User-Agent"], h["Cosy-Version"]) + } + if h["Cosy-Data-Policy"] != "agree" { + t.Fatalf("%s data-policy=%q", region, h["Cosy-Data-Policy"]) + } + if h["Cosy-Business-Product"] != "cli" || h["Cosy-Business-Type"] != "agent" || + h["Cosy-Scene"] != "assistant" || h["Cosy-Clientip"] != qauth.CosyClientIP { + t.Fatalf("%s business headers incomplete: %+v", region, h) } if h["Cosy-MachineOS"] != qauth.CosyMachineOS || h["Cosy-MachineHostname"] != qauth.MachineHostname("uid-fp") { - t.Fatalf("%s os/host=%q/%q", tc.region, h["Cosy-MachineOS"], h["Cosy-MachineHostname"]) + t.Fatalf("%s os/host=%q/%q", region, h["Cosy-MachineOS"], h["Cosy-MachineHostname"]) } } } diff --git a/internal/providers/qoder/qauth/fingerprint.go b/internal/providers/qoder/qauth/fingerprint.go index 50035afa..37151cb0 100644 --- a/internal/providers/qoder/qauth/fingerprint.go +++ b/internal/providers/qoder/qauth/fingerprint.go @@ -10,32 +10,49 @@ import ( // CosyVersion is the single client/protocol version cli2api presents to // Qoder: the cosy-version header, the bearer payload's cosyVersion, the chat -// body version and the User-Agent all derive from it. It mirrors the pinned -// qodercli build cli2api emulates (session_type "qodercli", product "cli"). -const CosyVersion = "1.1.32" +// body version and the OpenAPI User-Agent all derive from it. It mirrors the +// pinned qodercli build (1.1.75; hub PR #75 / v1.7.3). +const CosyVersion = "1.1.75" + +// DesktopCosyVersion is the Cosy-Version the official desktop app sends on +// campaign / check-in requests (hub desktop 0.4.8). Distinct from CosyVersion +// (CLI / reasoning). +const DesktopCosyVersion = "0.4.8" + +// CosyUserAgent is the User-Agent official qodercli 1.1.75 leaves unset so the +// bundled undici fetch supplies its default ("undici"). +const CosyUserAgent = "undici" // CosyMachineOS mirrors qodercli's `${arch}_${platform}` value for a Linux // x86_64 host, which is how cli2api is deployed. const CosyMachineOS = "x86_64_linux" -// User-Agent product names per edition. The China edition ships as -// QoderWork, the international edition as Qoder. -const ( - userAgentProductCN = "QoderWork" - userAgentProductGlobal = "Qoder" -) +// CosyClientIP is the link-local address official qodercli puts in +// cosy-clientip on outbound COSY requests. +const CosyClientIP = "169.254.198.161" -// UserAgent returns the edition-specific User-Agent. Anything that is not the -// international region is treated as the China edition (the default region). +// applyCosyBusinessHeaders adds the business / scene / client-ip headers +// qodercli 1.1.75 sends on every COSY request. +func applyCosyBusinessHeaders(h http.Header) { + h.Set("cosy-business-product", "cli") + h.Set("cosy-business-type", "agent") + h.Set("cosy-scene", "assistant") + h.Set("cosy-clientip", CosyClientIP) +} + +// UserAgent returns the COSY reasoning User-Agent. Official qodercli does not +// set UA itself; undici supplies "undici". Edition no longer splits the UA +// (hub PR #75). Region is accepted for call-site compatibility. func UserAgent(region *RegionConfig) string { - if region != nil && region.Name == Global.Name { - return userAgentProductGlobal + "/" + CosyVersion - } - return userAgentProductCN + "/" + CosyVersion + _ = region + return CosyUserAgent } -// UserAgentForURL returns the edition User-Agent for a request URL. -func UserAgentForURL(rawURL string) string { return UserAgent(regionForURL(rawURL)) } +// UserAgentForURL returns the COSY User-Agent for a request URL. +func UserAgentForURL(rawURL string) string { + _ = rawURL + return CosyUserAgent +} // regionForURL infers the edition from a request URL's host. func regionForURL(rawURL string) *RegionConfig { @@ -59,12 +76,14 @@ func MachineHostname(uid string) string { return "DESKTOP-" + strings.ToUpper(hex.EncodeToString(sum[:])[:7]) } -// applyClientFingerprint sets the edition User-Agent and, on inference SSE -// requests only (as qodercli does), cosy-machineos/cosy-machinehostname. +// applyClientFingerprint sets the undici User-Agent, always sends +// cosy-machineos (qodercli 1.1.75), and on inference SSE requests only also +// sends cosy-machinehostname. func applyClientFingerprint(h http.Header, rawURL, uid string, inferSSE bool) { - h.Set("user-agent", UserAgentForURL(rawURL)) + _ = rawURL + h.Set("user-agent", CosyUserAgent) + h.Set("cosy-machineos", CosyMachineOS) if inferSSE { - h.Set("cosy-machineos", CosyMachineOS) h.Set("cosy-machinehostname", MachineHostname(uid)) } } diff --git a/internal/providers/qoder/qauth/fingerprint_test.go b/internal/providers/qoder/qauth/fingerprint_test.go index d7ad25a9..4f4ca38f 100644 --- a/internal/providers/qoder/qauth/fingerprint_test.go +++ b/internal/providers/qoder/qauth/fingerprint_test.go @@ -9,20 +9,26 @@ import ( "testing" ) -func TestUserAgentSplitsByEdition(t *testing.T) { - if got := UserAgent(CN); got != "QoderWork/"+CosyVersion { - t.Fatalf("CN UA=%q", got) +func TestUserAgentIsUndici(t *testing.T) { + if got := UserAgent(CN); got != CosyUserAgent { + t.Fatalf("CN UA=%q want %q", got, CosyUserAgent) } - if got := UserAgent(Global); got != "Qoder/"+CosyVersion { - t.Fatalf("global UA=%q", got) + if got := UserAgent(Global); got != CosyUserAgent { + t.Fatalf("global UA=%q want %q", got, CosyUserAgent) } - if UserAgentForURL("https://api1.qoder.sh/algo/x") != UserAgent(Global) || - UserAgentForURL("https://gateway.qoder.com.cn/algo/x") != UserAgent(CN) { - t.Fatal("URL edition inference wrong") + if UserAgentForURL("https://api1.qoder.sh/algo/x") != CosyUserAgent || + UserAgentForURL("https://gateway.qoder.com.cn/algo/x") != CosyUserAgent { + t.Fatal("URL UA must be undici for both editions") } if openAPIUserAgentVersion != CosyVersion { t.Fatal("OpenAPI UA version must follow CosyVersion") } + if CosyVersion != "1.1.75" { + t.Fatalf("CosyVersion=%q want 1.1.75", CosyVersion) + } + if DesktopCosyVersion != "0.4.8" { + t.Fatalf("DesktopCosyVersion=%q want 0.4.8", DesktopCosyVersion) + } } func TestMachineHostnameStablePerAccount(t *testing.T) { @@ -53,18 +59,33 @@ func TestBearerHeadersFingerprint(t *testing.T) { sess := newTestSession(t) _, _ = CallGet(context.Background(), sess, srv.URL+"/algo/api/x") - if got.Get("Cosy-Version") != CosyVersion || strings.Contains(got.Get("User-Agent"), "Go-http-client") || - got.Get("User-Agent") != UserAgent(CN) { + if got.Get("Cosy-Version") != CosyVersion || got.Get("User-Agent") != CosyUserAgent { t.Fatalf("json headers: ver=%q ua=%q", got.Get("Cosy-Version"), got.Get("User-Agent")) } - if got.Get("Cosy-MachineOS") != "" || got.Get("Cosy-MachineHostname") != "" { - t.Fatal("machine os/hostname must be SSE-only") + if got.Get("Cosy-Data-Policy") != "agree" { + t.Fatalf("data-policy=%q want agree", got.Get("Cosy-Data-Policy")) + } + if got.Get("Cosy-Business-Product") != "cli" || got.Get("Cosy-Business-Type") != "agent" || + got.Get("Cosy-Scene") != "assistant" || got.Get("Cosy-Clientip") != CosyClientIP { + t.Fatalf("business headers missing: product=%q type=%q scene=%q ip=%q", + got.Get("Cosy-Business-Product"), got.Get("Cosy-Business-Type"), + got.Get("Cosy-Scene"), got.Get("Cosy-Clientip")) + } + // qodercli 1.1.75: machineos on all COSY requests; hostname SSE-only. + if got.Get("Cosy-MachineOS") != CosyMachineOS { + t.Fatalf("json Cosy-MachineOS=%q want %q", got.Get("Cosy-MachineOS"), CosyMachineOS) + } + if got.Get("Cosy-MachineHostname") != "" { + t.Fatal("machine hostname must be SSE-only") } _ = OpenStreamLines(context.Background(), sess, srv.URL+"/algo/api/chat", []byte(`{}`), nil, func(string) error { return nil }) if got.Get("Cosy-MachineOS") != CosyMachineOS || got.Get("Cosy-MachineHostname") != MachineHostname("uid-test") { t.Fatalf("sse headers os=%q host=%q", got.Get("Cosy-MachineOS"), got.Get("Cosy-MachineHostname")) } + if got.Get("User-Agent") != CosyUserAgent { + t.Fatalf("sse UA=%q", got.Get("User-Agent")) + } } func payloadJSON(t *testing.T, b64 string) string { diff --git a/internal/providers/qoder/qauth/qauth.go b/internal/providers/qoder/qauth/qauth.go index 7c5c6fe3..45c2fd72 100644 --- a/internal/providers/qoder/qauth/qauth.go +++ b/internal/providers/qoder/qauth/qauth.go @@ -1101,7 +1101,7 @@ func drainBody(body io.Reader) { func makeCommonHeaders(sess *SessionContext, date, bearer, accept string) http.Header { h := http.Header{} - h.Set("cosy-data-policy", "AGREE") + h.Set("cosy-data-policy", "agree") h.Set("content-type", "application/json") h.Set("cosy-machinetype", sess.MachineType) h.Set("cosy-clienttype", "5") @@ -1115,6 +1115,7 @@ func makeCommonHeaders(sess *SessionContext, date, bearer, accept string) http.H h.Set("cosy-machineid", sess.MachineID) h.Set("cosy-machinetoken", sess.MachineToken) h.Set("login-version", "v2") + applyCosyBusinessHeaders(h) return h }