@bitkyc08/opencodex 2.48.0 → 2.50.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (141) hide show
  1. package/AGENTS_INSTALL.md +9 -1
  2. package/README.md +11 -5
  3. package/SPONSORS.md +1 -1
  4. package/assets/sponsors/orcarouter.png +0 -0
  5. package/assets/sponsors/packycode.png +0 -0
  6. package/gui/dist/assets/index-BoBRSehJ.css +1 -0
  7. package/gui/dist/assets/index-C39tnjXO.js +115 -0
  8. package/gui/dist/index.html +2 -2
  9. package/gui/dist/provider-icons/packycode.svg +19 -0
  10. package/gui/dist/provider-icons/qoder.svg +5 -0
  11. package/package.json +5 -3
  12. package/src/adapters/anthropic.ts +31 -16
  13. package/src/adapters/codebuddy/adapter.ts +85 -0
  14. package/src/adapters/codebuddy/profiles.ts +52 -0
  15. package/src/adapters/coding-agent/profile.ts +100 -0
  16. package/src/adapters/coding-agent/protocol.ts +463 -0
  17. package/src/adapters/coding-agent/turn.ts +353 -0
  18. package/src/adapters/google.ts +15 -11
  19. package/src/adapters/mimo-free.ts +3 -0
  20. package/src/adapters/openai-chat.ts +2 -2
  21. package/src/adapters/openai-responses.ts +18 -11
  22. package/src/adapters/qoder/adapter.ts +70 -0
  23. package/src/adapters/qoder/live-models.ts +89 -0
  24. package/src/adapters/qoder/profiles.ts +36 -0
  25. package/src/adapters/registry.ts +12 -0
  26. package/src/adapters/responses-tool-schema.ts +113 -8
  27. package/src/claude/inbound.ts +17 -5
  28. package/src/cli/account-api.ts +18 -3
  29. package/src/cli/account-auth.ts +8 -1
  30. package/src/cli/account-extended.ts +2 -1
  31. package/src/cli/account.ts +1 -0
  32. package/src/cli/capabilities.ts +15 -1
  33. package/src/cli/dispatch.ts +2 -0
  34. package/src/cli/doctor.ts +40 -0
  35. package/src/cli/effort.ts +24 -8
  36. package/src/cli/help.ts +2 -0
  37. package/src/cli/index.ts +29 -2
  38. package/src/cli/models-runtime.ts +8 -3
  39. package/src/cli/observe.ts +13 -3
  40. package/src/cli/provider-runtime.ts +2 -1
  41. package/src/cli/registry.ts +2 -2
  42. package/src/cli/system-command.ts +10 -3
  43. package/src/cli/usage-report.ts +9 -5
  44. package/src/clients/config-export/zcode.ts +24 -0
  45. package/src/codex/account-lifecycle.ts +35 -2
  46. package/src/codex/account-runtime-state.ts +6 -1
  47. package/src/codex/account-store.ts +72 -9
  48. package/src/codex/account-usability.ts +3 -2
  49. package/src/codex/auth-api.ts +113 -26
  50. package/src/codex/auth-collision.ts +12 -2
  51. package/src/codex/auth-context.ts +96 -7
  52. package/src/codex/catalog/parsing.ts +23 -0
  53. package/src/codex/catalog/provider-fetch.ts +144 -11
  54. package/src/codex/catalog/sync.ts +14 -0
  55. package/src/codex/inject.ts +128 -30
  56. package/src/codex/internal/catalog-writer.ts +3 -0
  57. package/src/codex/journal.ts +61 -12
  58. package/src/codex/model-cache.ts +11 -4
  59. package/src/codex/native-profile-startup.ts +72 -5
  60. package/src/codex/native-profile-store.ts +2 -2
  61. package/src/codex/ocx-compaction-history.ts +226 -0
  62. package/src/codex/project-config-warnings.ts +3 -1
  63. package/src/codex/quota-auto-refresh.ts +6 -1
  64. package/src/codex/quota.ts +71 -15
  65. package/src/codex/reserve-availability.ts +21 -5
  66. package/src/codex/runtime.ts +45 -1
  67. package/src/codex/sync.ts +5 -0
  68. package/src/combos/index.ts +2 -0
  69. package/src/combos/resolve.ts +52 -0
  70. package/src/config.ts +59 -0
  71. package/src/generated/compatibility-version.json +178 -114
  72. package/src/images/loop.ts +1 -0
  73. package/src/images/xai-video-client.ts +2 -0
  74. package/src/integrations/registry.ts +1 -0
  75. package/src/lib/errors.ts +8 -0
  76. package/src/lib/privacy.ts +25 -0
  77. package/src/lib/process-control.ts +52 -8
  78. package/src/lib/upstream-retry.ts +1 -0
  79. package/src/oauth/chatgpt.ts +83 -0
  80. package/src/oauth/health.ts +47 -12
  81. package/src/oauth/index.ts +46 -8
  82. package/src/oauth/token-guardian.ts +32 -6
  83. package/src/oauth/xai.ts +151 -8
  84. package/src/providers/api-key-selection-capture.ts +10 -0
  85. package/src/providers/api-key-selection.ts +2 -7
  86. package/src/providers/caller-authorization.ts +36 -0
  87. package/src/providers/codebuddy-models.ts +184 -0
  88. package/src/providers/derive.ts +5 -0
  89. package/src/providers/free-directory.ts +26 -2
  90. package/src/providers/google-ai-studio-model-discovery.ts +74 -0
  91. package/src/providers/openai-sidecar.ts +35 -11
  92. package/src/providers/opencode-zen-rate-limit.ts +75 -0
  93. package/src/providers/qoder-models.ts +25 -0
  94. package/src/providers/quota.ts +15 -0
  95. package/src/providers/registry.ts +140 -1
  96. package/src/responses/compaction.ts +4 -0
  97. package/src/responses/task-input.ts +21 -1
  98. package/src/router.ts +1 -1
  99. package/src/server/auth-cors.ts +6 -0
  100. package/src/server/chat-completions.ts +30 -13
  101. package/src/server/chat-native.ts +10 -1
  102. package/src/server/claude-messages.ts +17 -7
  103. package/src/server/images.ts +3 -2
  104. package/src/server/index.ts +25 -2
  105. package/src/server/management/account-selection-stream.ts +13 -4
  106. package/src/server/management/config-routes.ts +24 -5
  107. package/src/server/management/logs-usage-routes.ts +5 -1
  108. package/src/server/management/model-rows.ts +16 -1
  109. package/src/server/management/native-integration-routes.ts +2 -1
  110. package/src/server/management/oauth-account-routes.ts +6 -2
  111. package/src/server/management/provider-routes.ts +33 -2
  112. package/src/server/management/request-history-routes.ts +4 -2
  113. package/src/server/management/route-registry.ts +5 -4
  114. package/src/server/management/shared.ts +66 -3
  115. package/src/server/management-api.ts +15 -1
  116. package/src/server/port-reclaim.ts +11 -26
  117. package/src/server/request-decompress.ts +91 -3
  118. package/src/server/request-log.ts +16 -0
  119. package/src/server/responses/codex-ws-wire.ts +1 -1
  120. package/src/server/responses/collaboration.ts +4 -9
  121. package/src/server/responses/compact.ts +8 -2
  122. package/src/server/responses/context-overflow.ts +11 -0
  123. package/src/server/responses/core.ts +285 -57
  124. package/src/server/responses/fetch-helpers.ts +18 -7
  125. package/src/server/responses/policy-fallback.ts +18 -2
  126. package/src/server/search.ts +2 -2
  127. package/src/service.ts +128 -9
  128. package/src/storage/cleanup.ts +77 -45
  129. package/src/types/accounts.ts +18 -0
  130. package/src/types/config.ts +43 -1
  131. package/src/types/provider.ts +56 -0
  132. package/src/types.ts +4 -0
  133. package/src/usage/log.ts +24 -0
  134. package/src/vision/anthropic-describe.ts +1 -0
  135. package/src/web-search/anthropic-executor.ts +1 -0
  136. package/src/web-search/loop.ts +1 -0
  137. package/src/web-search/ollama-executor.ts +127 -0
  138. package/src/web-search/passthrough-bridge.ts +761 -0
  139. package/src/web-search/progress-stream.ts +4 -0
  140. package/gui/dist/assets/index-B5r7LNHN.js +0 -115
  141. package/gui/dist/assets/index-D5SiRo8X.css +0 -1
@@ -7,6 +7,11 @@
7
7
  * bodies and may omit `Retry-After` / `X-RateLimit-*`; when those headers are
8
8
  * present they still take precedence. Distinct from the keyless desktop
9
9
  * ~200 requests / 5h quota documented on `opencode-free`.
10
+ *
11
+ * The same module also owns the keyless free-tier admission explanation (#4121):
12
+ * Zen rejects a request that carries no `x-opencode-session` header with
13
+ * `MissingSessionID` / "OpenCode's free tier can only be used in OpenCode".
14
+ * opencodex does not synthesize that header — see {@link enrichOpenCodeZenFreeTierMessage}.
10
15
  */
11
16
  import { validateClientRetryAfterHeader } from "../lib/retry-after";
12
17
  import { registryEntryForProviderDestination } from "./registry";
@@ -100,3 +105,73 @@ export function enrichOpenCodeZenRateLimitMessage(
100
105
  + paceHint
101
106
  );
102
107
  }
108
+
109
+ /**
110
+ * Zen's keyless free tier admits only OpenCode's own client. A request without an
111
+ * `x-opencode-session` header is refused with error type `MissingSessionID` and the
112
+ * message "OpenCode's free tier can only be used in OpenCode" (#4121).
113
+ *
114
+ * Presence of the header is the whole gate — any value clears it — so opencodex could
115
+ * pass by minting one. It does not. Fabricating a session identifier and a versioned
116
+ * `opencode/<version>` User-Agent is a claim to *be* the OpenCode client, and no upstream
117
+ * contract authorizes a third-party agent to make it; an HTTP 200 obtained that way is a
118
+ * bypassed admission check, not permission. Until OpenCode publishes a third-party
119
+ * integration path for this exact keyless tier, the supported route is the keyed
120
+ * `opencode-zen` provider.
121
+ *
122
+ * Two markers are matched because the two request surfaces expose different parts of the
123
+ * upstream envelope: the Responses path forwards the bounded raw body (which carries the
124
+ * `MissingSessionID` type), while the native Chat path forwards only the parsed message.
125
+ */
126
+ const OPENCODE_ZEN_FREE_TIER_LOCK_IN = /MissingSessionID|free tier can only be used in OpenCode/i;
127
+
128
+ /** Idempotence marker — the appended guidance must not stack across enrichment layers. */
129
+ const FREE_TIER_ENRICHMENT_MARKER = "does not send a fabricated OpenCode session header";
130
+
131
+ /** True when an upstream error body is Zen's keyless free-tier admission refusal. */
132
+ export function isOpenCodeZenFreeTierLockIn(message: string, upstreamErrorType?: string | null): boolean {
133
+ if (upstreamErrorType && OPENCODE_ZEN_FREE_TIER_LOCK_IN.test(upstreamErrorType)) return true;
134
+ return OPENCODE_ZEN_FREE_TIER_LOCK_IN.test(message);
135
+ }
136
+
137
+ /**
138
+ * Replace a raw `MissingSessionID` passthrough with an explanation of the upstream
139
+ * restriction and the supported alternative. No-op for every other provider and every
140
+ * other error, and idempotent so layered enrichment cannot append it twice.
141
+ */
142
+ export function enrichOpenCodeZenFreeTierMessage(
143
+ message: string,
144
+ opts: {
145
+ providerName?: string;
146
+ baseUrl?: string;
147
+ adapter?: string;
148
+ /** Upstream `error.type`, when the caller parsed one out of the envelope. */
149
+ upstreamErrorType?: string | null;
150
+ },
151
+ ): string {
152
+ if (message.includes(FREE_TIER_ENRICHMENT_MARKER)) return message;
153
+ if (!isOpenCodeZenFreeTierLockIn(message, opts.upstreamErrorType)) return message;
154
+ if (!isOpenCodeZenRateLimitProvider(opts)) return message;
155
+ return (
156
+ `${message}`
157
+ + " OpenCode Zen's keyless free tier admits only OpenCode's own client: it refuses any"
158
+ + " request that arrives without an x-opencode-session header."
159
+ + ` opencodex ${FREE_TIER_ENRICHMENT_MARKER}, because presenting itself as the OpenCode`
160
+ + " client is a claim no upstream contract supports."
161
+ + " Use the keyed opencode-zen provider with an OpenCode Zen API key"
162
+ + " (https://opencode.ai/auth), or route this model through another provider."
163
+ + " Upstream terms: https://opencode.ai/docs/zen/."
164
+ );
165
+ }
166
+
167
+ /**
168
+ * Single entry point for Zen upstream-error guidance on the Responses wire: short-window
169
+ * rate limits first, then the keyless free-tier admission refusal. Each layer is a no-op
170
+ * outside its own case, so the composition is safe for every other upstream failure.
171
+ */
172
+ export function enrichOpenCodeZenUpstreamMessage(
173
+ message: string,
174
+ opts: Parameters<typeof enrichOpenCodeZenRateLimitMessage>[1] & { upstreamErrorType?: string | null },
175
+ ): string {
176
+ return enrichOpenCodeZenFreeTierMessage(enrichOpenCodeZenRateLimitMessage(message, opts), opts);
177
+ }
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Cold-start fallback from the official Qoder Global model documentation, verified 2026-09-03.
3
+ * The account-specific `qoder --list-models` result is authoritative whenever discovery succeeds.
4
+ */
5
+ export const QODER_GLOBAL_MODELS = [
6
+ "Qwen3.8-Max",
7
+ "Qwen3.7-Max",
8
+ "Qwen3.7-Plus",
9
+ "Kimi-K3",
10
+ "Kimi-K2.7-Code",
11
+ "GLM-5.3",
12
+ "GLM-5.2",
13
+ "DeepSeek-V4-Pro",
14
+ ] as const;
15
+
16
+ /** Live Qoder CN roster captured from the official CLI on 2026-09-03. */
17
+ export const QODER_CN_MODELS = [
18
+ "Qwen3.8-Max",
19
+ "Qwen3.8-Flash",
20
+ "Qwen3.7-Max",
21
+ "Qwen3.7-Plus",
22
+ "Qwen3.7-Flash",
23
+ ] as const;
24
+
25
+ export const QODER_REASONING_EFFORTS = ["low", "medium", "high", "xhigh", "max"] as const;
@@ -1398,6 +1398,21 @@ function parseClaudeLimit(value: unknown): { label: string; percent: number; res
1398
1398
  /** Claude's OAuth usage endpoint, probed with ONE account's own bearer token. */
1399
1399
  const anthropicUsageInflight = new Map<string, Promise<ProviderQuota | null>>();
1400
1400
 
1401
+ /**
1402
+ * Anthropic per-credential usage.
1403
+ *
1404
+ * This endpoint reports quota only. Its body carries `five_hour`, `seven_day`, the
1405
+ * model-scoped weekly buckets (`seven_day_fable`/`_opus`/`_sonnet`) and a `limits` array,
1406
+ * and **no subscription or tier field** — nor does the OAuth token response, which yields only
1407
+ * `account.uuid` and `account.email_address` (`src/oauth/anthropic.ts`). That is why
1408
+ * `OAuthAccountSummary.plan` is `null` for Anthropic rather than populated here (#3777); it is
1409
+ * a missing upstream field, not an unfinished mapping.
1410
+ *
1411
+ * A tier must not be inferred from what is here. Percentages are normalized per account, so a
1412
+ * Max x5 seat at 50% is byte-identical to a Max x20 seat at 50%, and the presence of a
1413
+ * model-scoped window tracks entitlement rather than seat size. Populate `plan` only when
1414
+ * upstream returns the tier itself.
1415
+ */
1401
1416
  async function fetchAnthropicUsageQuota(accessToken: string): Promise<ProviderQuota | null> {
1402
1417
  const joinable = anthropicUsageInflight.get(accessToken);
1403
1418
  if (joinable) return joinable;
@@ -21,6 +21,21 @@ import {
21
21
  import { cursorFastCapableBases } from "../adapters/cursor/catalog";
22
22
  import { COMMAND_CODE_MODEL_REASONING_EFFORTS } from "./command-code-efforts";
23
23
  import { isCanonicalOpenRouterTarget } from "./openrouter-routing";
24
+ import {
25
+ CODEBUDDY_CN_MODELS,
26
+ CODEBUDDY_CN_MODEL_CONTEXT_WINDOWS,
27
+ CODEBUDDY_CN_MODEL_DEFAULT_REASONING_EFFORTS,
28
+ CODEBUDDY_CN_MODEL_MAX_OUTPUT_TOKENS,
29
+ CODEBUDDY_CN_MODEL_REASONING_EFFORTS,
30
+ CODEBUDDY_CN_NO_VISION_MODELS,
31
+ CODEBUDDY_GLOBAL_MODELS,
32
+ CODEBUDDY_GLOBAL_MODEL_CONTEXT_WINDOWS,
33
+ CODEBUDDY_GLOBAL_MODEL_DEFAULT_REASONING_EFFORTS,
34
+ CODEBUDDY_GLOBAL_MODEL_MAX_OUTPUT_TOKENS,
35
+ CODEBUDDY_GLOBAL_MODEL_REASONING_EFFORTS,
36
+ CODEBUDDY_REASONING_EFFORTS,
37
+ } from "./codebuddy-models";
38
+ import { QODER_CN_MODELS, QODER_GLOBAL_MODELS, QODER_REASONING_EFFORTS } from "./qoder-models";
24
39
 
25
40
  export type ProviderAuthKind = "forward" | "oauth" | "key" | "local";
26
41
  export type MetadataModelIdNormalize = "case-insensitive";
@@ -160,6 +175,13 @@ export interface ProviderRegistryEntry {
160
175
  staticHeaders?: Record<string, string>;
161
176
  modelSuffixBracketStrip?: boolean;
162
177
  featured?: boolean;
178
+ /**
179
+ * Paid provider sponsorship under SPONSORS.md. `main` is reserved for model developers,
180
+ * `standard` for relays and gateways. The picker pins sponsor rows first (alphabetical among
181
+ * themselves) and labels them; nothing else reads this field. Routing, failover, quota, and
182
+ * defaults never consult it — that boundary is what SPONSORS.md promises users.
183
+ */
184
+ sponsor?: { tier: "main" | "standard"; url: string };
163
185
  dashboardPreset?: boolean;
164
186
  note?: string;
165
187
  dashboardUrl?: string;
@@ -1903,6 +1925,9 @@ export const PROVIDER_REGISTRY: readonly ProviderRegistryEntry[] = [
1903
1925
  authKind: "key", dashboardUrl: "https://www.orcarouter.ai/console",
1904
1926
  // The catalog is public, so a successful /models probe cannot validate a submitted key.
1905
1927
  apiKeyValidation: "unknown",
1928
+ // Standard sponsor under SPONSORS.md (agreement signed 2026-09-07). Pins the row in the
1929
+ // picker and adds the chip; nothing about routing or defaults changes.
1930
+ sponsor: { tier: "standard", url: "https://www.orcarouter.ai/?utm_source=opencodex&utm_medium=readme" },
1906
1931
  defaultModel: "openai/gpt-5.5",
1907
1932
  models: ORCAROUTER_MODELS,
1908
1933
  liveModels: true,
@@ -1915,6 +1940,25 @@ export const PROVIDER_REGISTRY: readonly ProviderRegistryEntry[] = [
1915
1940
  preserveReasoningContentModels: ORCAROUTER_TEXT_ONLY_MODELS,
1916
1941
  note: "OpenAI-compatible adaptive router. Models and multimodal capabilities are discovered live from the public chat catalog. Use the OrcaRouter account entry for PKCE login.",
1917
1942
  },
1943
+ {
1944
+ // PackyCode: API relay (packyapi.com) for Claude Code, Codex, Gemini and more. Codex traffic
1945
+ // uses the OpenAI-compatible host from their Codex/Kimi Code guides (docs.packyapi.com):
1946
+ // https://cf.api.fan/v1 — GET /v1/models answers 401 without a key, so the host is live and
1947
+ // discovery narrows to what the key's token group allows. Model ids are bare OpenAI-style
1948
+ // ids (the Codex token group lists gpt-5.5 / gpt-5.1-codex).
1949
+ // Standard sponsor under SPONSORS.md; the dashboardUrl carries their affiliate code.
1950
+ id: "packycode", label: "PackyCode", adapter: "openai-chat", baseUrl: "https://cf.api.fan/v1",
1951
+ authKind: "key", dashboardUrl: "https://www.packyapi.com/register?aff=k5KT",
1952
+ sponsor: { tier: "standard", url: "https://www.packyapi.com/register?aff=k5KT" },
1953
+ defaultModel: "gpt-5.5",
1954
+ models: ["gpt-5.5", "gpt-5.1-codex"],
1955
+ liveModels: true,
1956
+ // New key preset: opt into collision preservation so a row named `packycode` that a user
1957
+ // points at a different PackyCode host keeps its own destination instead of being pulled
1958
+ // back onto the Codex endpoint below.
1959
+ preserveCustomDestination: true,
1960
+ note: "API relay for Claude Code, Codex, Gemini and more. Create a Codex-group token at packyapi.com; live discovery lists what the token group allows.",
1961
+ },
1918
1962
  {
1919
1963
  // BizRouter: Korean enterprise LLM gateway (api.bizrouter.ai). Model ids are
1920
1964
  // vendor-namespaced (`<vendor>/<model>`) and pass through to the upstream as-is.
@@ -2974,7 +3018,7 @@ export const PROVIDER_REGISTRY: readonly ProviderRegistryEntry[] = [
2974
3018
  keyOptional: true,
2975
3019
  featured: true,
2976
3020
  liveModels: true,
2977
- note: "No key needed — public desktop tier. OpenCode currently advertises about 200 Big Pickle/free-model requests per 5 hours. The same Zen gateway can also short-window rate-limit free models at roughly 15-20 requests/minute, and may return generic 429s without Retry-After (opencodex synthesizes backoff only when that header is omitted). Free models are discovered live from Zen. Data use: per OpenCode's Zen docs (https://opencode.ai/docs/zen/), prompts sent to free models may be retained and used for training/improvement — do not send confidential material through this provider.",
3021
+ note: "No key needed, but OpenCode now gates this tier to its own client: Zen refuses any request that arrives without an x-opencode-session header (error type MissingSessionID, \"OpenCode's free tier can only be used in OpenCode\"). opencodex does not mint that header or claim an OpenCode client identity, because no upstream contract authorizes a third-party agent to present itself as OpenCode. Until OpenCode publishes a third-party integration path for the keyless tier, use the keyed opencode-zen provider instead (https://opencode.ai/auth). Quota figures for when the tier admitted a request: OpenCode advertises about 200 Big Pickle/free-model requests per 5 hours, and the same Zen gateway can short-window rate-limit free models at roughly 15-20 requests/minute, and may return generic 429s without Retry-After (opencodex synthesizes backoff only when that header is omitted). Free models are discovered live from Zen. Data use: per OpenCode's Zen docs (https://opencode.ai/docs/zen/), prompts sent to free models may be retained and used for training/improvement — do not send confidential material through this provider.",
2978
3022
  dashboardUrl: "https://opencode.ai",
2979
3023
  staticHeaders: {
2980
3024
  // Zen answers a bare runtime User-Agent (Bun/x.y.z) more aggressively than a client
@@ -3140,6 +3184,101 @@ export const PROVIDER_REGISTRY: readonly ProviderRegistryEntry[] = [
3140
3184
  },
3141
3185
  // FREEZE 2026-07-10: no public OpenAI-compatible endpoint is documented. Evidence: devlog/_plan/260710_provider_hardening/003_research_aggregators.md.
3142
3186
  { id: "gitlab-duo", label: "GitLab Duo", baseUrl: "https://cloud.gitlab.com/ai/v1/proxy/openai/v1", adapter: "openai-chat", authKind: "key", dashboardUrl: "https://gitlab.com/-/user_settings/personal_access_tokens" },
3187
+ {
3188
+ // Official Qoder Global CLI automation surface. The canonical URL is an identity boundary;
3189
+ // inference and model discovery are performed only by the installed vendor CLI. Authentication
3190
+ // uses the documented PAT environment variable and never imports desktop/session credentials.
3191
+ id: "qoder",
3192
+ label: "Qoder (Global)",
3193
+ adapter: "qoder",
3194
+ baseUrl: "https://qoder.com",
3195
+ authKind: "key",
3196
+ apiKeyValidation: "unknown",
3197
+ preserveCustomDestination: true,
3198
+ dashboardUrl: "https://qoder.com/account/integrations",
3199
+ defaultModel: "Qwen3.8-Max",
3200
+ models: [...QODER_GLOBAL_MODELS],
3201
+ liveModels: true,
3202
+ reasoningEfforts: [...QODER_REASONING_EFFORTS],
3203
+ noVisionModels: [...QODER_GLOBAL_MODELS],
3204
+ note: "Official Qoder Global CLI using QODER_PERSONAL_ACCESS_TOKEN. Models are discovered per account with `qoder --list-models`; the documented roster is a degraded fallback. The CLI runs single-turn with tools, MCP, settings hooks, and session persistence disabled. Requires `npm install -g @qoder-ai/qodercli`.",
3205
+ },
3206
+ {
3207
+ // Qoder CN is a separate credential, executable, destination, entitlement cache, and health
3208
+ // domain. It deliberately does not reuse the OAuth/private-protocol design from #3010.
3209
+ id: "qoder-cn",
3210
+ label: "Qoder CN",
3211
+ adapter: "qoder",
3212
+ baseUrl: "https://qoder.cn",
3213
+ authKind: "key",
3214
+ apiKeyValidation: "unknown",
3215
+ preserveCustomDestination: true,
3216
+ dashboardUrl: "https://qoder.cn/account/integrations",
3217
+ defaultModel: "Qwen3.8-Max",
3218
+ models: [...QODER_CN_MODELS],
3219
+ liveModels: true,
3220
+ reasoningEfforts: [...QODER_REASONING_EFFORTS],
3221
+ noVisionModels: [...QODER_CN_MODELS],
3222
+ note: "Official Qoder CN CLI using QODERCN_PERSONAL_ACCESS_TOKEN. Models are discovered per account with `qodercn --list-models`; the verified roster is a degraded fallback. The CLI runs single-turn with tools, MCP, settings hooks, and session persistence disabled. Requires `npm install -g @qodercn-ai/qoderclicn`.",
3223
+ },
3224
+ {
3225
+ // Official CodeBuddy Code CLI provider (Tencent Cloud), GLOBAL / `public` environment.
3226
+ // Transport is the vendor-documented headless CLI automation surface
3227
+ // (`codebuddy -p --output-format stream-json --tools ""`) authenticated with the official
3228
+ // `CODEBUDDY_API_KEY` (https://www.codebuddy.ai/profile/keys). It does NOT read desktop
3229
+ // session files, import desktop bearer tokens, impersonate the desktop client, or call the
3230
+ // private console endpoint — the approach closed in #687 and left in draft in #2244.
3231
+ // baseUrl is the canonical region identity: the adapter fails closed if it is overridden, so a
3232
+ // global key is never sent to the CN environment (that is the separate `codebuddy-cn` entry).
3233
+ // v1 runs tools-disabled so Codex keeps tool ownership; this provider is text/reasoning only
3234
+ // until the control-protocol tool bridge lands (see docs). Free/trial/promotional/subscription
3235
+ // credits draw from the same official API-key pool. Requires the CLI: `npm i -g @tencent-ai/codebuddy-code`.
3236
+ // GOVERNANCE: whether routing this vendor automation surface behind a proxy for a third-party
3237
+ // agent satisfies CodeBuddy's AUP is an open question flagged for maintainer security review.
3238
+ id: "codebuddy",
3239
+ label: "CodeBuddy (Global)",
3240
+ adapter: "codebuddy",
3241
+ baseUrl: "https://www.codebuddy.ai",
3242
+ authKind: "key",
3243
+ apiKeyValidation: "unknown",
3244
+ preserveCustomDestination: true,
3245
+ dashboardUrl: "https://www.codebuddy.ai/profile/keys",
3246
+ defaultModel: "default-model",
3247
+ models: CODEBUDDY_GLOBAL_MODELS,
3248
+ liveModels: false,
3249
+ modelContextWindows: CODEBUDDY_GLOBAL_MODEL_CONTEXT_WINDOWS,
3250
+ modelMaxOutputTokens: CODEBUDDY_GLOBAL_MODEL_MAX_OUTPUT_TOKENS,
3251
+ defaultMaxOutputTokens: 32_000,
3252
+ reasoningEfforts: CODEBUDDY_REASONING_EFFORTS,
3253
+ modelReasoningEfforts: CODEBUDDY_GLOBAL_MODEL_REASONING_EFFORTS,
3254
+ modelDefaultReasoningEfforts: CODEBUDDY_GLOBAL_MODEL_DEFAULT_REASONING_EFFORTS,
3255
+ note: "Official CodeBuddy Code CLI (Tencent Cloud), global/public environment. Uses the documented CODEBUDDY_API_KEY + headless CLI surface; never reads desktop sessions or private console endpoints. Region-isolated from codebuddy-cn. v1 disables CLI tools (--tools \"\") so Codex retains tool ownership: text/reasoning only for now. Requires `npm i -g @tencent-ai/codebuddy-code`. AUP/routing authorization flagged for maintainer security review.",
3256
+ },
3257
+ {
3258
+ // Official CodeBuddy Code CLI provider, CHINA / `internal` environment. Identical adapter and
3259
+ // binary as `codebuddy`; the region is fixed by the profile's CODEBUDDY_INTERNET_ENVIRONMENT
3260
+ // and this canonical baseUrl. CN key: https://copilot.tencent.com/profile/keys. The CN model
3261
+ // roster differs from Global (see codebuddy-models.ts) and is seeded separately (§八).
3262
+ id: "codebuddy-cn",
3263
+ label: "CodeBuddy (CN)",
3264
+ adapter: "codebuddy",
3265
+ baseUrl: "https://www.codebuddy.cn",
3266
+ authKind: "key",
3267
+ apiKeyValidation: "unknown",
3268
+ preserveCustomDestination: true,
3269
+ dashboardUrl: "https://copilot.tencent.com/profile/keys",
3270
+ defaultModel: "default",
3271
+ models: CODEBUDDY_CN_MODELS,
3272
+ liveModels: false,
3273
+ modelContextWindows: CODEBUDDY_CN_MODEL_CONTEXT_WINDOWS,
3274
+ modelMaxOutputTokens: CODEBUDDY_CN_MODEL_MAX_OUTPUT_TOKENS,
3275
+ defaultMaxOutputTokens: 32_000,
3276
+ reasoningEfforts: CODEBUDDY_REASONING_EFFORTS,
3277
+ modelReasoningEfforts: CODEBUDDY_CN_MODEL_REASONING_EFFORTS,
3278
+ modelDefaultReasoningEfforts: CODEBUDDY_CN_MODEL_DEFAULT_REASONING_EFFORTS,
3279
+ noVisionModels: CODEBUDDY_CN_NO_VISION_MODELS,
3280
+ note: "Official CodeBuddy Code CLI (Tencent Cloud), China/internal environment. Uses the documented CODEBUDDY_API_KEY + headless CLI surface; never reads desktop sessions or private console endpoints. Region-isolated from codebuddy (Global); credentials are never exchanged across regions. v1 disables CLI tools (--tools \"\"): text/reasoning only for now. Requires `npm i -g @tencent-ai/codebuddy-code`. AUP/routing authorization flagged for maintainer security review.",
3281
+ },
3143
3282
  ];
3144
3283
 
3145
3284
  export function providerRegistryFastWireError(
@@ -17,6 +17,10 @@
17
17
 
18
18
  export const OCX_COMPACTION_PREFIX = "ocx1:";
19
19
 
20
+ export const OCX_NATIVE_REPLAY_RECOVERY_NOTE =
21
+ "Threads compacted through a routed provider can contain OpenCodeX-owned ocx1 state. "
22
+ + "Before resuming one through native Codex, run `ocx recover-history --ocx-compaction <thread-id> --yes`.";
23
+
20
24
  /** Mirrors codex-rs core/templates/compact/prompt.md (the local-compaction instruction). */
21
25
  export const COMPACT_PROMPT = `You are performing a CONTEXT CHECKPOINT COMPACTION. Create a handoff summary for another LLM that will resume the task.
22
26
 
@@ -20,9 +20,29 @@ function supportedBlock(value: unknown): value is TaskInputBlock {
20
20
  return value.detail === undefined || (typeof value.detail === "string" && imageDetails.has(value.detail));
21
21
  }
22
22
 
23
+ /**
24
+ * Does this item carry a pairing key? A tool result is paired by `call_id`; a seed is not.
25
+ *
26
+ * Presence of the FIELD is not presence of a KEY (#3807). Codex desktop seeds a sub-agent
27
+ * thread with a lone `function_call_output` that some client builds emit with an explicit
28
+ * `call_id: null` or `""` rather than omitting it. Those values can never pair with a
29
+ * `function_call`, so treating them as a paired result sent the item to the guard in
30
+ * core.ts and answered 400 for a turn that is really external task input.
31
+ *
32
+ * A wrong-typed key (number, object) is NOT relaxed: that is malformed input rather than
33
+ * the absent-pairing seed shape, and it keeps the #3259 rejection.
34
+ */
35
+ function hasPairingKey(item: Record<string, unknown>): boolean {
36
+ if (!("call_id" in item)) return false;
37
+ const callId = item.call_id;
38
+ if (callId === null) return false;
39
+ if (typeof callId === "string") return callId.trim().length > 0;
40
+ return true;
41
+ }
42
+
23
43
  /** Recognize Codex external task input without repairing ordinary orphaned tool results. */
24
44
  export function externalTaskInputContent(item: unknown): string | OcxContentPart[] | undefined {
25
- if (!isObj(item) || item.type !== "function_call_output" || "call_id" in item) return undefined;
45
+ if (!isObj(item) || item.type !== "function_call_output" || hasPairingKey(item)) return undefined;
26
46
  if (!nonBlank(item.id) || !nonBlank(item.name) || !nonBlank(item.namespace)) return undefined;
27
47
  const output = item.output;
28
48
  if (typeof output === "string") return nonBlank(output) ? output : undefined;
package/src/router.ts CHANGED
@@ -10,7 +10,7 @@ import {
10
10
  import type { NormalizedComboConfig } from "./combos/types";
11
11
  import { hasOwnProvider } from "./config/provider-name";
12
12
  import { providerUsesKeyAuthOverride, resolveProviderApiKey } from "./providers/key-store";
13
- import { captureProviderApiKeySelection } from "./providers/api-key-selection";
13
+ import { captureProviderApiKeySelection } from "./providers/api-key-selection-capture";
14
14
  import { assertProviderDestinationAllowed } from "./lib/destination-policy";
15
15
  import { redactSecretString, redactUrlForLog } from "./lib/redact";
16
16
  import {
@@ -6,6 +6,7 @@ import {
6
6
  codexAutoStartEnabled,
7
7
  modelPreferHostedToolsConfigError,
8
8
  providerModelCostsConfigError,
9
+ providerWebSearchBridgeConfigError,
9
10
  requestPacingConfigError,
10
11
  retryOn429PolicyConfigError,
11
12
  sanitizeModelCostsForDisplay,
@@ -650,6 +651,10 @@ export function providerManagementConfigError(name: unknown, provider: unknown):
650
651
  if (requestPacingError) {
651
652
  return `provider ${JSON.stringify(redactSecretString(name))} ${requestPacingError}`;
652
653
  }
654
+ const webSearchBridgeError = providerWebSearchBridgeConfigError(raw.webSearchBridge);
655
+ if (webSearchBridgeError) {
656
+ return `provider ${JSON.stringify(redactSecretString(name))} ${webSearchBridgeError}`;
657
+ }
653
658
  const upstreamHttpVersionError = upstreamHttpVersionConfigError(raw.upstreamHttpVersion);
654
659
  if (upstreamHttpVersionError) {
655
660
  return `provider ${JSON.stringify(redactSecretString(name))} ${upstreamHttpVersionError}`;
@@ -847,6 +852,7 @@ const PROVIDER_CONFIG_FIELD_POLICY = {
847
852
  xaiResponsesDefaultVersion: "runtime",
848
853
  supportsResponsesCustomTools: "editor",
849
854
  responsesSnapshotRepair: "editor",
855
+ webSearchBridge: "editor",
850
856
  reasoningEffortMap: "editor",
851
857
  modelReasoningEffortMap: "editor",
852
858
  reasoningWireFormat: "editor",
@@ -28,7 +28,7 @@ import { resolveWireProtocolOverride } from "./adapter-resolve";
28
28
  import { resolveOpenCodeGoTransport } from "../providers/opencode-go-transport";
29
29
  import { normalizeLogConversationId, sessionLaneIdFromRequest } from "./request-log-conversation";
30
30
  import type { OcxConfig } from "../types";
31
- import { readJsonRequestBody } from "./request-decompress";
31
+ import { readJsonRequestBody, resolveInboundBodyLimitBytes } from "./request-decompress";
32
32
  import {
33
33
  addFinalRequestLog,
34
34
  httpStatusForRequestLogTerminal,
@@ -38,6 +38,9 @@ import {
38
38
  } from "./request-log";
39
39
  import { responseWithDeferredRequestLog } from "./relay";
40
40
  import { handleResponses } from "./responses";
41
+ import { providerConsumesCallerAuthorization } from "../providers/caller-authorization";
42
+ import { captureExplicitOpenAiCallerAuth } from "../providers/openai-sidecar";
43
+ import { captureCallerDirectAuth } from "../providers/caller-authorization";
41
44
  import type { AdmissionLease } from "../lib/admission";
42
45
  import type { DataPlaneAdmission } from "./auth-cors";
43
46
  import { tryClaimNativeMainProfileForTurn } from "../codex/native-main-admission";
@@ -59,9 +62,9 @@ type Rec = Record<string, unknown>;
59
62
  function isRec(v: unknown): v is Rec {
60
63
  return !!v && typeof v === "object" && !Array.isArray(v);
61
64
  }
62
- async function readChatBody(req: Request, budget: TranslatorBudget): Promise<unknown> {
65
+ async function readChatBody(req: Request, budget: TranslatorBudget, maxBytes: number): Promise<unknown> {
63
66
  try {
64
- return await readJsonRequestBody(req, budget);
67
+ return await readJsonRequestBody(req, budget, maxBytes);
65
68
  } catch (err) {
66
69
  if (isTranslatorBudgetExceededError(err)) throw err;
67
70
  throw new ChatCompletionsRequestError(err instanceof Error && err.message ? err.message : "Invalid JSON body");
@@ -103,7 +106,7 @@ async function handleChatCompletionsWithBudget(
103
106
  ): Promise<Response> {
104
107
  let chatBody: Rec;
105
108
  try {
106
- const rawBody = await readChatBody(req, translatorBudget);
109
+ const rawBody = await readChatBody(req, translatorBudget, resolveInboundBodyLimitBytes(config.maxInboundBodyBytes));
107
110
  assertChatCompletionsRoutingBody(rawBody);
108
111
  chatBody = rawBody;
109
112
  } catch (err) {
@@ -133,7 +136,8 @@ async function handleChatCompletionsWithBudget(
133
136
  // it registers (extra_headers, sent verbatim by upstream Grok). Dashboard usage
134
137
  // bucketing only — never an auth or billing signal.
135
138
  if (req.headers.get("x-opencodex-grok") === "1") logCtx.surface = "grok";
136
- let directRoute = false;
139
+ let callerAuthorizationRoute = false;
140
+ let routeMayChangeCredentialDomain = false;
137
141
  let settledRoute: ReturnType<typeof routeModel> | null = null;
138
142
  let chatNativeRoute: ReturnType<typeof routeModel> | null = null;
139
143
  try {
@@ -150,9 +154,9 @@ async function handleChatCompletionsWithBudget(
150
154
  logCtx.provider = route.providerName;
151
155
  logCtx.routeDecision = route.routeDecision;
152
156
  settledRoute = route;
153
- if (route.provider.adapter === "openai-responses") {
154
- directRoute = route.codexAccountMode === "direct";
155
- }
157
+ routeMayChangeCredentialDomain = route.combo !== undefined || route.routeKind === "policy";
158
+ callerAuthorizationRoute = !routeMayChangeCredentialDomain
159
+ && providerConsumesCallerAuthorization(route.provider);
156
160
  if (route.provider.adapter === "cursor" || route.provider.adapter === "kiro") {
157
161
  const parts: string[] = [];
158
162
  if (chatBody.messages !== undefined) parts.push(JSON.stringify(chatBody.messages));
@@ -240,17 +244,23 @@ async function handleChatCompletionsWithBudget(
240
244
  && isCodexReserveHelperUnsupported(config, settledRoute.modelId, logIds?.admission, visionDescribeTerminal)) {
241
245
  return chatCompletionsErrorResponse(400, CODEX_RESERVE_HELPER_UNSUPPORTED_MESSAGE, "invalid_request_error");
242
246
  }
247
+ const nativeCallerAuth = captureExplicitOpenAiCallerAuth(req.headers, config);
248
+ // Caller-owned only: stored-main enrichment below is sidecar authority, never Direct authority.
249
+ const callerDirectAuth = captureCallerDirectAuth(req.headers, config);
250
+ let openAiSidecarAuth = nativeCallerAuth;
243
251
  const headers = new Headers({ "content-type": "application/json" });
244
252
  // Internal bridge metadata; the Go resolver scopes and hashes it before upstream use.
245
253
  const openCodeSession = req.headers.get("x-opencode-session");
246
254
  if (openCodeSession) headers.set("x-opencode-session", openCodeSession);
247
255
  for (const name of FORWARD_HEADERS) {
248
- if (name === "authorization" && !directRoute) continue;
256
+ if (routeMayChangeCredentialDomain && (name === "authorization" || name === "chatgpt-account-id")) continue;
257
+ if (name === "authorization" && !callerAuthorizationRoute) continue;
249
258
  const value = req.headers.get(name);
250
259
  if (value) headers.set(name, value);
251
260
  }
252
- // Prefer main ChatGPT auth so OpenAI-backed sidecars remain reachable on routed turns.
253
- if (!directRoute) {
261
+ // A noncanonical caller-auth route can use stored main auth only through a sidecar snapshot.
262
+ // Later shadow/thread rewrites strip primary credentials at the actual Responses boundary.
263
+ if (!callerAuthorizationRoute || (settledRoute && !isCanonicalOpenAiForwardProvider(settledRoute.provider))) {
254
264
  // This enrichment is optional for routed/non-main providers. If native main
255
265
  // is fenced, omit it and let auth-context reject only a final physical-main
256
266
  // selection while healthy pool/provider routes continue.
@@ -259,8 +269,12 @@ async function handleChatCompletionsWithBudget(
259
269
  const { getMainAccountToken } = await import("../codex/main-account");
260
270
  const token = getMainAccountToken();
261
271
  if (token) {
262
- headers.set("authorization", `Bearer ${token.accessToken}`);
263
- headers.set("chatgpt-account-id", token.chatgptAccountId);
272
+ const mainHeaders = new Headers({ authorization: `Bearer ${token.accessToken}`, "chatgpt-account-id": token.chatgptAccountId });
273
+ openAiSidecarAuth ??= captureExplicitOpenAiCallerAuth(mainHeaders, config);
274
+ if (!callerAuthorizationRoute && !routeMayChangeCredentialDomain) {
275
+ headers.set("authorization", `Bearer ${token.accessToken}`);
276
+ headers.set("chatgpt-account-id", token.chatgptAccountId);
277
+ }
264
278
  }
265
279
  } catch {
266
280
  /* optional */
@@ -299,6 +313,9 @@ async function handleChatCompletionsWithBudget(
299
313
  addFinalRequestLog(logIds.requestId, logIds.start, logCtx, status, meta);
300
314
  };
301
315
  const upstream = await handleResponses(internalReq, config, logCtx, {
316
+ openAiSidecarAuth,
317
+ nativeCallerAuth,
318
+ callerDirectAuth,
302
319
  ...(logIds?.turnAdmissionLease ? { turnAdmissionLease: logIds.turnAdmissionLease } : {}),
303
320
  // #1686: the Chat surface translates its body and replays here, so the admission fact has
304
321
  // to ride along or a bearer-admitted Chat caller would still be refused by Direct.
@@ -40,6 +40,7 @@ import {
40
40
  } from "../providers/key-failover";
41
41
  import { fastPolicyForModel } from "../providers/service-tier";
42
42
  import { providerApiKeySelectionIsCurrent, resolveCurrentProviderApiKeyTransport } from "../providers/api-key-selection";
43
+ import { enrichOpenCodeZenFreeTierMessage } from "../providers/opencode-zen-rate-limit";
43
44
  import type { OcxProviderTransport } from "../providers/xai-transport";
44
45
  import type { RouteResult } from "../router";
45
46
  import type { OcxConfig, OcxProviderConfig } from "../types";
@@ -438,12 +439,20 @@ export async function handleNativeChatCompletions(options: HandleNativeChatOptio
438
439
  && (isCyberPolicyCode(upstreamCode) || isCyberPolicyMessage(upstreamMessage))
439
440
  ? upstreamMessage
440
441
  : detail ? `Provider error ${response.status}: ${detail}` : `Provider error ${response.status}`;
442
+ // Zen's keyless free tier refuses the request outright rather than rate-limiting it, and
443
+ // the raw `MissingSessionID` tells a user nothing about why or what to do (#4121).
444
+ const clientMessage = enrichOpenCodeZenFreeTierMessage(message, {
445
+ providerName: route.providerName,
446
+ baseUrl: route.provider.baseUrl,
447
+ adapter: route.provider.adapter,
448
+ upstreamErrorType: upstreamType,
449
+ });
441
450
  const classified = classifyError(
442
451
  response.status,
443
452
  upstreamType ?? (response.status === 401 ? "authentication_error"
444
453
  : response.status === 429 ? "rate_limit_error"
445
454
  : response.status >= 500 ? "server_error" : "invalid_request_error"),
446
- message,
455
+ clientMessage,
447
456
  );
448
457
  if (isCyberPolicyCode(upstreamCode) || classified.code === CYBER_POLICY_ERROR_CODE) {
449
458
  classified.code = CYBER_POLICY_ERROR_CODE;
@@ -34,7 +34,7 @@ import { registryEntryForProviderDestination } from "../providers/registry";
34
34
  import { evidenceFromBody } from "../routing/request-evidence";
35
35
  import { resolveWireProtocolOverride } from "./adapter-resolve";
36
36
  import type { OcxConfig } from "../types";
37
- import { readJsonRequestBody } from "./request-decompress";
37
+ import { readJsonRequestBody, resolveInboundBodyLimitBytes } from "./request-decompress";
38
38
  import { addFinalRequestLog, httpStatusForRequestLogTerminal, recordFirstOutput, type RequestLogContext, type RequestLogEntry } from "./request-log";
39
39
  import { conversationIdFromClaudeMetadata, normalizeLogConversationId, sessionLaneIdFromRequest } from "./request-log-conversation";
40
40
  import { responseWithDeferredRequestLog } from "./relay";
@@ -129,9 +129,9 @@ function claudeInboundDisabled(config: OcxConfig): Response | null {
129
129
  return null;
130
130
  }
131
131
 
132
- async function readAnthropicBody(req: Request, budget: TranslatorBudget): Promise<unknown> {
132
+ async function readAnthropicBody(req: Request, budget: TranslatorBudget, maxBytes: number): Promise<unknown> {
133
133
  try {
134
- return await readJsonRequestBody(req, budget);
134
+ return await readJsonRequestBody(req, budget, maxBytes);
135
135
  } catch (err) {
136
136
  if (isTranslatorBudgetExceededError(err)) throw err;
137
137
  throw new AnthropicRequestError(err instanceof Error && err.message ? err.message : "Invalid JSON body");
@@ -603,7 +603,7 @@ export async function fetchWithHeaderDeadline(
603
603
  ): Promise<HeaderDeadlineFetchResult> {
604
604
  const deadline = makeDeadline(timeoutMs, parent);
605
605
  try {
606
- const upstream = await fetchImpl(input, { ...init, signal: deadline.signal, timeout: 0 });
606
+ const upstream = await fetchImpl(input, { ...init, redirect: "manual", signal: deadline.signal, timeout: 0 });
607
607
  return { kind: "response", upstream };
608
608
  } catch (error) {
609
609
  if (deadline.didExpire()) return { kind: "timeout" };
@@ -655,7 +655,7 @@ async function handleClaudeMessagesWithBudget(
655
655
  let fastRow: ParsedFastRowId | null = null;
656
656
  let requestedModel = "";
657
657
  try {
658
- anthropicBody = await readAnthropicBody(req, translatorBudget);
658
+ anthropicBody = await readAnthropicBody(req, translatorBudget, resolveInboundBodyLimitBytes(config.maxInboundBodyBytes));
659
659
  // Defensive [1m] strip (devlog 138): clients normally remove the context-variant
660
660
  // marker themselves; the 1M signal we act on is the anthropic-beta header.
661
661
  // Case-insensitive — the CLI matches /\[1m\]/i (audit 021 #7).
@@ -837,6 +837,7 @@ async function handleClaudeMessagesWithBudget(
837
837
  }
838
838
 
839
839
  const headers = new Headers({ "content-type": "application/json" });
840
+ let trustedClaudeMainAuth: { authorization: string; chatgptAccountId?: string } | undefined;
840
841
  for (const name of FORWARD_HEADERS) {
841
842
  // The caller's bearer is the proxy admission token (ocx claude placeholder), never a
842
843
  // ChatGPT credential — forwarding it upstream turns into {"detail":"Unauthorized"}.
@@ -852,8 +853,13 @@ async function handleClaudeMessagesWithBudget(
852
853
  const { getMainAccountToken } = await import("../codex/main-account");
853
854
  const token = getMainAccountToken();
854
855
  if (token) {
855
- headers.set("authorization", `Bearer ${token.accessToken}`);
856
+ const authorization = `Bearer ${token.accessToken}`;
857
+ headers.set("authorization", authorization);
856
858
  headers.set("chatgpt-account-id", token.chatgptAccountId);
859
+ trustedClaudeMainAuth = {
860
+ authorization,
861
+ ...(token.chatgptAccountId ? { chatgptAccountId: token.chatgptAccountId } : {}),
862
+ };
857
863
  }
858
864
  }
859
865
  if (opencodeGoRoute) {
@@ -923,6 +929,10 @@ async function handleClaudeMessagesWithBudget(
923
929
  // would fire, disagreeing with the pre-flight decision above.
924
930
  inboundWire: "anthropic",
925
931
  stripClaudeMainAuthForNoncanonicalForward: true,
932
+ ...(trustedClaudeMainAuth ? { trustedClaudeMainAuth } : {}),
933
+ // Claude's internal stored-main enrichment is not an original caller credential.
934
+ nativeCallerAuth: null,
935
+ callerDirectAuth: null,
926
936
  translatorBudget,
927
937
  ...(logIds ? { onFirstOutput: () => recordFirstOutput(logCtx, logIds.start) } : {}),
928
938
  onNativePassthroughTerminal: status => finalizeNativeLog(httpStatusForRequestLogTerminal(status, logCtx), { terminalStatus: status, closeReason: "terminal" }),
@@ -1144,7 +1154,7 @@ export async function handleClaudeCountTokens(
1144
1154
  let body: unknown;
1145
1155
  const translatorBudget = createTranslatorBudget();
1146
1156
  try {
1147
- body = await readAnthropicBody(req, translatorBudget);
1157
+ body = await readAnthropicBody(req, translatorBudget, resolveInboundBodyLimitBytes(config.maxInboundBodyBytes));
1148
1158
  } catch (err) {
1149
1159
  if (err instanceof DesktopModelMappingUnavailableError) return desktopMappingUnavailableResponse(err);
1150
1160
  if (err instanceof AnthropicRequestError) return anthropicErrorResponse(400, err.message);