@bitkyc08/opencodex 2.49.0 → 2.51.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 +3 -0
- package/bin/ocx.mjs +222 -71
- package/gui/dist/assets/index-D7BdZpZm.js +115 -0
- package/gui/dist/index.html +1 -1
- package/package.json +1 -1
- package/src/adapters/qoder/adapter.ts +69 -1
- package/src/adapters/qoder/scaffold-guard.ts +233 -0
- package/src/claude/agents-inject.ts +29 -5
- package/src/claude/desktop-3p.ts +31 -3
- package/src/claude/gateway-cache.ts +12 -21
- 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 +43 -1
- package/src/cli/claude-agent-startup-sync.ts +26 -1
- package/src/cli/claude.ts +138 -20
- package/src/cli/config-command.ts +67 -1
- package/src/cli/connect.ts +181 -14
- package/src/cli/dispatch.ts +53 -9
- package/src/cli/doctor.ts +9 -2
- package/src/cli/ensure-desired-integrations.ts +10 -0
- package/src/cli/gui-pair-client.ts +1 -12
- package/src/cli/help.ts +4 -1
- package/src/cli/hub.ts +367 -0
- package/src/cli/index.ts +99 -31
- package/src/cli/launcher-context.ts +1 -1
- package/src/cli/models-runtime.ts +8 -3
- package/src/cli/observe.ts +13 -3
- package/src/cli/registry.ts +43 -3
- package/src/cli/status.ts +325 -5
- package/src/cli/version-skew.ts +4 -1
- package/src/cli.ts +2 -2
- package/src/client/catalog-compatibility.ts +192 -0
- package/src/client/connect.ts +31 -0
- package/src/client/hub-client.ts +52 -0
- package/src/client/hub-state.ts +214 -0
- package/src/clients/config-export/zcode.ts +24 -0
- package/src/codex/account-runtime-state.ts +6 -1
- package/src/codex/account-store.ts +72 -9
- package/src/codex/account-usability.ts +50 -13
- package/src/codex/auth-api.ts +156 -28
- package/src/codex/auth-context.ts +21 -0
- package/src/codex/catalog/effort.ts +67 -8
- package/src/codex/catalog/parsing.ts +23 -0
- package/src/codex/catalog/provider-fetch.ts +71 -2
- package/src/codex/catalog/sync.ts +99 -0
- package/src/codex/codex-write-lock.ts +11 -2
- package/src/codex/desired-state.ts +47 -1
- package/src/codex/inject-coordination.ts +10 -5
- package/src/codex/inject.ts +29 -12
- package/src/codex/loopback-target.ts +45 -0
- package/src/codex/quota-auto-refresh.ts +6 -1
- package/src/codex/quota.ts +54 -8
- package/src/codex/routing.ts +48 -1
- package/src/codex/runtime.ts +37 -3
- package/src/codex/sync.ts +29 -9
- package/src/codex/warmup.ts +21 -4
- package/src/combos/index.ts +2 -0
- package/src/combos/resolve.ts +52 -0
- package/src/config/pending-teardown.ts +1 -1
- package/src/config.ts +184 -12
- package/src/generated/compatibility-version.json +188 -116
- package/src/grok/status.ts +9 -1
- package/src/integrations/config-io.ts +54 -1
- package/src/lib/bun-runtime.ts +1 -1
- package/src/lib/errors.ts +8 -0
- package/src/lib/gui-pair-capability.ts +27 -0
- package/src/lib/local-destinations.ts +162 -0
- package/src/lib/package-tree-integrity.ts +1 -1
- package/src/lib/privacy.ts +25 -0
- package/src/lib/process-control.ts +130 -20
- package/src/lib/service-secrets.ts +28 -0
- package/src/lib/test-home-guard.ts +49 -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/providers/google-ai-studio-model-discovery.ts +74 -0
- package/src/providers/opencode-go-transport.ts +9 -1
- package/src/providers/opencode-zen-rate-limit.ts +75 -0
- package/src/providers/quota.ts +20 -1
- package/src/providers/registry.ts +35 -6
- package/src/remote/hub-state.ts +182 -0
- package/src/server/auth-cors.ts +11 -0
- package/src/server/chat-completions.ts +10 -7
- package/src/server/chat-native.ts +10 -1
- package/src/server/claude-messages.ts +12 -6
- package/src/server/hub-state.ts +98 -0
- package/src/server/images.ts +2 -2
- package/src/server/index.ts +149 -8
- package/src/server/management/api-access.ts +14 -3
- package/src/server/management/config-routes.ts +2 -2
- package/src/server/management/cursor-integration-routes.ts +13 -4
- package/src/server/management/logs-usage-routes.ts +4 -1
- package/src/server/management/model-rows.ts +16 -1
- package/src/server/management/oauth-account-routes.ts +6 -2
- package/src/server/management/provider-routes.ts +9 -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 +1 -1
- package/src/server/proxy-liveness.ts +7 -1
- package/src/server/request-decompress.ts +91 -3
- package/src/server/request-log-conversation.ts +41 -1
- package/src/server/request-log.ts +10 -0
- package/src/server/responses/codex-auth-error.ts +18 -1
- package/src/server/responses/codex-ws-exchange.ts +36 -4
- package/src/server/responses/codex-ws-wire.ts +76 -5
- package/src/server/responses/compact.ts +28 -11
- package/src/server/responses/context-overflow.ts +11 -0
- package/src/server/responses/core.ts +201 -48
- package/src/server/responses/policy-fallback.ts +13 -3
- package/src/server/search.ts +2 -2
- package/src/server/system-env-shell.ts +14 -2
- package/src/server/system-env.ts +106 -14
- package/src/service.ts +965 -68
- package/src/types/accounts.ts +18 -0
- package/src/types/config.ts +93 -4
- package/src/types/provider.ts +56 -0
- package/src/types.ts +4 -0
- package/src/update/badge.ts +3 -2
- package/src/update/index.ts +317 -64
- package/src/update/install-detection.d.mts +6 -0
- package/src/update/install-detection.mjs +73 -0
- package/src/update/job.ts +101 -49
- package/src/update/pnpm-global-install.d.mts +144 -0
- package/src/update/pnpm-global-install.mjs +591 -0
- package/src/update/pnpm-invocation.d.mts +43 -0
- package/src/update/pnpm-invocation.mjs +141 -0
- package/src/update/registry-integrity.d.mts +16 -0
- package/src/update/registry-integrity.mjs +37 -0
- package/src/update/transactional-install.d.mts +1 -1
- package/src/update/transactional-install.mjs +101 -7
- package/src/update/tray-update-plan.mjs +1 -1
- package/src/vision/plan.ts +13 -3
- package/src/vision/routed-describe.ts +51 -20
- package/src/web-search/ollama-executor.ts +127 -0
- package/src/web-search/passthrough-bridge.ts +761 -0
- package/gui/dist/assets/index-BtyONQrZ.js +0 -115
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The hub-state contract shared by `GET|HEAD /v1/hub-state` and every connected client.
|
|
3
|
+
*
|
|
4
|
+
* Why it exists (#4236): an agent working on a connected client machine read that machine's
|
|
5
|
+
* own `~/.opencodex/config.json` and `ocx status`, saw `xai ✗ not logged in`, no grok provider
|
|
6
|
+
* and five delegable models, and concluded the hub could not serve grok — while the hub had
|
|
7
|
+
* xAI logged in and was serving grok all along. A client's local credential store is empty BY
|
|
8
|
+
* DESIGN, so reporting it as the truth is not a cosmetic defect: it makes the client lie about
|
|
9
|
+
* the only machine that has the facts.
|
|
10
|
+
*
|
|
11
|
+
* What may cross this boundary is deliberately narrow. The client holds a per-client DATA key
|
|
12
|
+
* and no management credential, so this is a least-privilege data-plane read in the `/v1/catalog`
|
|
13
|
+
* (#809) tradition rather than a widened `/api/*` boundary. The payload is BOOLEANS and model
|
|
14
|
+
* ids: `hasCredential` is the same `!!p.apiKey` projection `GET /api/providers` already ships,
|
|
15
|
+
* and `loggedIn` is `oauthLoginSummary`'s boolean with the email and account id dropped
|
|
16
|
+
* entirely. No keys, no tokens, no quotas, no usage, no account identity — and nothing of that
|
|
17
|
+
* shape may be added later, because this surface is reachable with a data key.
|
|
18
|
+
*
|
|
19
|
+
* The delta over `/v1/catalog`, stated exactly, because "only two booleans" was wrong and a
|
|
20
|
+
* wrong boundary claim is worse than none. Provider names already leak through `/v1/catalog`
|
|
21
|
+
* slugs and `/v1/models` ids, but only for providers those routes list. What this route adds is:
|
|
22
|
+
* `hasCredential`, `loggedIn`, `authMode`, the featured roster — and the NAME and adapter of an
|
|
23
|
+
* ENABLED provider that the catalog omits for want of a usable credential. That last one is the
|
|
24
|
+
* point of the route (a client has to be able to say "the hub has xai configured and has no key
|
|
25
|
+
* for it" rather than "the hub cannot serve grok"), and it is the whole widening.
|
|
26
|
+
*
|
|
27
|
+
* A provider the operator marked `disabled` is NOT exported — see `src/server/hub-state.ts`.
|
|
28
|
+
* Naming it would tell a data-key holder about a provider no other data-plane route mentions,
|
|
29
|
+
* and a client has no use for it: it is not routable, so "absent" is the truthful report.
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
export const HUB_STATE_SCHEMA_VERSION = 1;
|
|
33
|
+
|
|
34
|
+
/** Hard caps, so the serialized body is bounded by construction rather than by hope. */
|
|
35
|
+
export const MAX_HUB_STATE_PROVIDERS = 200;
|
|
36
|
+
export const MAX_HUB_STATE_SUBAGENT_MODELS = 32;
|
|
37
|
+
export const MAX_HUB_STATE_OAUTH_PROVIDERS = 200;
|
|
38
|
+
export const MAX_HUB_STATE_STRING_CHARS = 200;
|
|
39
|
+
/** Response/transfer ceiling. The caps above keep a realistic body two orders below this. */
|
|
40
|
+
export const MAX_HUB_STATE_BYTES = 64 * 1024;
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Mirrors `OcxProviderConfig.authMode` (src/types/provider.ts); default `"key"`.
|
|
44
|
+
*
|
|
45
|
+
* It is shape, not secret: it says HOW a provider authenticates, which is what lets a client
|
|
46
|
+
* explain `hasCredential: false` on an `oauth` provider without claiming nothing is configured.
|
|
47
|
+
*/
|
|
48
|
+
export const HUB_STATE_AUTH_MODES = ["key", "forward", "oauth", "local"] as const;
|
|
49
|
+
export type HubStateAuthMode = (typeof HUB_STATE_AUTH_MODES)[number] | null;
|
|
50
|
+
|
|
51
|
+
export interface HubStateProvider {
|
|
52
|
+
name: string;
|
|
53
|
+
adapter: string;
|
|
54
|
+
authMode: HubStateAuthMode;
|
|
55
|
+
/** Presence only — the same `!!p.apiKey` projection `GET /api/providers` ships. */
|
|
56
|
+
hasCredential: boolean;
|
|
57
|
+
/**
|
|
58
|
+
* Always `false` from a hub of this version, which does not export a disabled provider at all.
|
|
59
|
+
* The field stays in the contract because an OLDER hub does send `true`, and a client reading
|
|
60
|
+
* one must still be able to label the row rather than present it as routable.
|
|
61
|
+
*/
|
|
62
|
+
disabled: boolean;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export interface HubStateOAuthEntry {
|
|
66
|
+
provider: string;
|
|
67
|
+
/** `oauthLoginSummary().loggedIn`. The email and account id are dropped, not masked. */
|
|
68
|
+
loggedIn: boolean;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export interface HubStateDTO {
|
|
72
|
+
schemaVersion: typeof HUB_STATE_SCHEMA_VERSION;
|
|
73
|
+
/** Always "hub": the route 404s on any other role, so a client can trust what it reads. */
|
|
74
|
+
runtimeRole: "hub";
|
|
75
|
+
hubVersion: string;
|
|
76
|
+
/** `hub.dataPublicOrigin` when the operator set one; null rather than a guess. */
|
|
77
|
+
origin: string | null;
|
|
78
|
+
providers: HubStateProvider[];
|
|
79
|
+
oauth: HubStateOAuthEntry[];
|
|
80
|
+
/** The hub's effective featured subagent roster — what a client should delegate to. */
|
|
81
|
+
subagentModels: string[];
|
|
82
|
+
/**
|
|
83
|
+
* At least one row was dropped to fit a cap above, so the arrays are a prefix rather than the
|
|
84
|
+
* whole truth. Silent truncation is what this flag exists to prevent: a client that lists 200
|
|
85
|
+
* of 240 providers and says nothing has told the reader the other 40 do not exist, which is
|
|
86
|
+
* the same class of confident-and-wrong report #4236 is about.
|
|
87
|
+
*/
|
|
88
|
+
truncated: boolean;
|
|
89
|
+
claudeCode: { enabled: boolean };
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function boundedString(value: unknown, max = MAX_HUB_STATE_STRING_CHARS): string | null {
|
|
93
|
+
if (typeof value !== "string") return null;
|
|
94
|
+
const trimmed = value.trim();
|
|
95
|
+
if (!trimmed || trimmed.length > max || /[\x00-\x1f\x7f]/.test(trimmed)) return null;
|
|
96
|
+
return trimmed;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Validate a hub-state body received over the wire.
|
|
101
|
+
*
|
|
102
|
+
* Returns null instead of throwing so both the live fetch and the on-disk cache can reject a
|
|
103
|
+
* malformed document the same way, and so a client NEVER degrades to its own local login state
|
|
104
|
+
* on a shape it does not recognize — degrading quietly is the defect being fixed.
|
|
105
|
+
*
|
|
106
|
+
* Unknown keys are dropped rather than refused: a newer hub must be readable by an older
|
|
107
|
+
* client, and the fields this projection reads are all required.
|
|
108
|
+
*/
|
|
109
|
+
export function parseHubStateBody(value: unknown): HubStateDTO | null {
|
|
110
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return null;
|
|
111
|
+
const raw = value as Record<string, unknown>;
|
|
112
|
+
if (raw.schemaVersion !== HUB_STATE_SCHEMA_VERSION) return null;
|
|
113
|
+
if (raw.runtimeRole !== "hub") return null;
|
|
114
|
+
const hubVersion = boundedString(raw.hubVersion, 64);
|
|
115
|
+
if (!hubVersion) return null;
|
|
116
|
+
const origin = raw.origin === null || raw.origin === undefined ? null : boundedString(raw.origin, 512);
|
|
117
|
+
if (raw.origin !== null && raw.origin !== undefined && origin === null) return null;
|
|
118
|
+
if (!Array.isArray(raw.providers) || raw.providers.length > MAX_HUB_STATE_PROVIDERS) return null;
|
|
119
|
+
if (!Array.isArray(raw.oauth) || raw.oauth.length > MAX_HUB_STATE_OAUTH_PROVIDERS) return null;
|
|
120
|
+
if (!Array.isArray(raw.subagentModels) || raw.subagentModels.length > MAX_HUB_STATE_SUBAGENT_MODELS) return null;
|
|
121
|
+
const claudeCode = raw.claudeCode;
|
|
122
|
+
if (!claudeCode || typeof claudeCode !== "object" || Array.isArray(claudeCode)) return null;
|
|
123
|
+
const enabled = (claudeCode as Record<string, unknown>).enabled;
|
|
124
|
+
if (typeof enabled !== "boolean") return null;
|
|
125
|
+
// Absent means false: a hub that predates the flag truncated nothing this client can detect,
|
|
126
|
+
// and refusing its document would turn a new honesty field into a compatibility break. A
|
|
127
|
+
// present non-boolean is still refused, like every other field here.
|
|
128
|
+
if (raw.truncated !== undefined && typeof raw.truncated !== "boolean") return null;
|
|
129
|
+
const truncated = raw.truncated === true;
|
|
130
|
+
|
|
131
|
+
const providers: HubStateProvider[] = [];
|
|
132
|
+
for (const row of raw.providers) {
|
|
133
|
+
if (!row || typeof row !== "object" || Array.isArray(row)) return null;
|
|
134
|
+
const entry = row as Record<string, unknown>;
|
|
135
|
+
const name = boundedString(entry.name);
|
|
136
|
+
const adapter = boundedString(entry.adapter);
|
|
137
|
+
if (!name || !adapter) return null;
|
|
138
|
+
if (typeof entry.hasCredential !== "boolean" || typeof entry.disabled !== "boolean") return null;
|
|
139
|
+
const authMode = entry.authMode;
|
|
140
|
+
const normalizedAuthMode = typeof authMode === "string" && (HUB_STATE_AUTH_MODES as readonly string[]).includes(authMode)
|
|
141
|
+
? authMode as NonNullable<HubStateAuthMode>
|
|
142
|
+
: null;
|
|
143
|
+
// An unrecognized authMode is refused rather than nulled: nulling it would let a newer hub's
|
|
144
|
+
// new mode read as "unset", which is a different claim about the provider.
|
|
145
|
+
if (authMode !== null && authMode !== undefined && normalizedAuthMode === null) return null;
|
|
146
|
+
providers.push({
|
|
147
|
+
name,
|
|
148
|
+
adapter,
|
|
149
|
+
authMode: normalizedAuthMode,
|
|
150
|
+
hasCredential: entry.hasCredential,
|
|
151
|
+
disabled: entry.disabled,
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const oauth: HubStateOAuthEntry[] = [];
|
|
156
|
+
for (const row of raw.oauth) {
|
|
157
|
+
if (!row || typeof row !== "object" || Array.isArray(row)) return null;
|
|
158
|
+
const entry = row as Record<string, unknown>;
|
|
159
|
+
const provider = boundedString(entry.provider);
|
|
160
|
+
if (!provider || typeof entry.loggedIn !== "boolean") return null;
|
|
161
|
+
oauth.push({ provider, loggedIn: entry.loggedIn });
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const subagentModels: string[] = [];
|
|
165
|
+
for (const row of raw.subagentModels) {
|
|
166
|
+
const model = boundedString(row);
|
|
167
|
+
if (!model) return null;
|
|
168
|
+
subagentModels.push(model);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
return {
|
|
172
|
+
schemaVersion: HUB_STATE_SCHEMA_VERSION,
|
|
173
|
+
runtimeRole: "hub",
|
|
174
|
+
hubVersion,
|
|
175
|
+
origin,
|
|
176
|
+
providers,
|
|
177
|
+
oauth,
|
|
178
|
+
subagentModels,
|
|
179
|
+
truncated,
|
|
180
|
+
claudeCode: { enabled },
|
|
181
|
+
};
|
|
182
|
+
}
|
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,
|
|
@@ -438,6 +439,11 @@ export const AUTH_MATRIX: readonly ApiAuthMatrixRow[] = [
|
|
|
438
439
|
// /v1/models and for the same reason — it forwards no caller credential upstream — so a
|
|
439
440
|
// remote client no longer needs an admin token just to read the model catalog.
|
|
440
441
|
{ endpoint: "/v1/catalog", bearer: "accepted", dedicated: "accepted", xApiKey: "accepted" },
|
|
442
|
+
// #4236: the hub-state read a connected client uses instead of reporting its own empty
|
|
443
|
+
// credential store. Same admission set and the same justification as the two rows above —
|
|
444
|
+
// it forwards no caller credential upstream and its body is booleans plus model ids — and it
|
|
445
|
+
// 404s on any host whose runtimeRole is not "hub", so no standalone install gains a surface.
|
|
446
|
+
{ endpoint: "/v1/hub-state", bearer: "accepted", dedicated: "accepted", xApiKey: "accepted" },
|
|
441
447
|
];
|
|
442
448
|
|
|
443
449
|
/** Whether `token` is the environment-provided management secret. */
|
|
@@ -650,6 +656,10 @@ export function providerManagementConfigError(name: unknown, provider: unknown):
|
|
|
650
656
|
if (requestPacingError) {
|
|
651
657
|
return `provider ${JSON.stringify(redactSecretString(name))} ${requestPacingError}`;
|
|
652
658
|
}
|
|
659
|
+
const webSearchBridgeError = providerWebSearchBridgeConfigError(raw.webSearchBridge);
|
|
660
|
+
if (webSearchBridgeError) {
|
|
661
|
+
return `provider ${JSON.stringify(redactSecretString(name))} ${webSearchBridgeError}`;
|
|
662
|
+
}
|
|
653
663
|
const upstreamHttpVersionError = upstreamHttpVersionConfigError(raw.upstreamHttpVersion);
|
|
654
664
|
if (upstreamHttpVersionError) {
|
|
655
665
|
return `provider ${JSON.stringify(redactSecretString(name))} ${upstreamHttpVersionError}`;
|
|
@@ -847,6 +857,7 @@ const PROVIDER_CONFIG_FIELD_POLICY = {
|
|
|
847
857
|
xaiResponsesDefaultVersion: "runtime",
|
|
848
858
|
supportsResponsesCustomTools: "editor",
|
|
849
859
|
responsesSnapshotRepair: "editor",
|
|
860
|
+
webSearchBridge: "editor",
|
|
850
861
|
reasoningEffortMap: "editor",
|
|
851
862
|
modelReasoningEffortMap: "editor",
|
|
852
863
|
reasoningWireFormat: "editor",
|
|
@@ -26,9 +26,12 @@ import { NoEligiblePolicyCandidateError, UnknownRoutingPolicyError, routeModel }
|
|
|
26
26
|
import { evidenceFromBody } from "../routing/request-evidence";
|
|
27
27
|
import { resolveWireProtocolOverride } from "./adapter-resolve";
|
|
28
28
|
import { resolveOpenCodeGoTransport } from "../providers/opencode-go-transport";
|
|
29
|
-
import {
|
|
29
|
+
import {
|
|
30
|
+
getOrAllocateRequestSessionLane,
|
|
31
|
+
linkRequestSessionLane,
|
|
32
|
+
} from "./request-log-conversation";
|
|
30
33
|
import type { OcxConfig } from "../types";
|
|
31
|
-
import { readJsonRequestBody } from "./request-decompress";
|
|
34
|
+
import { readJsonRequestBody, resolveInboundBodyLimitBytes } from "./request-decompress";
|
|
32
35
|
import {
|
|
33
36
|
addFinalRequestLog,
|
|
34
37
|
httpStatusForRequestLogTerminal,
|
|
@@ -62,9 +65,9 @@ type Rec = Record<string, unknown>;
|
|
|
62
65
|
function isRec(v: unknown): v is Rec {
|
|
63
66
|
return !!v && typeof v === "object" && !Array.isArray(v);
|
|
64
67
|
}
|
|
65
|
-
async function readChatBody(req: Request, budget: TranslatorBudget): Promise<unknown> {
|
|
68
|
+
async function readChatBody(req: Request, budget: TranslatorBudget, maxBytes: number): Promise<unknown> {
|
|
66
69
|
try {
|
|
67
|
-
return await readJsonRequestBody(req, budget);
|
|
70
|
+
return await readJsonRequestBody(req, budget, maxBytes);
|
|
68
71
|
} catch (err) {
|
|
69
72
|
if (isTranslatorBudgetExceededError(err)) throw err;
|
|
70
73
|
throw new ChatCompletionsRequestError(err instanceof Error && err.message ? err.message : "Invalid JSON body");
|
|
@@ -106,7 +109,7 @@ async function handleChatCompletionsWithBudget(
|
|
|
106
109
|
): Promise<Response> {
|
|
107
110
|
let chatBody: Rec;
|
|
108
111
|
try {
|
|
109
|
-
const rawBody = await readChatBody(req, translatorBudget);
|
|
112
|
+
const rawBody = await readChatBody(req, translatorBudget, resolveInboundBodyLimitBytes(config.maxInboundBodyBytes));
|
|
110
113
|
assertChatCompletionsRoutingBody(rawBody);
|
|
111
114
|
chatBody = rawBody;
|
|
112
115
|
} catch (err) {
|
|
@@ -142,8 +145,7 @@ async function handleChatCompletionsWithBudget(
|
|
|
142
145
|
let chatNativeRoute: ReturnType<typeof routeModel> | null = null;
|
|
143
146
|
try {
|
|
144
147
|
const route = routeModel(config, chatBody.model as string, evidenceFromBody(chatBody));
|
|
145
|
-
route.provider = resolveOpenCodeGoTransport(route.provider,
|
|
146
|
-
sessionLaneIdFromRequest(req.headers) ?? normalizeLogConversationId(req.headers.get("x-opencode-session")));
|
|
148
|
+
route.provider = resolveOpenCodeGoTransport(route.provider, getOrAllocateRequestSessionLane(req));
|
|
147
149
|
// Settle the wire once so every branch below reads the adapter this model will
|
|
148
150
|
// actually use, not the provider-wide default (#404).
|
|
149
151
|
route.provider = resolveWireProtocolOverride(route.providerName, route.modelId, route.provider, "chat");
|
|
@@ -305,6 +307,7 @@ async function handleChatCompletionsWithBudget(
|
|
|
305
307
|
headers,
|
|
306
308
|
body: internalBodyJson,
|
|
307
309
|
});
|
|
310
|
+
linkRequestSessionLane(req, internalReq);
|
|
308
311
|
|
|
309
312
|
let nativeLogged = false;
|
|
310
313
|
const finalizeNativeLog = (status: number, meta: { terminalStatus?: RequestLogEntry["terminalStatus"]; closeReason: "terminal" | "client_cancel" | "non_stream" }) => {
|
|
@@ -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,9 +34,14 @@ 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
|
-
import {
|
|
39
|
+
import {
|
|
40
|
+
conversationIdFromClaudeMetadata,
|
|
41
|
+
linkRequestSessionLane,
|
|
42
|
+
normalizeLogConversationId,
|
|
43
|
+
sessionLaneIdFromRequest,
|
|
44
|
+
} from "./request-log-conversation";
|
|
40
45
|
import { responseWithDeferredRequestLog } from "./relay";
|
|
41
46
|
import { handleResponses } from "./responses";
|
|
42
47
|
import {
|
|
@@ -129,9 +134,9 @@ function claudeInboundDisabled(config: OcxConfig): Response | null {
|
|
|
129
134
|
return null;
|
|
130
135
|
}
|
|
131
136
|
|
|
132
|
-
async function readAnthropicBody(req: Request, budget: TranslatorBudget): Promise<unknown> {
|
|
137
|
+
async function readAnthropicBody(req: Request, budget: TranslatorBudget, maxBytes: number): Promise<unknown> {
|
|
133
138
|
try {
|
|
134
|
-
return await readJsonRequestBody(req, budget);
|
|
139
|
+
return await readJsonRequestBody(req, budget, maxBytes);
|
|
135
140
|
} catch (err) {
|
|
136
141
|
if (isTranslatorBudgetExceededError(err)) throw err;
|
|
137
142
|
throw new AnthropicRequestError(err instanceof Error && err.message ? err.message : "Invalid JSON body");
|
|
@@ -655,7 +660,7 @@ async function handleClaudeMessagesWithBudget(
|
|
|
655
660
|
let fastRow: ParsedFastRowId | null = null;
|
|
656
661
|
let requestedModel = "";
|
|
657
662
|
try {
|
|
658
|
-
anthropicBody = await readAnthropicBody(req, translatorBudget);
|
|
663
|
+
anthropicBody = await readAnthropicBody(req, translatorBudget, resolveInboundBodyLimitBytes(config.maxInboundBodyBytes));
|
|
659
664
|
// Defensive [1m] strip (devlog 138): clients normally remove the context-variant
|
|
660
665
|
// marker themselves; the 1M signal we act on is the anthropic-beta header.
|
|
661
666
|
// Case-insensitive — the CLI matches /\[1m\]/i (audit 021 #7).
|
|
@@ -897,6 +902,7 @@ async function handleClaudeMessagesWithBudget(
|
|
|
897
902
|
headers,
|
|
898
903
|
body: JSON.stringify(internalBody),
|
|
899
904
|
});
|
|
905
|
+
linkRequestSessionLane(req, internalReq);
|
|
900
906
|
} finally {
|
|
901
907
|
reservation.release();
|
|
902
908
|
}
|
|
@@ -1154,7 +1160,7 @@ export async function handleClaudeCountTokens(
|
|
|
1154
1160
|
let body: unknown;
|
|
1155
1161
|
const translatorBudget = createTranslatorBudget();
|
|
1156
1162
|
try {
|
|
1157
|
-
body = await readAnthropicBody(req, translatorBudget);
|
|
1163
|
+
body = await readAnthropicBody(req, translatorBudget, resolveInboundBodyLimitBytes(config.maxInboundBodyBytes));
|
|
1158
1164
|
} catch (err) {
|
|
1159
1165
|
if (err instanceof DesktopModelMappingUnavailableError) return desktopMappingUnavailableResponse(err);
|
|
1160
1166
|
if (err instanceof AnthropicRequestError) return anthropicErrorResponse(400, err.message);
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The hub's own projection for `GET|HEAD /v1/hub-state` (#4236).
|
|
3
|
+
*
|
|
4
|
+
* Pure: it takes the config and an already-computed login summary and returns the DTO. The
|
|
5
|
+
* route owns admission, the role gate and the size ceiling; this module owns what a client is
|
|
6
|
+
* allowed to learn. Keeping the projection here — and building each row field by field rather
|
|
7
|
+
* than spreading a provider or a login summary — is what makes "no keys, no emails, no account
|
|
8
|
+
* ids" checkable by reading one function. A spread would silently start exporting whatever the
|
|
9
|
+
* next field added to those records happens to be.
|
|
10
|
+
*
|
|
11
|
+
* Contract and caps live in `src/remote/hub-state.ts` so the client validates the same shape.
|
|
12
|
+
*/
|
|
13
|
+
import {
|
|
14
|
+
HUB_STATE_SCHEMA_VERSION,
|
|
15
|
+
MAX_HUB_STATE_OAUTH_PROVIDERS,
|
|
16
|
+
MAX_HUB_STATE_PROVIDERS,
|
|
17
|
+
MAX_HUB_STATE_SUBAGENT_MODELS,
|
|
18
|
+
HUB_STATE_AUTH_MODES,
|
|
19
|
+
type HubStateDTO,
|
|
20
|
+
type HubStateOAuthEntry,
|
|
21
|
+
type HubStateProvider,
|
|
22
|
+
} from "../remote/hub-state";
|
|
23
|
+
import { DEFAULT_SUBAGENT_MODELS } from "../config/subagent-models";
|
|
24
|
+
import type { OcxConfig } from "../types";
|
|
25
|
+
|
|
26
|
+
export type HubStateConfigView = Pick<OcxConfig, "providers" | "subagentModels" | "claudeCode" | "hub">;
|
|
27
|
+
|
|
28
|
+
/** A login summary row as `oauthLoginSummary()` returns it; extra fields are never read. */
|
|
29
|
+
export interface HubStateLoginRow {
|
|
30
|
+
provider: string;
|
|
31
|
+
loggedIn: boolean;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* The hub's effective featured roster: the same "unset means the defaults, an explicit `[]`
|
|
36
|
+
* means none" rule `buildClaudeAgentDefs` applies, so a client that delegates from this list
|
|
37
|
+
* sees exactly what the hub itself would offer.
|
|
38
|
+
*/
|
|
39
|
+
export function hubSubagentRoster(config: Pick<OcxConfig, "subagentModels">): string[] {
|
|
40
|
+
return uncappedSubagentRoster(config).slice(0, MAX_HUB_STATE_SUBAGENT_MODELS);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** The same roster before the cap, so `truncated` can be computed instead of guessed. */
|
|
44
|
+
function uncappedSubagentRoster(config: Pick<OcxConfig, "subagentModels">): string[] {
|
|
45
|
+
const roster = config.subagentModels === undefined ? DEFAULT_SUBAGENT_MODELS : config.subagentModels;
|
|
46
|
+
return roster
|
|
47
|
+
.filter((entry): entry is string => typeof entry === "string" && entry.trim() !== "")
|
|
48
|
+
.map(entry => entry.trim());
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function buildHubState(
|
|
52
|
+
config: HubStateConfigView,
|
|
53
|
+
logins: readonly HubStateLoginRow[],
|
|
54
|
+
hubVersion: string,
|
|
55
|
+
): HubStateDTO {
|
|
56
|
+
// A disabled provider is dropped, not exported with a flag. `/v1/catalog` and `/v1/models`
|
|
57
|
+
// both filter it out (`src/router.ts`, `src/codex/catalog/*`), so exporting it here was the
|
|
58
|
+
// one thing this route told a data-key holder that no other data-plane route does — and a
|
|
59
|
+
// client has no use for it either: it cannot be routed to, so absence IS the truthful report,
|
|
60
|
+
// and `authMode` already explains a present-but-credential-less row without it.
|
|
61
|
+
const enabledProviders = Object.entries(config.providers ?? {}).filter(([, provider]) => provider.disabled !== true);
|
|
62
|
+
const providers: HubStateProvider[] = enabledProviders
|
|
63
|
+
.slice(0, MAX_HUB_STATE_PROVIDERS)
|
|
64
|
+
.map(([name, provider]) => ({
|
|
65
|
+
name,
|
|
66
|
+
adapter: provider.adapter,
|
|
67
|
+
authMode: provider.authMode !== undefined && HUB_STATE_AUTH_MODES.includes(provider.authMode)
|
|
68
|
+
? provider.authMode
|
|
69
|
+
: null,
|
|
70
|
+
// Presence only. Identical to the projection GET /api/providers already ships.
|
|
71
|
+
hasCredential: Boolean(provider.apiKey),
|
|
72
|
+
// Always false here. The field stays in the contract for an older hub's documents; see
|
|
73
|
+
// `HubStateProvider` in src/remote/hub-state.ts.
|
|
74
|
+
disabled: false,
|
|
75
|
+
}));
|
|
76
|
+
// Field-by-field, never a spread: oauthLoginSummary also carries the operator's email.
|
|
77
|
+
const oauth: HubStateOAuthEntry[] = logins
|
|
78
|
+
.slice(0, MAX_HUB_STATE_OAUTH_PROVIDERS)
|
|
79
|
+
.map(entry => ({ provider: entry.provider, loggedIn: entry.loggedIn === true }));
|
|
80
|
+
const rosterBeforeCap = uncappedSubagentRoster(config).length;
|
|
81
|
+
// Said out loud rather than silently: the caps are a prefix, and a client presenting a prefix
|
|
82
|
+
// as the whole list is the same confident-and-wrong report this route exists to prevent.
|
|
83
|
+
const truncated = enabledProviders.length > MAX_HUB_STATE_PROVIDERS
|
|
84
|
+
|| logins.length > MAX_HUB_STATE_OAUTH_PROVIDERS
|
|
85
|
+
|| rosterBeforeCap > MAX_HUB_STATE_SUBAGENT_MODELS;
|
|
86
|
+
return {
|
|
87
|
+
schemaVersion: HUB_STATE_SCHEMA_VERSION,
|
|
88
|
+
runtimeRole: "hub",
|
|
89
|
+
hubVersion,
|
|
90
|
+
origin: config.hub?.dataPublicOrigin ?? null,
|
|
91
|
+
providers,
|
|
92
|
+
oauth,
|
|
93
|
+
subagentModels: hubSubagentRoster(config),
|
|
94
|
+
truncated,
|
|
95
|
+
// Same predicate the launch path uses: absence means enabled.
|
|
96
|
+
claudeCode: { enabled: config.claudeCode?.enabled !== false },
|
|
97
|
+
};
|
|
98
|
+
}
|
package/src/server/images.ts
CHANGED
|
@@ -29,7 +29,7 @@ import { sidecarEnter } from "../lib/sidecar-tracker";
|
|
|
29
29
|
import type { OcxConfig } from "../types";
|
|
30
30
|
import { resolveFirstUsableOpenAiSidecar, selectImagesProvider } from "../providers/openai-sidecar";
|
|
31
31
|
import { getProviderRegistryEntry } from "../providers/registry";
|
|
32
|
-
import { readJsonRequestBody } from "./request-decompress";
|
|
32
|
+
import { readJsonRequestBody, resolveInboundBodyLimitBytes } from "./request-decompress";
|
|
33
33
|
import { ForwardAdmissionCredentialError, validateForwardAdmissionCredential } from "./auth-cors";
|
|
34
34
|
import type { RequestLogContext } from "./request-log";
|
|
35
35
|
import { codexLogAccountId, decodeRequestErrorResponse } from "./responses";
|
|
@@ -602,7 +602,7 @@ export async function handleImages(
|
|
|
602
602
|
): Promise<Response> {
|
|
603
603
|
let body: unknown;
|
|
604
604
|
try {
|
|
605
|
-
body = await readJsonRequestBody(req);
|
|
605
|
+
body = await readJsonRequestBody(req, undefined, resolveInboundBodyLimitBytes(config.maxInboundBodyBytes));
|
|
606
606
|
} catch (err) {
|
|
607
607
|
return decodeRequestErrorResponse(err, "images");
|
|
608
608
|
}
|