@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.
- package/AGENTS_INSTALL.md +9 -1
- package/README.md +11 -5
- package/SPONSORS.md +1 -1
- package/assets/sponsors/orcarouter.png +0 -0
- package/assets/sponsors/packycode.png +0 -0
- package/gui/dist/assets/index-BoBRSehJ.css +1 -0
- package/gui/dist/assets/index-C39tnjXO.js +115 -0
- package/gui/dist/index.html +2 -2
- package/gui/dist/provider-icons/packycode.svg +19 -0
- package/gui/dist/provider-icons/qoder.svg +5 -0
- package/package.json +5 -3
- package/src/adapters/anthropic.ts +31 -16
- package/src/adapters/codebuddy/adapter.ts +85 -0
- package/src/adapters/codebuddy/profiles.ts +52 -0
- package/src/adapters/coding-agent/profile.ts +100 -0
- package/src/adapters/coding-agent/protocol.ts +463 -0
- package/src/adapters/coding-agent/turn.ts +353 -0
- package/src/adapters/google.ts +15 -11
- package/src/adapters/mimo-free.ts +3 -0
- package/src/adapters/openai-chat.ts +2 -2
- package/src/adapters/openai-responses.ts +18 -11
- package/src/adapters/qoder/adapter.ts +70 -0
- package/src/adapters/qoder/live-models.ts +89 -0
- package/src/adapters/qoder/profiles.ts +36 -0
- package/src/adapters/registry.ts +12 -0
- package/src/adapters/responses-tool-schema.ts +113 -8
- package/src/claude/inbound.ts +17 -5
- package/src/cli/account-api.ts +18 -3
- package/src/cli/account-auth.ts +8 -1
- package/src/cli/account-extended.ts +2 -1
- package/src/cli/account.ts +1 -0
- package/src/cli/capabilities.ts +15 -1
- package/src/cli/dispatch.ts +2 -0
- package/src/cli/doctor.ts +40 -0
- package/src/cli/effort.ts +24 -8
- package/src/cli/help.ts +2 -0
- package/src/cli/index.ts +29 -2
- package/src/cli/models-runtime.ts +8 -3
- package/src/cli/observe.ts +13 -3
- package/src/cli/provider-runtime.ts +2 -1
- package/src/cli/registry.ts +2 -2
- package/src/cli/system-command.ts +10 -3
- package/src/cli/usage-report.ts +9 -5
- package/src/clients/config-export/zcode.ts +24 -0
- package/src/codex/account-lifecycle.ts +35 -2
- package/src/codex/account-runtime-state.ts +6 -1
- package/src/codex/account-store.ts +72 -9
- package/src/codex/account-usability.ts +3 -2
- package/src/codex/auth-api.ts +113 -26
- package/src/codex/auth-collision.ts +12 -2
- package/src/codex/auth-context.ts +96 -7
- package/src/codex/catalog/parsing.ts +23 -0
- package/src/codex/catalog/provider-fetch.ts +144 -11
- package/src/codex/catalog/sync.ts +14 -0
- package/src/codex/inject.ts +128 -30
- package/src/codex/internal/catalog-writer.ts +3 -0
- package/src/codex/journal.ts +61 -12
- package/src/codex/model-cache.ts +11 -4
- package/src/codex/native-profile-startup.ts +72 -5
- package/src/codex/native-profile-store.ts +2 -2
- package/src/codex/ocx-compaction-history.ts +226 -0
- package/src/codex/project-config-warnings.ts +3 -1
- package/src/codex/quota-auto-refresh.ts +6 -1
- package/src/codex/quota.ts +71 -15
- package/src/codex/reserve-availability.ts +21 -5
- package/src/codex/runtime.ts +45 -1
- package/src/codex/sync.ts +5 -0
- package/src/combos/index.ts +2 -0
- package/src/combos/resolve.ts +52 -0
- package/src/config.ts +59 -0
- package/src/generated/compatibility-version.json +178 -114
- package/src/images/loop.ts +1 -0
- package/src/images/xai-video-client.ts +2 -0
- package/src/integrations/registry.ts +1 -0
- package/src/lib/errors.ts +8 -0
- package/src/lib/privacy.ts +25 -0
- package/src/lib/process-control.ts +52 -8
- package/src/lib/upstream-retry.ts +1 -0
- package/src/oauth/chatgpt.ts +83 -0
- package/src/oauth/health.ts +47 -12
- package/src/oauth/index.ts +46 -8
- package/src/oauth/token-guardian.ts +32 -6
- package/src/oauth/xai.ts +151 -8
- package/src/providers/api-key-selection-capture.ts +10 -0
- package/src/providers/api-key-selection.ts +2 -7
- package/src/providers/caller-authorization.ts +36 -0
- package/src/providers/codebuddy-models.ts +184 -0
- package/src/providers/derive.ts +5 -0
- package/src/providers/free-directory.ts +26 -2
- package/src/providers/google-ai-studio-model-discovery.ts +74 -0
- package/src/providers/openai-sidecar.ts +35 -11
- package/src/providers/opencode-zen-rate-limit.ts +75 -0
- package/src/providers/qoder-models.ts +25 -0
- package/src/providers/quota.ts +15 -0
- package/src/providers/registry.ts +140 -1
- package/src/responses/compaction.ts +4 -0
- package/src/responses/task-input.ts +21 -1
- package/src/router.ts +1 -1
- package/src/server/auth-cors.ts +6 -0
- package/src/server/chat-completions.ts +30 -13
- package/src/server/chat-native.ts +10 -1
- package/src/server/claude-messages.ts +17 -7
- package/src/server/images.ts +3 -2
- package/src/server/index.ts +25 -2
- package/src/server/management/account-selection-stream.ts +13 -4
- package/src/server/management/config-routes.ts +24 -5
- package/src/server/management/logs-usage-routes.ts +5 -1
- package/src/server/management/model-rows.ts +16 -1
- package/src/server/management/native-integration-routes.ts +2 -1
- package/src/server/management/oauth-account-routes.ts +6 -2
- package/src/server/management/provider-routes.ts +33 -2
- package/src/server/management/request-history-routes.ts +4 -2
- package/src/server/management/route-registry.ts +5 -4
- package/src/server/management/shared.ts +66 -3
- package/src/server/management-api.ts +15 -1
- package/src/server/port-reclaim.ts +11 -26
- package/src/server/request-decompress.ts +91 -3
- package/src/server/request-log.ts +16 -0
- package/src/server/responses/codex-ws-wire.ts +1 -1
- package/src/server/responses/collaboration.ts +4 -9
- package/src/server/responses/compact.ts +8 -2
- package/src/server/responses/context-overflow.ts +11 -0
- package/src/server/responses/core.ts +285 -57
- package/src/server/responses/fetch-helpers.ts +18 -7
- package/src/server/responses/policy-fallback.ts +18 -2
- package/src/server/search.ts +2 -2
- package/src/service.ts +128 -9
- package/src/storage/cleanup.ts +77 -45
- package/src/types/accounts.ts +18 -0
- package/src/types/config.ts +43 -1
- package/src/types/provider.ts +56 -0
- package/src/types.ts +4 -0
- package/src/usage/log.ts +24 -0
- package/src/vision/anthropic-describe.ts +1 -0
- package/src/web-search/anthropic-executor.ts +1 -0
- package/src/web-search/loop.ts +1 -0
- package/src/web-search/ollama-executor.ts +127 -0
- package/src/web-search/passthrough-bridge.ts +761 -0
- package/src/web-search/progress-stream.ts +4 -0
- package/gui/dist/assets/index-B5r7LNHN.js +0 -115
- 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;
|
package/src/providers/quota.ts
CHANGED
|
@@ -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
|
|
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" ||
|
|
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 {
|
package/src/server/auth-cors.ts
CHANGED
|
@@ -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
|
|
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
|
-
|
|
154
|
-
|
|
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"
|
|
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
|
-
//
|
|
253
|
-
|
|
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
|
-
|
|
263
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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);
|