@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
|
@@ -102,7 +102,7 @@ import {
|
|
|
102
102
|
} from "../../lib/errors";
|
|
103
103
|
import { injectionDebugLog } from "../../lib/injection-debug-log";
|
|
104
104
|
import { resolveClientRetryAfter } from "../../lib/retry-after";
|
|
105
|
-
import {
|
|
105
|
+
import { enrichOpenCodeZenUpstreamMessage } from "../../providers/opencode-zen-rate-limit";
|
|
106
106
|
import { CODE_MODE_EXEC_TOOL_NAME, modelInList, namespacedToolName } from "../../types";
|
|
107
107
|
import type {
|
|
108
108
|
AdapterEvent,
|
|
@@ -150,6 +150,11 @@ import {
|
|
|
150
150
|
} from "../../oauth/generic-account-failover";
|
|
151
151
|
import { resolveCopilotApiBaseUrl } from "../../oauth/github-copilot";
|
|
152
152
|
import { buildWebSearchTool, planWebSearch, runWithWebSearch, shouldResolveOpenAiWebSearchSidecar } from "../../web-search";
|
|
153
|
+
import {
|
|
154
|
+
createOllamaBridgeExecutor,
|
|
155
|
+
createPassthroughWebSearchBridgeStream,
|
|
156
|
+
planPassthroughWebSearchBridge,
|
|
157
|
+
} from "../../web-search/passthrough-bridge";
|
|
153
158
|
import { buildImageTool, buildVideoTool, planImageBridge, planVideoBridge, runWithImageBridge, clampImageMaxRounds, IMAGE_GEN_TOOL_NAME, VIDEO_GEN_TOOL_NAME } from "../../images";
|
|
154
159
|
import { describeImagesInPlace, isModelTextOnly, planVisionSidecar, resolveOpenAiVisionModel, shouldResolveOpenAiVisionSidecar, stripImagesInPlace } from "../../vision";
|
|
155
160
|
import { createAdapterEventQueue, preflightAdapterEvents, type AdapterEventQueue } from "../../adapters/run-turn-queue";
|
|
@@ -243,7 +248,13 @@ import { hasPassiveAccountQuota, recordAnthropicAccountQuotaFromHeaders, recordP
|
|
|
243
248
|
import { captureConfigGeneration } from "../../lib/state-store-sweeper";
|
|
244
249
|
import { applyOpenAiVirtualModel, resolveOpenAiCompactModel } from "../../providers/openai-virtual-models";
|
|
245
250
|
import { isUsageDebugEnabled } from "../../usage/debug";
|
|
246
|
-
import {
|
|
251
|
+
import {
|
|
252
|
+
readJsonRequestBody,
|
|
253
|
+
describeInboundBodyRefusal,
|
|
254
|
+
resolveInboundBodyLimitBytes,
|
|
255
|
+
DecompressedBodyTooLargeError,
|
|
256
|
+
UnsupportedContentEncodingError,
|
|
257
|
+
} from "../request-decompress";
|
|
247
258
|
import { resolveAdapter, resolveWireProtocolOverride } from "../adapter-resolve";
|
|
248
259
|
import {
|
|
249
260
|
providerModelResponsesTerminalRepair,
|
|
@@ -309,6 +320,8 @@ import {
|
|
|
309
320
|
} from "../request-log";
|
|
310
321
|
import {
|
|
311
322
|
conversationIdFromResponsesRequest,
|
|
323
|
+
getOrAllocateRequestSessionLane,
|
|
324
|
+
linkRequestSessionLane,
|
|
312
325
|
normalizeLogConversationId,
|
|
313
326
|
reasoningReplayConversationIdFromResponsesRequest,
|
|
314
327
|
sessionLaneIdFromRequest,
|
|
@@ -424,7 +437,7 @@ import {
|
|
|
424
437
|
} from "../responses-undeclared-tool-guard";
|
|
425
438
|
import { createGithubCopilotResponsesBlockRewrite } from "../github-copilot-responses-repair";
|
|
426
439
|
import { responsesJsonToSseStream } from "../responses-json-events";
|
|
427
|
-
import { streamingContextOverflowResponse } from "./context-overflow";
|
|
440
|
+
import { jsonContextOverflowResponse, streamingContextOverflowResponse } from "./context-overflow";
|
|
428
441
|
import { guardTerminalEventStream } from "./terminal-guard";
|
|
429
442
|
import {
|
|
430
443
|
emptyCompletionRetryEnabled,
|
|
@@ -1012,14 +1025,14 @@ export function usesCodexForwardPoolAuth(
|
|
|
1012
1025
|
&& provider.authMode === "forward" && provider.adapter === "openai-responses";
|
|
1013
1026
|
}
|
|
1014
1027
|
|
|
1015
|
-
function codexWsQuotaObserver(authCtx: CodexAuthContext, provider: OcxProviderConfig): CodexWsQuotaObserver | undefined {
|
|
1028
|
+
function codexWsQuotaObserver(authCtx: CodexAuthContext, provider: OcxProviderConfig, modelId?: string): CodexWsQuotaObserver | undefined {
|
|
1016
1029
|
if (!isCanonicalOpenAiForwardProvider(provider) || !usesCodexForwardPoolAuth(authCtx, provider)) return undefined;
|
|
1017
1030
|
const { accountId, writerGeneration } = authCtx;
|
|
1018
1031
|
const credentialGeneration = authCtx.kind === "pool" ? authCtx.generation : undefined;
|
|
1019
1032
|
const mainWriter = authCtx.kind === "main-pool" ? authCtx.mainQuotaWriter : undefined;
|
|
1020
1033
|
return headers => {
|
|
1021
1034
|
if (credentialGeneration !== undefined && !isCodexAccountGenerationLive(accountId, credentialGeneration)) return;
|
|
1022
|
-
applyCapturedCodexQuota(accountId, headers, writerGeneration, mainWriter);
|
|
1035
|
+
applyCapturedCodexQuota(accountId, headers, writerGeneration, mainWriter, { modelId });
|
|
1023
1036
|
};
|
|
1024
1037
|
}
|
|
1025
1038
|
|
|
@@ -1382,6 +1395,7 @@ async function retryCodexPoolOnAlternateAccount(
|
|
|
1382
1395
|
firstResponse.headers,
|
|
1383
1396
|
firstAuthCtx.writerGeneration,
|
|
1384
1397
|
firstAuthCtx.kind === "main-pool" ? firstAuthCtx.mainQuotaWriter : undefined,
|
|
1398
|
+
{ modelId: route.modelId },
|
|
1385
1399
|
);
|
|
1386
1400
|
}
|
|
1387
1401
|
const deferFirstOutcome = shouldDeferCodexResetDerivedCooldown(
|
|
@@ -1473,7 +1487,7 @@ async function retryCodexPoolOnAlternateAccount(
|
|
|
1473
1487
|
providerFetch(route.provider, options.codexWsRuntimeIdentity, {
|
|
1474
1488
|
providerName: route.providerName,
|
|
1475
1489
|
modelId: route.modelId,
|
|
1476
|
-
onCodexWsQuota: codexWsQuotaObserver(retryAuthCtx, route.provider),
|
|
1490
|
+
onCodexWsQuota: codexWsQuotaObserver(retryAuthCtx, route.provider, route.modelId),
|
|
1477
1491
|
beforeDispatch: isCanonicalOpenAiForwardProvider(route.provider)
|
|
1478
1492
|
? createCodexReserveDispatchGuard(retryAuthCtx, options.codexAuthPolicy ?? config, route.modelId, options.admission, options.visionDescribeTerminal === true) : undefined,
|
|
1479
1493
|
}),
|
|
@@ -1607,7 +1621,7 @@ export function decodeRequestErrorResponse(err: unknown, label: string): Respons
|
|
|
1607
1621
|
return formatErrorResponse(415, "invalid_request_error", err.message);
|
|
1608
1622
|
}
|
|
1609
1623
|
if (err instanceof DecompressedBodyTooLargeError) {
|
|
1610
|
-
return formatErrorResponse(413, "
|
|
1624
|
+
return formatErrorResponse(413, "inbound_body_too_large", describeInboundBodyRefusal(err));
|
|
1611
1625
|
}
|
|
1612
1626
|
console.warn(`[${label}] request body decode/parse failed: ${err instanceof Error ? `${err.name}: ${err.message}` : String(err)}`);
|
|
1613
1627
|
return formatErrorResponse(400, "invalid_request_error", "Invalid JSON body");
|
|
@@ -2248,6 +2262,50 @@ function isTerminalPoolRefreshFailure(error: unknown): boolean {
|
|
|
2248
2262
|
return error instanceof TokenRefreshError && (error.reason === "revoked" || error.reason === "expired");
|
|
2249
2263
|
}
|
|
2250
2264
|
|
|
2265
|
+
/**
|
|
2266
|
+
* The refusal an operator meets when a stored pool credential's forced refresh does not complete.
|
|
2267
|
+
*
|
|
2268
|
+
* A bare "retry this request" reads as a transient fault in the proxy, which is how #4212's
|
|
2269
|
+
* reporter spent an afternoon concluding OpenCodex had broken while one of their own accounts was
|
|
2270
|
+
* the thing that needed them. It stays a retryable 503 and stays non-quarantining, because the
|
|
2271
|
+
* refresh genuinely may succeed and a token-endpoint 5xx must not retire a healthy account
|
|
2272
|
+
* (#2887). What it adds is the account and the exit: when retrying stops helping, that account
|
|
2273
|
+
* has to be signed in again.
|
|
2274
|
+
*
|
|
2275
|
+
* The label is a public account selector when the request carried one, otherwise the durable
|
|
2276
|
+
* `p`-prefixed log label — never the raw pool id and never the email. Those are the identifiers
|
|
2277
|
+
* `responses-compaction-routing.test.ts` and `codex-auth-context.test.ts` already assert must not
|
|
2278
|
+
* reach an operator-facing surface, and an error body travels further than a log line, not less.
|
|
2279
|
+
* When neither is resolvable the sentence degrades to "the selected Codex pool account" rather
|
|
2280
|
+
* than naming something opaque, because a wrong name is worse than no name.
|
|
2281
|
+
*
|
|
2282
|
+
* The wording says "sign in to that account again" and deliberately does NOT say
|
|
2283
|
+
* "reauthentication". `classifyError` runs `isAuthenticationMessage` before it reaches the
|
|
2284
|
+
* `status === 503` arm, and that check is status-blind on the bare substring "authentication",
|
|
2285
|
+
* which "reauthentication" contains. A body carrying that word is reclassified to
|
|
2286
|
+
* `authentication_error` / `invalid_api_key` even though the HTTP status stays 503 — and Codex
|
|
2287
|
+
* applies retry-after backoff only for `server_is_overloaded`, so the friendlier sentence would
|
|
2288
|
+
* have quietly disabled the retry this refusal exists to ask for. `options.code` cannot buy the
|
|
2289
|
+
* classification back; only the wording can.
|
|
2290
|
+
*/
|
|
2291
|
+
export function poolCredentialRefreshIncompleteResponse(args: {
|
|
2292
|
+
authCtx: CodexAuthContext;
|
|
2293
|
+
config: Pick<OcxConfig, "codexAccounts">;
|
|
2294
|
+
accountSelector?: string;
|
|
2295
|
+
}): Response {
|
|
2296
|
+
const label = args.accountSelector ?? codexAuthContextLogLabel(args.authCtx, args.config);
|
|
2297
|
+
const account = label ? `Codex pool account ${label}` : "the selected Codex pool account";
|
|
2298
|
+
const response = formatErrorResponse(
|
|
2299
|
+
503,
|
|
2300
|
+
"server_busy",
|
|
2301
|
+
`Codex credential refresh did not complete for ${account}; retry this request. `
|
|
2302
|
+
+ "If it keeps failing, sign in to that account again.",
|
|
2303
|
+
);
|
|
2304
|
+
const headers = new Headers(response.headers);
|
|
2305
|
+
headers.set("Retry-After", "1");
|
|
2306
|
+
return new Response(response.body, { status: response.status, headers });
|
|
2307
|
+
}
|
|
2308
|
+
|
|
2251
2309
|
/**
|
|
2252
2310
|
* One forced refresh and one same-account rebuild for a stored pool credential that
|
|
2253
2311
|
* upstream rejected with a pre-stream 401. `quarantine` distinguishes a dead grant,
|
|
@@ -2318,14 +2376,15 @@ async function refreshPoolForwardAuth(args: {
|
|
|
2318
2376
|
response: formatErrorResponse(401, "authentication_error", "Selected Codex account needs reauthentication"),
|
|
2319
2377
|
};
|
|
2320
2378
|
}
|
|
2321
|
-
|
|
2322
|
-
|
|
2323
|
-
|
|
2324
|
-
|
|
2325
|
-
|
|
2326
|
-
|
|
2327
|
-
|
|
2328
|
-
|
|
2379
|
+
return {
|
|
2380
|
+
ok: false,
|
|
2381
|
+
quarantine: false,
|
|
2382
|
+
response: poolCredentialRefreshIncompleteResponse({
|
|
2383
|
+
authCtx,
|
|
2384
|
+
config,
|
|
2385
|
+
accountSelector: route.codexAccountNamespace,
|
|
2386
|
+
}),
|
|
2387
|
+
};
|
|
2329
2388
|
}
|
|
2330
2389
|
}
|
|
2331
2390
|
|
|
@@ -2439,8 +2498,7 @@ async function applyFinalRouteRequestNormalization(args: {
|
|
|
2439
2498
|
|
|
2440
2499
|
// Settle the wire once so logging, fast-mode, auth, and sidecars read the adapter
|
|
2441
2500
|
// this request will actually use (#404).
|
|
2442
|
-
route.provider = resolveOpenCodeGoTransport(route.provider,
|
|
2443
|
-
sessionLaneIdFromRequest(req.headers) ?? normalizeLogConversationId(req.headers.get("x-opencode-session")));
|
|
2501
|
+
route.provider = resolveOpenCodeGoTransport(route.provider, getOrAllocateRequestSessionLane(req));
|
|
2444
2502
|
route.provider = resolveWireProtocolOverride(route.providerName, route.modelId, route.provider, inboundWire);
|
|
2445
2503
|
if (preserveAnthropicResponseModel) parsed._responseModelId = responseModelId;
|
|
2446
2504
|
logCtx.model = route.modelId;
|
|
@@ -2779,6 +2837,10 @@ export async function handleComboResponses(
|
|
|
2779
2837
|
logCtx.routeDecision = comboRouteDecisionTrace(config, comboId, pick, requestedModel);
|
|
2780
2838
|
|
|
2781
2839
|
let lastFailure: Response | null = null;
|
|
2840
|
+
// The exhausted-combo mapping below runs outside the loop, where `failure.upstreamCode`
|
|
2841
|
+
// is gone, so carry the loop's own classification decision instead of re-deriving a
|
|
2842
|
+
// weaker one from the status alone (#4149).
|
|
2843
|
+
let lastFailureClassifiesOverflow = false;
|
|
2782
2844
|
while (pick) {
|
|
2783
2845
|
if (options.abortSignal?.aborted) return clientCancelledResponse();
|
|
2784
2846
|
const childLog: RequestLogContext = {
|
|
@@ -2800,6 +2862,7 @@ export async function handleComboResponses(
|
|
|
2800
2862
|
headers: childHeaders,
|
|
2801
2863
|
body: JSON.stringify(childBody),
|
|
2802
2864
|
});
|
|
2865
|
+
linkRequestSessionLane(req, childRequest);
|
|
2803
2866
|
let resolvedAuth: CodexAuthContext | undefined;
|
|
2804
2867
|
let terminalRecorder: ((status: ResponsesTerminalStatus, httpStatusOverride?: number) => void) | undefined;
|
|
2805
2868
|
const started = Date.now();
|
|
@@ -2986,6 +3049,12 @@ export async function handleComboResponses(
|
|
|
2986
3049
|
const failureDecision = comboFailureDecision(failure.response.status, failure.classificationText, {
|
|
2987
3050
|
code: failure.upstreamCode,
|
|
2988
3051
|
});
|
|
3052
|
+
const wantsStream = (rawBody as { stream?: unknown } | null)?.stream === true;
|
|
3053
|
+
// Local byte admission has its own diagnostic; do not relabel it as an upstream refusal.
|
|
3054
|
+
const classifyOverflow = failure.response.status === 413
|
|
3055
|
+
&& (wantsStream || (failure.upstreamCode !== "outbound_body_too_large"
|
|
3056
|
+
&& failure.upstreamCode !== "translation_buffer_limit"));
|
|
3057
|
+
lastFailureClassifiesOverflow = classifyOverflow;
|
|
2989
3058
|
if (storedPool401ReplayDispatched) {
|
|
2990
3059
|
if (failureDecision === "hop" && unreadableEncryptedAgentTask && !comboPayloadReadable) {
|
|
2991
3060
|
const recoveredTarget = await pickWithWait({
|
|
@@ -3010,15 +3079,19 @@ export async function handleComboResponses(
|
|
|
3010
3079
|
// Keep the spent Pool budget sticky even after a recovered routed child:
|
|
3011
3080
|
// no later failure may reopen ordinary combo/native account hopping.
|
|
3012
3081
|
adoptFailedChildLog(childLog);
|
|
3082
|
+
if (classifyOverflow && failureDecision === "stop") {
|
|
3083
|
+
return wantsStream
|
|
3084
|
+
? streamingContextOverflowResponse(requestedModel, options.translatorBudget)
|
|
3085
|
+
: jsonContextOverflowResponse();
|
|
3086
|
+
}
|
|
3013
3087
|
return lastFailure;
|
|
3014
3088
|
}
|
|
3015
3089
|
if (failureDecision === "stop") {
|
|
3016
3090
|
adoptFailedChildLog(childLog);
|
|
3017
|
-
if (
|
|
3018
|
-
|
|
3019
|
-
|
|
3020
|
-
|
|
3021
|
-
return streamingContextOverflowResponse(requestedModel, options.translatorBudget);
|
|
3091
|
+
if (classifyOverflow) {
|
|
3092
|
+
return wantsStream
|
|
3093
|
+
? streamingContextOverflowResponse(requestedModel, options.translatorBudget)
|
|
3094
|
+
: jsonContextOverflowResponse();
|
|
3022
3095
|
}
|
|
3023
3096
|
return lastFailure;
|
|
3024
3097
|
}
|
|
@@ -3068,9 +3141,11 @@ export async function handleComboResponses(
|
|
|
3068
3141
|
}
|
|
3069
3142
|
if (
|
|
3070
3143
|
lastFailure?.status === 413
|
|
3071
|
-
&&
|
|
3144
|
+
&& lastFailureClassifiesOverflow
|
|
3072
3145
|
) {
|
|
3073
|
-
return
|
|
3146
|
+
return (rawBody as { stream?: unknown } | null)?.stream === true
|
|
3147
|
+
? streamingContextOverflowResponse(requestedModel, options.translatorBudget)
|
|
3148
|
+
: jsonContextOverflowResponse();
|
|
3074
3149
|
}
|
|
3075
3150
|
return lastFailure!;
|
|
3076
3151
|
}
|
|
@@ -3222,7 +3297,7 @@ async function handleResponsesInner(
|
|
|
3222
3297
|
const agentTaskRecovery = agentTaskRecoveryConfig(config);
|
|
3223
3298
|
let body: unknown;
|
|
3224
3299
|
try {
|
|
3225
|
-
body = await readJsonRequestBody(req, translatorBudget);
|
|
3300
|
+
body = await readJsonRequestBody(req, translatorBudget, resolveInboundBodyLimitBytes(config.maxInboundBodyBytes));
|
|
3226
3301
|
} catch (err) {
|
|
3227
3302
|
if (options.abortSignal?.aborted || req.signal.aborted) {
|
|
3228
3303
|
return clientCancelledResponse();
|
|
@@ -3275,6 +3350,30 @@ async function handleResponsesInner(
|
|
|
3275
3350
|
}
|
|
3276
3351
|
}
|
|
3277
3352
|
}
|
|
3353
|
+
// A shadow-call replacement that names a COMBO is routing policy, not the identity of any
|
|
3354
|
+
// one pick. The late intercept site below resolves it through routeModel/tryPickComboModel,
|
|
3355
|
+
// which collapses the table to a single target while still tagging `routeKind: "combo"`, so
|
|
3356
|
+
// the combo gate on the next line never fires, handleComboResponses never runs, and 429/5xx
|
|
3357
|
+
// hops — which only exist inside that loop — are unreachable (#4129). Rewrite the selector
|
|
3358
|
+
// here instead, before comboIdFromRawBody reads `model`, and identify the combo by CONFIG
|
|
3359
|
+
// LOOKUP so the check can never observe a one-candidate collapse.
|
|
3360
|
+
if (!options.comboAttempt && body && typeof body === "object" && !Array.isArray(body)) {
|
|
3361
|
+
const shadowIntercept = config.shadowCallIntercept;
|
|
3362
|
+
const rawShadowModel = (body as { model?: unknown }).model;
|
|
3363
|
+
if (shadowIntercept?.enabled && shadowIntercept.model && typeof rawShadowModel === "string"
|
|
3364
|
+
&& isShadowSourceModel(rawShadowModel, shadowIntercept.sourceModels)) {
|
|
3365
|
+
const shadowComboId = resolveComboId(config, shadowIntercept.model);
|
|
3366
|
+
if (shadowComboId && Object.hasOwn(config.combos ?? {}, shadowComboId)) {
|
|
3367
|
+
(body as Record<string, unknown>).model = shadowIntercept.model;
|
|
3368
|
+
// Same rule as the late intercept site: record the operator-configured prefix that
|
|
3369
|
+
// matched, never the caller's raw model string. Matching is by prefix, so the raw
|
|
3370
|
+
// value is caller-controlled and reaches usage.jsonl and /api/logs.
|
|
3371
|
+
logCtx.shadowCallRewrittenFrom = sanitizeLogMetadataString(
|
|
3372
|
+
shadowSourceModelPrefix(rawShadowModel, shadowIntercept.sourceModels),
|
|
3373
|
+
);
|
|
3374
|
+
}
|
|
3375
|
+
}
|
|
3376
|
+
}
|
|
3278
3377
|
const comboId = !options.comboAttempt ? comboIdFromRawBody(body, config) : null;
|
|
3279
3378
|
if (comboId && Object.hasOwn(config.combos ?? {}, comboId)) {
|
|
3280
3379
|
options.onRequestBodyRead?.();
|
|
@@ -3612,10 +3711,19 @@ async function handleResponsesInner(
|
|
|
3612
3711
|
let recoveryFailureReason: AgentTaskRecoveryFailureReason | undefined;
|
|
3613
3712
|
// Native fallback and explicitly trusted direct Responses routes can consume ciphertext,
|
|
3614
3713
|
// so recover only after final route selection.
|
|
3714
|
+
//
|
|
3715
|
+
// Deliberately NOT gated on `threadSpawn` (#4089). Switching a live thread from a native
|
|
3716
|
+
// ChatGPT model to a routed provider replays a backend-minted encrypted agent message on every
|
|
3717
|
+
// later turn, and a model switch is not a spawn, so the spawn requirement failed the thread
|
|
3718
|
+
// closed permanently without ever attempting recovery. The trust boundary is
|
|
3719
|
+
// `recoveryAdmission()` in ./agent-task-recovery -- Codex originator, live native ChatGPT
|
|
3720
|
+
// bearer, matching chatgpt-account-id, no inbound API key, no proxy-admission secret -- which
|
|
3721
|
+
// admits only the owner of the session that would be spent. `threadSpawn` narrowed which of
|
|
3722
|
+
// that owner's own requests could use their own session; it kept nobody else out. The combo
|
|
3723
|
+
// gate above keeps its spawn requirement: that path has its own native-target filtering and
|
|
3724
|
+
// per-attempt failover, and the reported defect is on this path.
|
|
3615
3725
|
if (
|
|
3616
3726
|
inboundWire === "responses"
|
|
3617
|
-
&&
|
|
3618
|
-
threadSpawn
|
|
3619
3727
|
&& agentTaskRecovery
|
|
3620
3728
|
&& !isCanonicalOpenAiForwardProvider(route.provider)
|
|
3621
3729
|
&& !options.comboAttempt
|
|
@@ -5050,7 +5158,7 @@ async function handleResponsesInner(
|
|
|
5050
5158
|
dispatchOverride: oauthDispatch(request),
|
|
5051
5159
|
providerName: route.providerName,
|
|
5052
5160
|
modelId: route.modelId,
|
|
5053
|
-
onCodexWsQuota: codexWsQuotaObserver(authCtx, route.provider),
|
|
5161
|
+
onCodexWsQuota: codexWsQuotaObserver(authCtx, route.provider, route.modelId),
|
|
5054
5162
|
beforeDispatch: isCanonicalOpenAiForwardProvider(route.provider)
|
|
5055
5163
|
? createCodexReserveDispatchGuard(authCtx, options.codexAuthPolicy ?? config, route.modelId, options.admission, options.visionDescribeTerminal === true) : undefined,
|
|
5056
5164
|
}),
|
|
@@ -5128,7 +5236,7 @@ async function handleResponsesInner(
|
|
|
5128
5236
|
dispatchOverride: oauthDispatch(request),
|
|
5129
5237
|
providerName: route.providerName,
|
|
5130
5238
|
modelId: route.modelId,
|
|
5131
|
-
onCodexWsQuota: codexWsQuotaObserver(authCtx, route.provider),
|
|
5239
|
+
onCodexWsQuota: codexWsQuotaObserver(authCtx, route.provider, route.modelId),
|
|
5132
5240
|
beforeDispatch: isCanonicalOpenAiForwardProvider(route.provider)
|
|
5133
5241
|
? createCodexReserveDispatchGuard(authCtx, options.codexAuthPolicy ?? config, route.modelId, options.admission, options.visionDescribeTerminal === true) : undefined,
|
|
5134
5242
|
}),
|
|
@@ -5234,7 +5342,7 @@ async function handleResponsesInner(
|
|
|
5234
5342
|
dispatchOverride: oauthDispatch(request),
|
|
5235
5343
|
providerName: route.providerName,
|
|
5236
5344
|
modelId: route.modelId,
|
|
5237
|
-
onCodexWsQuota: codexWsQuotaObserver(authCtx, route.provider),
|
|
5345
|
+
onCodexWsQuota: codexWsQuotaObserver(authCtx, route.provider, route.modelId),
|
|
5238
5346
|
beforeDispatch: isCanonicalOpenAiForwardProvider(route.provider)
|
|
5239
5347
|
? createCodexReserveDispatchGuard(authCtx, options.codexAuthPolicy ?? config, route.modelId, options.admission, options.visionDescribeTerminal === true) : undefined,
|
|
5240
5348
|
}),
|
|
@@ -5354,7 +5462,7 @@ async function handleResponsesInner(
|
|
|
5354
5462
|
dispatchOverride: oauthDispatch(request),
|
|
5355
5463
|
providerName: route.providerName,
|
|
5356
5464
|
modelId: route.modelId,
|
|
5357
|
-
onCodexWsQuota: codexWsQuotaObserver(authCtx, route.provider),
|
|
5465
|
+
onCodexWsQuota: codexWsQuotaObserver(authCtx, route.provider, route.modelId),
|
|
5358
5466
|
beforeDispatch: isCanonicalOpenAiForwardProvider(route.provider)
|
|
5359
5467
|
? createCodexReserveDispatchGuard(authCtx, options.codexAuthPolicy ?? config, route.modelId, options.admission, options.visionDescribeTerminal === true) : undefined,
|
|
5360
5468
|
}),
|
|
@@ -5454,7 +5562,7 @@ async function handleResponsesInner(
|
|
|
5454
5562
|
dispatchOverride: oauthDispatch(request),
|
|
5455
5563
|
providerName: route.providerName,
|
|
5456
5564
|
modelId: route.modelId,
|
|
5457
|
-
onCodexWsQuota: codexWsQuotaObserver(authCtx, route.provider),
|
|
5565
|
+
onCodexWsQuota: codexWsQuotaObserver(authCtx, route.provider, route.modelId),
|
|
5458
5566
|
beforeDispatch: isCanonicalOpenAiForwardProvider(route.provider)
|
|
5459
5567
|
? createCodexReserveDispatchGuard(authCtx, options.codexAuthPolicy ?? config, route.modelId, options.admission, options.visionDescribeTerminal === true) : undefined,
|
|
5460
5568
|
}),
|
|
@@ -5658,7 +5766,8 @@ async function handleResponsesInner(
|
|
|
5658
5766
|
const { applyAccountQuotaFromUpstreamHeaders } = await import("../../codex/auth-api");
|
|
5659
5767
|
if (!isCodexWsQuotaObservedResponse(upstreamResponse)) {
|
|
5660
5768
|
applyAccountQuotaFromUpstreamHeaders(authCtx.accountId, upstreamResponse.headers,
|
|
5661
|
-
authCtx.writerGeneration, authCtx.kind === "main-pool" ? authCtx.mainQuotaWriter : undefined
|
|
5769
|
+
authCtx.writerGeneration, authCtx.kind === "main-pool" ? authCtx.mainQuotaWriter : undefined,
|
|
5770
|
+
{ modelId: route.modelId });
|
|
5662
5771
|
}
|
|
5663
5772
|
if (terminalBodyWillRecord) {
|
|
5664
5773
|
options.setTerminalOutcomeRecorder?.((status, httpStatusOverride) => {
|
|
@@ -5699,7 +5808,8 @@ async function handleResponsesInner(
|
|
|
5699
5808
|
|
|
5700
5809
|
// Non-2xx passthrough failures must never reach Codex as an empty body —
|
|
5701
5810
|
// Codex renders that as the opaque "Unknown error" (#452). Combo attempts
|
|
5702
|
-
// keep their typed failure envelope.
|
|
5811
|
+
// keep their typed failure envelope. Except for the classified 413 below,
|
|
5812
|
+
// non-empty bodies are relayed verbatim
|
|
5703
5813
|
// (headers included) so pool-retry Activation B/D and client diagnostics stay intact.
|
|
5704
5814
|
// Manual-redirect policy (#914): a 3xx is relayed as-is (Location preserved
|
|
5705
5815
|
// through sanitizePassthroughHeaders) so a redirect to a dead host can never
|
|
@@ -5727,11 +5837,10 @@ async function handleResponsesInner(
|
|
|
5727
5837
|
// The bounded reader owns the original body, deadline, abort settlement, and lock.
|
|
5728
5838
|
// Unsafe partial data falls back to #452's non-empty status-only JSON.
|
|
5729
5839
|
const errorText = await readDisplaySafeErrorText(upstreamResponse, upstream.signal, "");
|
|
5730
|
-
if (upstreamResponse.status === 413
|
|
5731
|
-
return
|
|
5732
|
-
parsed._responseModelId ?? parsed.modelId,
|
|
5733
|
-
|
|
5734
|
-
);
|
|
5840
|
+
if (upstreamResponse.status === 413) {
|
|
5841
|
+
return clientRequestedStream
|
|
5842
|
+
? streamingContextOverflowResponse(parsed._responseModelId ?? parsed.modelId, translatorBudget)
|
|
5843
|
+
: jsonContextOverflowResponse();
|
|
5735
5844
|
}
|
|
5736
5845
|
return formatPassthroughUpstreamError(upstreamResponse.status, errorText, {
|
|
5737
5846
|
statusText: upstreamResponse.statusText,
|
|
@@ -5760,15 +5869,60 @@ async function handleResponsesInner(
|
|
|
5760
5869
|
route.provider,
|
|
5761
5870
|
route.modelId,
|
|
5762
5871
|
);
|
|
5872
|
+
// #3761: opt-in hosted-web-search bridge. Codex always declares the hosted web_search tool,
|
|
5873
|
+
// and this branch relays that declaration on the assumption the destination executes it.
|
|
5874
|
+
// A KEY-auth gateway that does not (Ollama Cloud GLM) answers with a function_call named
|
|
5875
|
+
// web_search that nothing runs, and the undeclared-tool guard below ends the turn. When the
|
|
5876
|
+
// provider opts in, the bridge intercepts that one call, runs the search, continues the
|
|
5877
|
+
// conversation upstream, and hands back ordinary Responses SSE — so every rewrite below,
|
|
5878
|
+
// including the guard itself, still inspects the client-facing stream. Default OFF: without
|
|
5879
|
+
// the opt-in this is one planner call and the relay is byte-identical to before.
|
|
5880
|
+
const webSearchBridgePlan = planPassthroughWebSearchBridge(parsed, route.provider, {
|
|
5881
|
+
isPassthrough: true,
|
|
5882
|
+
stream: parsed.stream === true,
|
|
5883
|
+
});
|
|
5884
|
+
// The bridge wraps the RAW upstream body, so terminal repair below still owns the single
|
|
5885
|
+
// client-facing terminal — the bridge drops the terminal of every intercepted leg.
|
|
5886
|
+
const upstreamSseBody = webSearchBridgePlan
|
|
5887
|
+
? createPassthroughWebSearchBridgeStream({
|
|
5888
|
+
plan: webSearchBridgePlan,
|
|
5889
|
+
firstLeg: upstreamResponse.body,
|
|
5890
|
+
requestBody: request.body,
|
|
5891
|
+
// Continuation legs replay the same built request with the executed search appended.
|
|
5892
|
+
// The first leg already passed the recovery ladder, the outbound size ceiling, and the
|
|
5893
|
+
// host circuit; a KEY-auth destination has no OAuth refresh to replay on a later leg.
|
|
5894
|
+
send: (continuationBody: string) => fetchWithHeaderTimeout(
|
|
5895
|
+
request.url,
|
|
5896
|
+
{ method: request.method, headers: request.headers, body: continuationBody },
|
|
5897
|
+
upstream.signal,
|
|
5898
|
+
connectMs,
|
|
5899
|
+
true,
|
|
5900
|
+
providerFetch(route.provider, options.codexWsRuntimeIdentity, {
|
|
5901
|
+
dispatchOverride: oauthDispatch(request),
|
|
5902
|
+
providerName: route.providerName,
|
|
5903
|
+
modelId: route.modelId,
|
|
5904
|
+
}),
|
|
5905
|
+
false,
|
|
5906
|
+
),
|
|
5907
|
+
execute: createOllamaBridgeExecutor(webSearchBridgePlan, route.provider.apiKey ?? ""),
|
|
5908
|
+
// Appending a search result can push the continuation past the ceiling the first leg
|
|
5909
|
+
// was admitted under, so the same limit is re-applied before every later send.
|
|
5910
|
+
checkOutboundBody: (continuationBody: string) => {
|
|
5911
|
+
const result = checkOutboundBodySize(continuationBody, config.maxUpstreamBodyBytes);
|
|
5912
|
+
return result.admitted ? undefined : describeOutboundBodyRefusal(result);
|
|
5913
|
+
},
|
|
5914
|
+
signal: upstream.signal,
|
|
5915
|
+
})
|
|
5916
|
+
: upstreamResponse.body;
|
|
5763
5917
|
const passthroughSseBody = terminalRepairPolicy
|
|
5764
5918
|
? relayResponsesSseWithTerminalRepair(
|
|
5765
|
-
|
|
5919
|
+
upstreamSseBody,
|
|
5766
5920
|
upstream,
|
|
5767
5921
|
terminalRepairPolicy,
|
|
5768
5922
|
translatorBudget,
|
|
5769
5923
|
options.responsesTerminalRepairScheduler,
|
|
5770
5924
|
)
|
|
5771
|
-
:
|
|
5925
|
+
: upstreamSseBody;
|
|
5772
5926
|
const repairConfig = route.provider.responsesItemIdRepair;
|
|
5773
5927
|
// Grok Build renders deltas live but reconstructs its durable assistant
|
|
5774
5928
|
// turn from the completed response snapshot. Native Responses streams
|
|
@@ -7463,11 +7617,10 @@ async function handleResponsesInner(
|
|
|
7463
7617
|
} finally {
|
|
7464
7618
|
cleanupUpstreamAbort();
|
|
7465
7619
|
}
|
|
7466
|
-
if (upstreamResponse.status === 413
|
|
7467
|
-
return
|
|
7468
|
-
parsed._responseModelId ?? parsed.modelId,
|
|
7469
|
-
|
|
7470
|
-
);
|
|
7620
|
+
if (upstreamResponse.status === 413) {
|
|
7621
|
+
return clientRequestedStream
|
|
7622
|
+
? streamingContextOverflowResponse(parsed._responseModelId ?? parsed.modelId, translatorBudget)
|
|
7623
|
+
: jsonContextOverflowResponse();
|
|
7471
7624
|
}
|
|
7472
7625
|
if (!isFixedCodexAccount(authCtx)) {
|
|
7473
7626
|
recordSubagentQuotaFailureForThreadSpawn(
|
|
@@ -7487,7 +7640,7 @@ async function handleResponsesInner(
|
|
|
7487
7640
|
const message = normalized.cyberPolicy
|
|
7488
7641
|
? normalized.message
|
|
7489
7642
|
?? (isCyberPolicyCode(normalized.code) ? CYBER_POLICY_FALLBACK_MESSAGE : normalized.safeText)
|
|
7490
|
-
:
|
|
7643
|
+
: enrichOpenCodeZenUpstreamMessage(
|
|
7491
7644
|
`Provider error ${upstreamResponse.status}: ${normalized.safeText}`,
|
|
7492
7645
|
{
|
|
7493
7646
|
status: upstreamResponse.status,
|
|
@@ -7498,7 +7651,7 @@ async function handleResponsesInner(
|
|
|
7498
7651
|
hasApiKey: Boolean(route.provider.apiKey?.trim()),
|
|
7499
7652
|
upstreamRetryAfter,
|
|
7500
7653
|
// This recovery path is the HTTP Responses wire; custom runTurn transports
|
|
7501
|
-
// never reach
|
|
7654
|
+
// never reach enrichOpenCodeZenUpstreamMessage here.
|
|
7502
7655
|
supportsHttpSameKeyRetry: true,
|
|
7503
7656
|
},
|
|
7504
7657
|
);
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { comboFailureDecision } from "../../combos/failover";
|
|
2
2
|
import { readBoundedResponseBody } from "../../lib/bounded-body";
|
|
3
|
-
import { readJsonRequestBody } from "../request-decompress";
|
|
3
|
+
import { readJsonRequestBody, resolveInboundBodyLimitBytes } from "../request-decompress";
|
|
4
4
|
import { finishRequestAttempt, type RequestLogContext } from "../request-log";
|
|
5
|
+
import { linkRequestSessionLane } from "../request-log-conversation";
|
|
5
6
|
import type { OcxConfig } from "../../types";
|
|
6
7
|
import type { RouteCandidateTrace, RouteDecisionTraceV1 } from "../../routing/trace";
|
|
7
8
|
import { handleResponses as handleResponsesCore } from "./core";
|
|
@@ -56,12 +57,17 @@ function requestWithCandidate(
|
|
|
56
57
|
headers.delete("content-encoding");
|
|
57
58
|
headers.delete("content-length");
|
|
58
59
|
headers.set("content-type", "application/json");
|
|
59
|
-
|
|
60
|
+
const retryRequest = new Request(req.url, {
|
|
60
61
|
method: req.method,
|
|
61
62
|
headers,
|
|
62
63
|
body: JSON.stringify({ ...rawBody, model: `${candidate.provider}/${candidate.model}` }),
|
|
63
64
|
signal: req.signal,
|
|
64
65
|
});
|
|
66
|
+
// A sessionless request keeps the lane it was already allocated. Without this the second
|
|
67
|
+
// candidate reaches OpenCode Go under a different x-opencode-session than the first attempt,
|
|
68
|
+
// which is the same conversation split the header exists to prevent.
|
|
69
|
+
linkRequestSessionLane(req, retryRequest);
|
|
70
|
+
return retryRequest;
|
|
65
71
|
}
|
|
66
72
|
|
|
67
73
|
function errorCodeFromText(text: string): string | undefined {
|
|
@@ -145,7 +151,11 @@ export async function handleResponsesWithPolicyFallback(
|
|
|
145
151
|
};
|
|
146
152
|
let rawBody: Record<string, unknown> | null = null;
|
|
147
153
|
try {
|
|
148
|
-
const parsed = await readJsonRequestBody(
|
|
154
|
+
const parsed = await readJsonRequestBody(
|
|
155
|
+
req.clone(),
|
|
156
|
+
undefined,
|
|
157
|
+
resolveInboundBodyLimitBytes(config.maxInboundBodyBytes),
|
|
158
|
+
);
|
|
149
159
|
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) rawBody = parsed as Record<string, unknown>;
|
|
150
160
|
} catch {
|
|
151
161
|
// Core owns the client-facing parse/decompression error.
|
package/src/server/search.ts
CHANGED
|
@@ -33,7 +33,7 @@ import {
|
|
|
33
33
|
type ExactOpenAiSidecarAccount,
|
|
34
34
|
} from "../providers/openai-sidecar";
|
|
35
35
|
import { routeModel } from "../router";
|
|
36
|
-
import { readJsonRequestBody } from "./request-decompress";
|
|
36
|
+
import { readJsonRequestBody, resolveInboundBodyLimitBytes } from "./request-decompress";
|
|
37
37
|
import { ForwardAdmissionCredentialError, validateForwardAdmissionCredential } from "./auth-cors";
|
|
38
38
|
import type { RequestLogContext } from "./request-log";
|
|
39
39
|
import { codexLogAccountId, decodeRequestErrorResponse } from "./responses";
|
|
@@ -64,7 +64,7 @@ export async function handleSearch(
|
|
|
64
64
|
}
|
|
65
65
|
let body: unknown;
|
|
66
66
|
try {
|
|
67
|
-
body = await readJsonRequestBody(req);
|
|
67
|
+
body = await readJsonRequestBody(req, undefined, resolveInboundBodyLimitBytes(config.maxInboundBodyBytes));
|
|
68
68
|
} catch (err) {
|
|
69
69
|
return decodeRequestErrorResponse(err, "search");
|
|
70
70
|
}
|
|
@@ -7,6 +7,7 @@ import { resolveClaudeAuthMode } from "../claude/auth-mode";
|
|
|
7
7
|
import { ANTHROPIC_PARENT_ENV_SLOTS, trustedNodeLauncherContext, type AnthropicParentEnvSlot } from "../cli/launcher-context";
|
|
8
8
|
import type { OcxConfig } from "../types";
|
|
9
9
|
import { recordOwnedConfigPath } from "../lib/config-ownership";
|
|
10
|
+
import { localAdmissionToken, localInferenceDestination } from "../lib/local-destinations";
|
|
10
11
|
|
|
11
12
|
/**
|
|
12
13
|
* Does the opencodex dummy marker belong in the system environment?
|
|
@@ -79,9 +80,14 @@ export function writeShellEnvFile(
|
|
|
79
80
|
auto?: AutoContextMode,
|
|
80
81
|
deps: SystemEnvDeps = {},
|
|
81
82
|
): void {
|
|
83
|
+
// Same local destination the launchd domain gets, resolved through the same resolver rather
|
|
84
|
+
// than re-derived: the unauthenticated loopback listener when one is enabled, otherwise the
|
|
85
|
+
// bind address (#4236). The two files must not disagree, or a new shell and a launchd-started
|
|
86
|
+
// `claude` would dial different sockets.
|
|
87
|
+
const destination = localInferenceDestination(config, port);
|
|
82
88
|
const lines = [
|
|
83
89
|
`# Generated by opencodex — do not edit manually`,
|
|
84
|
-
`export ANTHROPIC_BASE_URL=${shellValue(
|
|
90
|
+
`export ANTHROPIC_BASE_URL=${shellValue(destination.origin)}`,
|
|
85
91
|
`export CLAUDE_CODE_ENABLE_GATEWAY_MODEL_DISCOVERY=${shellValue("1")}`,
|
|
86
92
|
];
|
|
87
93
|
// New lever keys are CONDITIONAL exports (audit 139 R2#1): a value the user already
|
|
@@ -89,7 +95,13 @@ export function writeShellEnvFile(
|
|
|
89
95
|
const conditional = (name: string, value: string) =>
|
|
90
96
|
`[ -z "\${${name}+x}" ] && export ${name}=${shellValue(value)}`;
|
|
91
97
|
if (systemEnvMarkerMode(config, deps) === "proxy") {
|
|
92
|
-
|
|
98
|
+
// On a bind that demands data-plane admission the credential may live only in
|
|
99
|
+
// `OPENCODEX_API_AUTH_TOKEN` or the hardened service token file, so the same ladder the
|
|
100
|
+
// launchd injection uses applies here. Never the admin token (reviewer constraint on #4236).
|
|
101
|
+
const hostAdmissionToken = destination.requiresAdmissionToken ? localAdmissionToken(config) : undefined;
|
|
102
|
+
if (hostAdmissionToken) {
|
|
103
|
+
lines.push(`export ANTHROPIC_AUTH_TOKEN=${shellValue(hostAdmissionToken)}`);
|
|
104
|
+
} else if (config.apiKeys?.length) {
|
|
93
105
|
lines.push(`export ANTHROPIC_AUTH_TOKEN=${shellValue(config.apiKeys[0].key)}`);
|
|
94
106
|
} else {
|
|
95
107
|
lines.push(conditional("ANTHROPIC_AUTH_TOKEN", PROXY_MARKER));
|