@bitkyc08/opencodex 2.39.0 → 2.40.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/README.md +49 -21
- package/gui/dist/assets/index-BHe2rl_C.js +112 -0
- package/gui/dist/assets/index-CJSb3HPe.css +1 -0
- package/gui/dist/index.html +2 -2
- package/package.json +1 -1
- package/src/adapters/cursor/catalog.ts +140 -7
- package/src/adapters/cursor/discovery.ts +79 -87
- package/src/adapters/cursor/effort-map.ts +11 -0
- package/src/adapters/cursor/request-builder.ts +31 -4
- package/src/adapters/cursor.ts +17 -0
- package/src/adapters/openai-chat.ts +95 -9
- package/src/adapters/openai-responses.ts +73 -9
- package/src/bridge.ts +58 -13
- package/src/claude/auth-mode.ts +6 -9
- package/src/claude/gateway-cache.ts +23 -3
- package/src/claude/model-info.ts +13 -2
- package/src/cli/access.ts +30 -0
- package/src/cli/account-extended.ts +19 -10
- package/src/cli/capabilities.ts +40 -1
- package/src/cli/claude-agent-startup-sync.ts +3 -0
- package/src/cli/claude.ts +134 -36
- package/src/cli/connect.ts +226 -0
- package/src/cli/dispatch.ts +126 -21
- package/src/cli/gui-pair-client.ts +170 -0
- package/src/cli/gui.ts +87 -0
- package/src/cli/help.ts +4 -1
- package/src/cli/index.ts +76 -22
- package/src/cli/models-runtime-subcommands.ts +34 -0
- package/src/cli/models-runtime.ts +4 -0
- package/src/cli/models.ts +2 -1
- package/src/cli/opencode.ts +2 -1
- package/src/cli/provider-runtime.ts +31 -1
- package/src/cli/registry.ts +32 -2
- package/src/cli/runtime-api.ts +10 -1
- package/src/cli/status.ts +26 -0
- package/src/cli/system-command.ts +9 -3
- package/src/client/connect.ts +650 -0
- package/src/client/hub-client.ts +457 -0
- package/src/client/hub-relay.ts +288 -0
- package/src/client/machine-api.ts +139 -0
- package/src/client/machine-auth.ts +54 -0
- package/src/client/machine-listener.ts +143 -0
- package/src/client/runtime.ts +93 -0
- package/src/client/state.ts +175 -0
- package/src/clients/config-export.ts +16 -5
- package/src/codex/auth-api.ts +169 -15
- package/src/codex/auth-context.ts +61 -6
- package/src/codex/catalog/aggregation.ts +17 -13
- package/src/codex/catalog/metadata.ts +28 -0
- package/src/codex/catalog/provider-fetch.ts +38 -5
- package/src/codex/catalog.ts +1 -1
- package/src/codex/desired-state.ts +13 -3
- package/src/codex/inject.ts +212 -31
- package/src/codex/journal.ts +54 -6
- package/src/codex/main-account.ts +105 -33
- package/src/codex/native-main-claim.ts +23 -1
- package/src/codex/native-residue.ts +7 -0
- package/src/codex/reset-credit-auto-redeem.ts +237 -0
- package/src/combos/request.ts +15 -3
- package/src/combos/types.ts +12 -0
- package/src/config/paths.ts +15 -1
- package/src/config/provider-validation.ts +40 -0
- package/src/config.ts +447 -3
- package/src/generated/compatibility-version.json +224 -108
- package/src/images/artifacts.ts +73 -37
- package/src/images/fulfill.ts +6 -1
- package/src/images/index.ts +1 -1
- package/src/images/plan.ts +16 -2
- package/src/images/synthetic-tool.ts +5 -0
- package/src/images/xai-client.ts +34 -1
- package/src/integrations/cursor-detect.ts +133 -0
- package/src/integrations/cursor-seen.ts +31 -0
- package/src/lib/errors.ts +18 -0
- package/src/lib/gui-pair-capability.ts +104 -0
- package/src/lib/lab-live-route-production.ts +2 -1
- package/src/lib/process-control.ts +4 -3
- package/src/lib/service-secrets.ts +161 -1
- package/src/lib/windows-system-proxy.ts +115 -0
- package/src/oauth/index.ts +3 -2
- package/src/oauth/pool-settings-capability.ts +55 -0
- package/src/providers/api-keys.ts +3 -1
- package/src/providers/codex-capacity.ts +34 -4
- package/src/providers/derive.ts +10 -0
- package/src/providers/fastwire.ts +10 -2
- package/src/providers/key-store.ts +197 -0
- package/src/providers/model-rename-migration.ts +3 -0
- package/src/providers/openai-sidecar.ts +3 -3
- package/src/providers/quota.ts +91 -42
- package/src/providers/registry.ts +33 -3
- package/src/reasoning-effort.ts +32 -0
- package/src/remote/protocol.ts +109 -0
- package/src/responses/citation-markers.ts +101 -0
- package/src/responses/parser.ts +43 -3
- package/src/responses/spill-store.ts +9 -4
- package/src/router.ts +72 -8
- package/src/server/auth-cors.ts +37 -1
- package/src/server/gui-session.ts +449 -0
- package/src/server/gui-static.ts +24 -7
- package/src/server/images.ts +237 -10
- package/src/server/index.ts +356 -30
- package/src/server/management/agent-settings-routes.ts +7 -1
- package/src/server/management/api-key-rotation.ts +74 -0
- package/src/server/management/combo-routes.ts +18 -6
- package/src/server/management/config-routes.ts +22 -3
- package/src/server/management/context.ts +5 -1
- package/src/server/management/cursor-integration-routes.ts +98 -0
- package/src/server/management/logs-usage-routes.ts +2 -1
- package/src/server/management/model-routes.ts +79 -1
- package/src/server/management/model-rows.ts +23 -1
- package/src/server/management/oauth-account-routes.ts +135 -2
- package/src/server/management/provider-routes.ts +143 -9
- package/src/server/management/route-registry.ts +10 -0
- package/src/server/management/session-routes.ts +13 -0
- package/src/server/management/system-routes.ts +13 -1
- package/src/server/management-api.ts +8 -3
- package/src/server/management-auth.ts +124 -82
- package/src/server/models-capabilities.ts +124 -0
- package/src/server/ports.ts +7 -0
- package/src/server/proxy-liveness.ts +7 -0
- package/src/server/request-log.ts +11 -5
- package/src/server/responses/agent-task-recovery.ts +1 -0
- package/src/server/responses/codex-auth-error.ts +3 -1
- package/src/server/responses/collaboration.ts +26 -1
- package/src/server/responses/combo-stream-preflight.ts +25 -6
- package/src/server/responses/compact.ts +32 -12
- package/src/server/responses/context-overflow.ts +49 -0
- package/src/server/responses/core.ts +196 -12
- package/src/server/responses/fetch-helpers.ts +2 -1
- package/src/server/responses/outbound-body-guard.ts +110 -0
- package/src/server/responses/ws-upstream.ts +87 -6
- package/src/server/responses-self-named-namespace-scrub.ts +181 -0
- package/src/server/system-env.ts +78 -25
- package/src/service.ts +218 -42
- package/src/types/config.ts +134 -0
- package/src/types/provider.ts +54 -1
- package/src/types/request.ts +2 -0
- package/src/types.ts +5 -0
- package/src/update/job.ts +33 -5
- package/src/usage/cost.ts +32 -1
- package/src/usage/expected-prices.ts +16 -0
- package/src/usage/log.ts +6 -2
- package/src/usage/summary.ts +48 -6
- package/gui/dist/assets/index-D-lchsPw.js +0 -112
- package/gui/dist/assets/index-uvENYLin.css +0 -1
|
@@ -3,13 +3,13 @@ import { bridgeToResponsesSSE, buildResponseJSON, formatErrorResponse, type Resp
|
|
|
3
3
|
import {
|
|
4
4
|
getConfigPath,
|
|
5
5
|
multiAgentGuidanceEnabled,
|
|
6
|
-
resolveEnvValue,
|
|
7
6
|
} from "../../config";
|
|
7
|
+
import { resolveProviderApiKey } from "../../providers/key-store";
|
|
8
8
|
import { parseRequest } from "../../responses/parser";
|
|
9
9
|
import { buildCompactV1Output, COMPACT_PROMPT, decodeCompactionSummary, extractCompactUserMessages } from "../../responses/compaction";
|
|
10
10
|
import { FORWARD_HEADERS, sanitizeReasoningInputContent } from "../../adapters/openai-responses";
|
|
11
11
|
import { expandPreviousResponseInput, previousResponseProviderState, rememberResponseState } from "../../responses/state";
|
|
12
|
-
import { NoEligiblePolicyCandidateError,
|
|
12
|
+
import { NoEligiblePolicyCandidateError, routeCompactionModel } from "../../router";
|
|
13
13
|
import { evidenceFromBody } from "../../routing/request-evidence";
|
|
14
14
|
import {
|
|
15
15
|
advanceComboAfterFailure,
|
|
@@ -267,6 +267,9 @@ async function refreshNativeMainCompactContext(args: {
|
|
|
267
267
|
}
|
|
268
268
|
return { ok: true, authCtx: refreshedAuthCtx, provider: refreshedProvider, headers };
|
|
269
269
|
} catch (error) {
|
|
270
|
+
if (req.signal.aborted) {
|
|
271
|
+
return { ok: false, response: formatErrorResponse(499, "client_cancelled", "Client cancelled compact request") };
|
|
272
|
+
}
|
|
270
273
|
return { ok: false, response: nativeMainRefreshFailureResponse(error) };
|
|
271
274
|
}
|
|
272
275
|
}
|
|
@@ -382,7 +385,10 @@ async function resolveAlternateCompactContext(args: {
|
|
|
382
385
|
requestScopedMainCredential: hasForwardableCodexBearer(req.headers, config),
|
|
383
386
|
beginCodexAccountSelection: codexAccountSelectionForTurn(turnAdmissionLease),
|
|
384
387
|
});
|
|
385
|
-
|
|
388
|
+
// Caller-owned main has no Pool account id. It is still a valid one-shot alternate after a
|
|
389
|
+
// stored account fails; resolveCodexAuthContext already prevents returning it when main is the
|
|
390
|
+
// excluded credential.
|
|
391
|
+
if (authCtx.accountId === excludeAccountId) return null;
|
|
386
392
|
const provider = applyCodexAuthContextToProvider(route.provider, authCtx, route.codexAccountMode);
|
|
387
393
|
const headers = new Headers({ "content-type": "application/json" });
|
|
388
394
|
const selected = headersForCodexAuthContext(req.headers, authCtx);
|
|
@@ -395,7 +401,7 @@ async function resolveAlternateCompactContext(args: {
|
|
|
395
401
|
headers.set("authorization", `Bearer ${override.accessToken}`);
|
|
396
402
|
headers.set("chatgpt-account-id", override.chatgptAccountId);
|
|
397
403
|
}
|
|
398
|
-
if (provider.apiKey) headers.set("authorization", `Bearer ${
|
|
404
|
+
if (provider.apiKey) headers.set("authorization", `Bearer ${resolveProviderApiKey(provider.apiKey)}`);
|
|
399
405
|
return { authCtx, provider, headers };
|
|
400
406
|
} catch (err) {
|
|
401
407
|
if (err instanceof CodexMainProfileDrainingError) {
|
|
@@ -503,7 +509,10 @@ export async function handleResponsesCompact(
|
|
|
503
509
|
// Compact requests route through the same policy evaluation as normal
|
|
504
510
|
// turns, so body-derived evidence (tools/image) must reach the first
|
|
505
511
|
// evaluation too - not only the later handleResponses dispatch.
|
|
506
|
-
|
|
512
|
+
// Codex selects a bare native model for compaction even when the operator
|
|
513
|
+
// routes ordinary turns elsewhere (#2901); the compaction-scoped router
|
|
514
|
+
// may land that on the configured default provider instead of 404.
|
|
515
|
+
route = routeCompactionModel(config, raw.model, evidenceFromBody(raw));
|
|
507
516
|
} catch (err) {
|
|
508
517
|
if (err instanceof NoEligiblePolicyCandidateError) {
|
|
509
518
|
// Persist the evaluation trace (per-candidate exclusions + the
|
|
@@ -556,7 +565,10 @@ export async function handleResponsesCompact(
|
|
|
556
565
|
// Native /responses/compact exists on the canonical ChatGPT backend and on the
|
|
557
566
|
// official OpenAI API. Any other Responses-shaped gateway must take the routed
|
|
558
567
|
// summarizer path below, or compaction fails against an endpoint it never had (#422).
|
|
559
|
-
|
|
568
|
+
// Combo-resolved targets skip native compact so failover can advance through the
|
|
569
|
+
// combo target list when the picked model returns 429/5xx — the routed path below
|
|
570
|
+
// dispatches through handleResponses → handleComboResponses with full failover.
|
|
571
|
+
if (supportsNativeResponsesCompactEndpoint(route.providerName, route.provider) && !accountGatedCompactWireModel && !route.combo) {
|
|
560
572
|
if (req.signal.aborted) {
|
|
561
573
|
return formatErrorResponse(499, "client_cancelled", "Client cancelled compact request");
|
|
562
574
|
}
|
|
@@ -614,6 +626,9 @@ export async function handleResponsesCompact(
|
|
|
614
626
|
}
|
|
615
627
|
}
|
|
616
628
|
} catch (err) {
|
|
629
|
+
if (req.signal.aborted) {
|
|
630
|
+
return formatErrorResponse(499, "client_cancelled", "Client cancelled compact request");
|
|
631
|
+
}
|
|
617
632
|
const response = mapCodexAuthContextErrorToResponse(err, {
|
|
618
633
|
accountSelector: route.codexAccountNamespace,
|
|
619
634
|
now: Date.now(),
|
|
@@ -625,7 +640,7 @@ export async function handleResponsesCompact(
|
|
|
625
640
|
? CODEX_FORWARD_BASE_URL
|
|
626
641
|
: (compactProvider.baseUrl ?? "").replace(/\/+$/, "");
|
|
627
642
|
if (compactProvider.authMode !== "forward" && compactProvider.apiKey) {
|
|
628
|
-
headers.set("authorization", `Bearer ${
|
|
643
|
+
headers.set("authorization", `Bearer ${resolveProviderApiKey(compactProvider.apiKey)}`);
|
|
629
644
|
}
|
|
630
645
|
const { reasoning: _reasoning, ...compactBodyRaw } = raw as typeof raw & { reasoning?: unknown };
|
|
631
646
|
// The regular /v1/responses path applies sanitizeReasoningInputContent via the adapter's
|
|
@@ -994,8 +1009,10 @@ export async function handleResponsesCompact(
|
|
|
994
1009
|
...raw,
|
|
995
1010
|
// Canonical ChatGPT Responses rejects non-streaming turns. Daybreak cannot use the
|
|
996
1011
|
// native compact endpoint either, so run its synthetic compaction as SSE and collapse
|
|
997
|
-
// the completed event back into the v1 compact JSON contract below.
|
|
998
|
-
|
|
1012
|
+
// the completed event back into the v1 compact JSON contract below. Combo-dispatched
|
|
1013
|
+
// turns also go out as SSE: failover can land on a canonical child that rejects a
|
|
1014
|
+
// non-streaming turn, and every combo-capable provider already serves streaming traffic.
|
|
1015
|
+
stream: accountGatedCompactWireModel || route.combo ? true : false,
|
|
999
1016
|
input: [...inputItems, { type: "compaction_trigger" }],
|
|
1000
1017
|
};
|
|
1001
1018
|
const internalHeaders = new Headers({ "content-type": "application/json" });
|
|
@@ -1069,9 +1086,12 @@ export async function handleResponsesCompact(
|
|
|
1069
1086
|
`compaction turn produced ${compactionItems.length} compaction items, expected exactly 1`,
|
|
1070
1087
|
);
|
|
1071
1088
|
}
|
|
1072
|
-
//
|
|
1073
|
-
// and should not decrypt it; /responses/compact callers
|
|
1074
|
-
|
|
1089
|
+
// Native Responses backends return a real opaque OpenAI-encrypted compaction item. OCX cannot
|
|
1090
|
+
// and should not decrypt it; preserve that item for /responses/compact callers. Synthetic
|
|
1091
|
+
// routed summaries are our `ocx1:` envelope and must be decoded into v1 history items.
|
|
1092
|
+
if (typeof compactionItems[0]!.encrypted_content === "string"
|
|
1093
|
+
&& compactionItems[0]!.encrypted_content.trim().length > 0
|
|
1094
|
+
&& !compactionItems[0]!.encrypted_content.startsWith("ocx1:")) {
|
|
1075
1095
|
const result = new Response(JSON.stringify({ output: compactionItems }), {
|
|
1076
1096
|
headers: { "Content-Type": "application/json" },
|
|
1077
1097
|
});
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { bridgeToResponsesSSE } from "../../bridge";
|
|
2
|
+
import type { TranslatorBudget } from "../../lib/translator-budget";
|
|
3
|
+
import type { AdapterEvent } from "../../types";
|
|
4
|
+
|
|
5
|
+
export const PROVIDER_INPUT_TOO_LARGE_MESSAGE =
|
|
6
|
+
"The provider rejected this turn because its input exceeds the provider size or context limit. Reduce the current input or compact the conversation before retrying.";
|
|
7
|
+
|
|
8
|
+
async function* contextOverflowEvents(): AsyncGenerator<AdapterEvent> {
|
|
9
|
+
yield {
|
|
10
|
+
type: "error",
|
|
11
|
+
message: PROVIDER_INPUT_TOO_LARGE_MESSAGE,
|
|
12
|
+
status: 413,
|
|
13
|
+
errorType: "invalid_request_error",
|
|
14
|
+
code: "context_length_exceeded",
|
|
15
|
+
retryable: false,
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Convert a pre-stream provider 413 into the terminal Responses event Codex understands.
|
|
21
|
+
*
|
|
22
|
+
* Codex treats an HTTP 413 as an unexpected, retryable transport failure and resends the
|
|
23
|
+
* same oversized body through its reconnect budget. A `response.failed` event carrying
|
|
24
|
+
* `context_length_exceeded` is instead terminal and marks the client context as full, so
|
|
25
|
+
* its next-turn compaction policy can run. The message is proxy-owned on purpose: upstream
|
|
26
|
+
* 413 bodies can echo request data and are not needed to classify an unambiguous status.
|
|
27
|
+
*/
|
|
28
|
+
export function streamingContextOverflowResponse(
|
|
29
|
+
modelId: string,
|
|
30
|
+
translatorBudget: TranslatorBudget,
|
|
31
|
+
): Response {
|
|
32
|
+
return new Response(bridgeToResponsesSSE(
|
|
33
|
+
contextOverflowEvents(),
|
|
34
|
+
modelId,
|
|
35
|
+
undefined,
|
|
36
|
+
undefined,
|
|
37
|
+
undefined,
|
|
38
|
+
undefined,
|
|
39
|
+
2_000,
|
|
40
|
+
{ translatorBudget },
|
|
41
|
+
), {
|
|
42
|
+
headers: {
|
|
43
|
+
"Content-Type": "text/event-stream",
|
|
44
|
+
"Cache-Control": "no-cache",
|
|
45
|
+
Connection: "keep-alive",
|
|
46
|
+
"X-Accel-Buffering": "no",
|
|
47
|
+
},
|
|
48
|
+
});
|
|
49
|
+
}
|
|
@@ -7,6 +7,10 @@ import {
|
|
|
7
7
|
backfillResponsesFieldsJson,
|
|
8
8
|
} from "./responses-field-backfill";
|
|
9
9
|
import { checkInputAdmission } from "./input-admission";
|
|
10
|
+
import {
|
|
11
|
+
checkOutboundBodySize,
|
|
12
|
+
describeOutboundBodyRefusal,
|
|
13
|
+
} from "./outbound-body-guard";
|
|
10
14
|
import { nativeContextLimits } from "../../codex/catalog";
|
|
11
15
|
import { describeUpstreamConnectFailure } from "./upstream-error";
|
|
12
16
|
import {
|
|
@@ -54,6 +58,7 @@ import {
|
|
|
54
58
|
import {
|
|
55
59
|
comboRouteDecisionTrace,
|
|
56
60
|
NoEligiblePolicyCandidateError,
|
|
61
|
+
routeCompactionModel,
|
|
57
62
|
routeConcreteModel,
|
|
58
63
|
routeModel,
|
|
59
64
|
type RouteResult,
|
|
@@ -241,6 +246,10 @@ import {
|
|
|
241
246
|
} from "../lifecycle";
|
|
242
247
|
import { redactSecretString, sanitizeLogMetadataString } from "../../lib/redact";
|
|
243
248
|
import { readBoundedResponseBody } from "../../lib/bounded-body";
|
|
249
|
+
import {
|
|
250
|
+
isRateLimitOrQuotaFailureMessage,
|
|
251
|
+
upstreamErrorMessageFromPayload,
|
|
252
|
+
} from "../../lib/errors";
|
|
244
253
|
import type { AdmissionLease } from "../../lib/admission";
|
|
245
254
|
import { supportedLadderFor } from "../effort-policy";
|
|
246
255
|
import { isThreadSpawnRequest } from "../effort-policy";
|
|
@@ -317,6 +326,11 @@ import {
|
|
|
317
326
|
restoreImageGenCallsInJson,
|
|
318
327
|
} from "../responses-image-gen-repair";
|
|
319
328
|
import { createResponsesModelPayloadRewrite, rewriteResponsesModelJson } from "../responses-model-rewrite";
|
|
329
|
+
import {
|
|
330
|
+
collectSelfNamedNamespaceScrubAuthorization,
|
|
331
|
+
createSelfNamedToolCallNamespaceScrubRewrite,
|
|
332
|
+
scrubSelfNamedToolCallNamespaceInJson,
|
|
333
|
+
} from "../responses-self-named-namespace-scrub";
|
|
320
334
|
import type { EffectiveSubagentRoster, SpawnAgentSurface } from "../../codex/catalog";
|
|
321
335
|
|
|
322
336
|
import { buildToolBridgeMaps, collabSurface, injectDeveloperMessage, multiAgentGuidanceText } from "./collaboration";
|
|
@@ -370,6 +384,7 @@ import {
|
|
|
370
384
|
} from "../responses-undeclared-tool-guard";
|
|
371
385
|
import { createGithubCopilotResponsesBlockRewrite } from "../github-copilot-responses-repair";
|
|
372
386
|
import { responsesJsonToSseStream } from "../responses-json-events";
|
|
387
|
+
import { streamingContextOverflowResponse } from "./context-overflow";
|
|
373
388
|
import { guardTerminalEventStream } from "./terminal-guard";
|
|
374
389
|
import {
|
|
375
390
|
emptyCompletionRetryEnabled,
|
|
@@ -904,8 +919,40 @@ async function shouldRetryCodexPoolAccountModel400(
|
|
|
904
919
|
}
|
|
905
920
|
|
|
906
921
|
/** Pre-stream quota/billing rejections that warrant one alternate-account attempt (#584). */
|
|
907
|
-
|
|
908
|
-
|
|
922
|
+
function codexQuotaFailureMessage(body: string): string | undefined {
|
|
923
|
+
try {
|
|
924
|
+
const payload = JSON.parse(body) as unknown;
|
|
925
|
+
const canonical = upstreamErrorMessageFromPayload(payload);
|
|
926
|
+
if (canonical !== undefined) return canonical;
|
|
927
|
+
if (typeof payload === "string") return payload;
|
|
928
|
+
if (!payload || typeof payload !== "object" || Array.isArray(payload)) return undefined;
|
|
929
|
+
const record = payload as Record<string, unknown>;
|
|
930
|
+
if (typeof record.message === "string") return record.message;
|
|
931
|
+
return typeof record.error === "string" ? record.error : undefined;
|
|
932
|
+
} catch {
|
|
933
|
+
// Plain-text gateways remain supported. Valid JSON is inspected only at recognized
|
|
934
|
+
// message fields so echoed request content elsewhere cannot trigger account cooldown.
|
|
935
|
+
return body;
|
|
936
|
+
}
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
export async function shouldRetryCodexPoolAccountQuota(
|
|
940
|
+
response: Response,
|
|
941
|
+
signal?: AbortSignal,
|
|
942
|
+
): Promise<boolean> {
|
|
943
|
+
if (response.status === 402 || response.status === 429) return true;
|
|
944
|
+
if (response.status < 500 || response.status >= 600) return false;
|
|
945
|
+
try {
|
|
946
|
+
// Reject malformed UTF-8 instead of matching quota words around replacement characters.
|
|
947
|
+
const body = await readBoundedResponseBody(response.clone(), { signal, fatalUtf8: true });
|
|
948
|
+
const message = body.displaySafe && !body.truncated
|
|
949
|
+
? codexQuotaFailureMessage(body.text)
|
|
950
|
+
: undefined;
|
|
951
|
+
return message !== undefined
|
|
952
|
+
&& isRateLimitOrQuotaFailureMessage(message);
|
|
953
|
+
} catch {
|
|
954
|
+
return false;
|
|
955
|
+
}
|
|
909
956
|
}
|
|
910
957
|
|
|
911
958
|
interface CodexPoolAccountRetryArgs {
|
|
@@ -945,7 +992,7 @@ interface CodexPoolAccountRetryArgs {
|
|
|
945
992
|
stream: boolean;
|
|
946
993
|
onResponse?: (
|
|
947
994
|
response: Response,
|
|
948
|
-
authCtx:
|
|
995
|
+
authCtx: CodexAuthContext,
|
|
949
996
|
request: Awaited<ReturnType<ReturnType<typeof resolveAdapter>["buildRequest"]>>,
|
|
950
997
|
) => void;
|
|
951
998
|
}
|
|
@@ -953,7 +1000,7 @@ interface CodexPoolAccountRetryArgs {
|
|
|
953
1000
|
type CodexPoolAccountRetryResult =
|
|
954
1001
|
| {
|
|
955
1002
|
kind: "retried";
|
|
956
|
-
authCtx:
|
|
1003
|
+
authCtx: CodexAuthContext;
|
|
957
1004
|
request: Awaited<ReturnType<ReturnType<typeof resolveAdapter>["buildRequest"]>>;
|
|
958
1005
|
upstreamResponse: Response;
|
|
959
1006
|
selectedForwardHeaders: Headers;
|
|
@@ -962,7 +1009,7 @@ type CodexPoolAccountRetryResult =
|
|
|
962
1009
|
| {
|
|
963
1010
|
kind: "transport";
|
|
964
1011
|
error: unknown;
|
|
965
|
-
authCtx:
|
|
1012
|
+
authCtx: CodexAuthContext;
|
|
966
1013
|
};
|
|
967
1014
|
|
|
968
1015
|
/** Keep retry-stage entitlement snapshots inside the native-main selection fence. */
|
|
@@ -1129,7 +1176,27 @@ async function retryCodexPoolOnAlternateAccount(
|
|
|
1129
1176
|
throw error;
|
|
1130
1177
|
}
|
|
1131
1178
|
}
|
|
1132
|
-
|
|
1179
|
+
// A validated request-owned main bearer is a real alternate when the failed credential was a
|
|
1180
|
+
// stored Pool account. It has no Pool account id to promote or cool, but it can own this one
|
|
1181
|
+
// bounded replay. The resolver already refuses it when main itself is the excluded credential.
|
|
1182
|
+
if (
|
|
1183
|
+
retryAuthCtx?.kind !== "pool"
|
|
1184
|
+
&& retryAuthCtx?.kind !== "main-pool"
|
|
1185
|
+
&& retryAuthCtx?.kind !== "main"
|
|
1186
|
+
) {
|
|
1187
|
+
// A body-confirmed quota response may arrive under HTTP 5xx. Without an alternate,
|
|
1188
|
+
// the ordinary terminal recorder sees only that wire status and would misclassify it
|
|
1189
|
+
// as transient, leaving the exhausted account immediately selectable next turn.
|
|
1190
|
+
if (outcomeStatus !== firstResponse.status && (outcomeStatus === 429 || outcomeStatus === 402)) {
|
|
1191
|
+
recordCodexUpstreamOutcome(config, firstAuthCtx.accountId, outcomeStatus, {
|
|
1192
|
+
...codexQuotaOutcomeMeta(firstResponse),
|
|
1193
|
+
threadId: firstAuthCtx.affinityKey,
|
|
1194
|
+
modelId: route.modelId,
|
|
1195
|
+
probeLeaseId: codexProbeLeaseId(firstAuthCtx),
|
|
1196
|
+
probeQuotaScope: codexProbeQuotaScope(firstAuthCtx),
|
|
1197
|
+
writerGeneration: firstAuthCtx.writerGeneration,
|
|
1198
|
+
});
|
|
1199
|
+
}
|
|
1133
1200
|
return { kind: "no-alternate" };
|
|
1134
1201
|
}
|
|
1135
1202
|
|
|
@@ -1243,6 +1310,9 @@ async function retryCodexPoolOnAlternateAccount(
|
|
|
1243
1310
|
retrySendCount += 1;
|
|
1244
1311
|
args.onResponse?.(upstreamResponse, retryAuthCtx, request);
|
|
1245
1312
|
if (!retrySameConfirmedAccount || retrySendCount >= maxRetrySends) break;
|
|
1313
|
+
// Caller-owned main is an alternate-account replay and can never enter the bounded
|
|
1314
|
+
// same-stored-account 400 loop above. Keep that invariant explicit for the account-id reads.
|
|
1315
|
+
if (retryAuthCtx.kind === "main") break;
|
|
1246
1316
|
if (!await shouldRetryCodexPoolAccountModel400(
|
|
1247
1317
|
upstreamResponse,
|
|
1248
1318
|
route.modelId,
|
|
@@ -1757,6 +1827,9 @@ async function resolveResponsesCodexAuth(
|
|
|
1757
1827
|
substituteMainCredential,
|
|
1758
1828
|
};
|
|
1759
1829
|
} catch (err) {
|
|
1830
|
+
if (options.abortSignal?.aborted || req.signal.aborted) {
|
|
1831
|
+
return { ok: false, response: clientCancelledResponse() };
|
|
1832
|
+
}
|
|
1760
1833
|
if (err instanceof CodexAuthContextError) {
|
|
1761
1834
|
const safeAccountLabel = route.codexAccountNamespace
|
|
1762
1835
|
? `${route.providerName}-${route.codexAccountNamespace}`
|
|
@@ -1902,6 +1975,9 @@ async function refreshNativeMainForwardAuth(args: {
|
|
|
1902
1975
|
});
|
|
1903
1976
|
return { ok: true, authCtx: refreshedAuthCtx, provider, headers };
|
|
1904
1977
|
} catch (error) {
|
|
1978
|
+
if (options.abortSignal?.aborted || req.signal.aborted) {
|
|
1979
|
+
return { ok: false, response: clientCancelledResponse() };
|
|
1980
|
+
}
|
|
1905
1981
|
return { ok: false, response: nativeMainRefreshFailureResponse(error) };
|
|
1906
1982
|
}
|
|
1907
1983
|
}
|
|
@@ -2102,7 +2178,7 @@ export async function handleComboResponses(
|
|
|
2102
2178
|
comboId: string,
|
|
2103
2179
|
config: OcxConfig,
|
|
2104
2180
|
logCtx: RequestLogContext,
|
|
2105
|
-
options: HandleResponsesOptions,
|
|
2181
|
+
options: HandleResponsesOptions & { translatorBudget: TranslatorBudget },
|
|
2106
2182
|
): Promise<Response> {
|
|
2107
2183
|
const requestedModel = typeof (rawBody as { model?: unknown } | null)?.model === "string"
|
|
2108
2184
|
? (rawBody as { model: string }).model
|
|
@@ -2451,6 +2527,12 @@ export async function handleComboResponses(
|
|
|
2451
2527
|
code: failure.upstreamCode,
|
|
2452
2528
|
}) === "stop") {
|
|
2453
2529
|
adoptFailedChildLog(childLog);
|
|
2530
|
+
if (
|
|
2531
|
+
failure.response.status === 413
|
|
2532
|
+
&& (rawBody as { stream?: unknown } | null)?.stream === true
|
|
2533
|
+
) {
|
|
2534
|
+
return streamingContextOverflowResponse(requestedModel, options.translatorBudget);
|
|
2535
|
+
}
|
|
2454
2536
|
return lastFailure;
|
|
2455
2537
|
}
|
|
2456
2538
|
console.warn(
|
|
@@ -2464,6 +2546,12 @@ export async function handleComboResponses(
|
|
|
2464
2546
|
if (!nextPick) adoptFailedChildLog(childLog);
|
|
2465
2547
|
pick = nextPick;
|
|
2466
2548
|
}
|
|
2549
|
+
if (
|
|
2550
|
+
lastFailure?.status === 413
|
|
2551
|
+
&& (rawBody as { stream?: unknown } | null)?.stream === true
|
|
2552
|
+
) {
|
|
2553
|
+
return streamingContextOverflowResponse(requestedModel, options.translatorBudget);
|
|
2554
|
+
}
|
|
2467
2555
|
return lastFailure!;
|
|
2468
2556
|
}
|
|
2469
2557
|
|
|
@@ -2750,9 +2838,15 @@ async function handleResponsesInner(
|
|
|
2750
2838
|
|
|
2751
2839
|
let route: RouteResult;
|
|
2752
2840
|
try {
|
|
2841
|
+
// A `compaction_trigger` turn may name a bare native model the operator has
|
|
2842
|
+
// no canonical OpenAI route for (#2901). Only the initial compaction route
|
|
2843
|
+
// may fall back to the configured default provider; combo attempts and the
|
|
2844
|
+
// later fallback/recovery re-routes keep the ordinary reservation.
|
|
2753
2845
|
const resolveRoute = (modelId: string) => options.comboAttempt
|
|
2754
2846
|
? routeConcreteModel(config, modelId)
|
|
2755
|
-
:
|
|
2847
|
+
: parsed._compactionRequest === true
|
|
2848
|
+
? routeCompactionModel(config, modelId, evidenceFromBody(parsed._rawBody))
|
|
2849
|
+
: routeModel(config, modelId, evidenceFromBody(parsed._rawBody));
|
|
2756
2850
|
const _sci = config.shadowCallIntercept;
|
|
2757
2851
|
let shadowRoute: RouteResult | undefined;
|
|
2758
2852
|
if (_sci?.enabled && _sci.model && isShadowSourceModel(parsed.modelId, _sci.sourceModels)) {
|
|
@@ -3097,6 +3191,12 @@ async function handleResponsesInner(
|
|
|
3097
3191
|
// instead of treating the first incompatible candidate as the end of the chain. The
|
|
3098
3192
|
// distinct code is what lets the fallback layer tell the two apart -- an upstream
|
|
3099
3193
|
// `context_length_exceeded` still stops, because retrying it elsewhere is guesswork.
|
|
3194
|
+
if (clientRequestedStream && !options.comboAttempt) {
|
|
3195
|
+
return streamingContextOverflowResponse(
|
|
3196
|
+
parsed._responseModelId ?? parsed.modelId,
|
|
3197
|
+
translatorBudget,
|
|
3198
|
+
);
|
|
3199
|
+
}
|
|
3100
3200
|
return formatErrorResponse(
|
|
3101
3201
|
413,
|
|
3102
3202
|
"input_admission_refused",
|
|
@@ -3562,6 +3662,11 @@ async function handleResponsesInner(
|
|
|
3562
3662
|
parsed._rawBody,
|
|
3563
3663
|
replayedInputPrefixLength,
|
|
3564
3664
|
);
|
|
3665
|
+
const selfNamedNamespaceScrubAuthorization = collectSelfNamedNamespaceScrubAuthorization(
|
|
3666
|
+
clientToolAuthorizationBody,
|
|
3667
|
+
toolBridgeMaps.bareCustomToolNames,
|
|
3668
|
+
toolBridgeMaps.bareFunctionToolNames,
|
|
3669
|
+
);
|
|
3565
3670
|
const clientExplicitWireToolCatalog = hasExplicitWireToolCatalog(clientToolAuthorizationBody);
|
|
3566
3671
|
const clientDeclaredWireToolNames = collectDeclaredWireToolNames(clientToolAuthorizationBody);
|
|
3567
3672
|
const clientDeclaredNamelessCallTypes = collectDeclaredNamelessClientCallTypes(
|
|
@@ -3819,6 +3924,48 @@ async function handleResponsesInner(
|
|
|
3819
3924
|
linkAbortSignal(upstream, options.abortSignal);
|
|
3820
3925
|
const connectMs = config.connectTimeoutMs ?? 200_000;
|
|
3821
3926
|
let upstreamResponse: Response;
|
|
3927
|
+
/**
|
|
3928
|
+
* Refuse a built body that exceeds the operator's configured ceiling, before it is sent.
|
|
3929
|
+
*
|
|
3930
|
+
* Unconfigured this measures nothing and returns undefined, so an unset proxy behaves
|
|
3931
|
+
* exactly as it does today. Runs at every point a body is built or rebuilt, because a
|
|
3932
|
+
* rebuild can produce a payload the initial check never saw.
|
|
3933
|
+
*/
|
|
3934
|
+
const refuseOversizedOutboundBody = (
|
|
3935
|
+
builtRequest: AdapterRequest,
|
|
3936
|
+
refusalAuthCtx: CodexAuthContext = authCtx,
|
|
3937
|
+
): Response | undefined => {
|
|
3938
|
+
const result = checkOutboundBodySize(builtRequest.body, config.maxUpstreamBodyBytes);
|
|
3939
|
+
if (result.admitted) return undefined;
|
|
3940
|
+
|
|
3941
|
+
// This returns before the surrounding fetch/finally owns the observation, so release
|
|
3942
|
+
// it here or one refused body holds translator budget for the process lifetime.
|
|
3943
|
+
builtRequest.releaseBodyObservation?.();
|
|
3944
|
+
upstream.abort();
|
|
3945
|
+
releaseUpstreamHostAdmission(hostAdmissionLease);
|
|
3946
|
+
hostAdmissionLease = null;
|
|
3947
|
+
releaseCodexAuthContextProbeLease(refusalAuthCtx);
|
|
3948
|
+
logCtx.errorCode = "outbound_body_too_large";
|
|
3949
|
+
console.warn(
|
|
3950
|
+
`[responses] refused an oversized outbound body: bytes=${result.bytes} limit=${result.limit} `
|
|
3951
|
+
+ `input_images=${result.imageCount} image_bytes=${result.imageBytes} `
|
|
3952
|
+
+ `model=${JSON.stringify(parsed.modelId)}`,
|
|
3953
|
+
);
|
|
3954
|
+
// A streaming client treats HTTP 413 as a retryable transport error and resends the same
|
|
3955
|
+
// oversized body — the reconnect loop #3177 exists to stop. Terminal overflow is the
|
|
3956
|
+
// honest shape, and it is what the upstream-413 path already returns.
|
|
3957
|
+
if (clientRequestedStream) {
|
|
3958
|
+
return streamingContextOverflowResponse(
|
|
3959
|
+
parsed._responseModelId ?? parsed.modelId,
|
|
3960
|
+
translatorBudget,
|
|
3961
|
+
);
|
|
3962
|
+
}
|
|
3963
|
+
return formatErrorResponse(
|
|
3964
|
+
413,
|
|
3965
|
+
"outbound_body_too_large",
|
|
3966
|
+
describeOutboundBodyRefusal(result),
|
|
3967
|
+
);
|
|
3968
|
+
};
|
|
3822
3969
|
const transportFailureResponse = (err: unknown): Response => {
|
|
3823
3970
|
upstream.abort();
|
|
3824
3971
|
if (options.abortSignal?.aborted) {
|
|
@@ -3861,6 +4008,8 @@ async function handleResponsesInner(
|
|
|
3861
4008
|
: describeUpstreamConnectFailure(err, connectMs);
|
|
3862
4009
|
return formatErrorResponse(502, "upstream_error", msg);
|
|
3863
4010
|
};
|
|
4011
|
+
const initialBodyRefusal = refuseOversizedOutboundBody(request);
|
|
4012
|
+
if (initialBodyRefusal) return initialBodyRefusal;
|
|
3864
4013
|
try {
|
|
3865
4014
|
// Transient-5xx pre-stream retry (devlog/_plan/260716_claudecode_hardening/010):
|
|
3866
4015
|
// the ChatGPT backend emits transient 502/520s that an immediate retry absorbs.
|
|
@@ -3935,6 +4084,8 @@ async function handleResponsesInner(
|
|
|
3935
4084
|
retryAdapter.name,
|
|
3936
4085
|
logCtx.accountLogLabel,
|
|
3937
4086
|
);
|
|
4087
|
+
const rebuiltBodyRefusal = refuseOversizedOutboundBody(request);
|
|
4088
|
+
if (rebuiltBodyRefusal) return { failed: rebuiltBodyRefusal };
|
|
3938
4089
|
try {
|
|
3939
4090
|
return await fetchWithTransientRetry(
|
|
3940
4091
|
innerRecovery => {
|
|
@@ -4027,6 +4178,10 @@ async function handleResponsesInner(
|
|
|
4027
4178
|
recordAdapterReasoning(logCtx, request);
|
|
4028
4179
|
recordAdapterTier(logCtx, request);
|
|
4029
4180
|
refreshUndeclaredToolGuard(request);
|
|
4181
|
+
// The 401 replay rebuilds the body before sending, so it needs the same ceiling as
|
|
4182
|
+
// every other build site; a replay is exactly when a grown payload reappears.
|
|
4183
|
+
const replayBodyRefusal = refuseOversizedOutboundBody(request);
|
|
4184
|
+
if (replayBodyRefusal) return replayBodyRefusal;
|
|
4030
4185
|
noteAttemptSend(logCtx.activeAttempt, passthroughEstimate, "oauth-401");
|
|
4031
4186
|
upstreamResponse = await fetchWithHeaderTimeout(
|
|
4032
4187
|
request.url,
|
|
@@ -4136,6 +4291,8 @@ async function handleResponsesInner(
|
|
|
4136
4291
|
return formatErrorResponse(400, "invalid_request_error", redactSecretString(msg));
|
|
4137
4292
|
}
|
|
4138
4293
|
refreshUndeclaredToolGuard(request);
|
|
4294
|
+
const refreshedBodyRefusal = refuseOversizedOutboundBody(request);
|
|
4295
|
+
if (refreshedBodyRefusal) return refreshedBodyRefusal;
|
|
4139
4296
|
try {
|
|
4140
4297
|
upstreamResponse = await fetchWithTransientRetry(
|
|
4141
4298
|
recovery => {
|
|
@@ -4255,9 +4412,14 @@ async function handleResponsesInner(
|
|
|
4255
4412
|
options.abortSignal,
|
|
4256
4413
|
)) {
|
|
4257
4414
|
poolRetryOutcome = 400;
|
|
4258
|
-
} else if (!authCtx.fixedAccount && shouldRetryCodexPoolAccountQuota(
|
|
4415
|
+
} else if (!authCtx.fixedAccount && await shouldRetryCodexPoolAccountQuota(
|
|
4416
|
+
upstreamResponse,
|
|
4417
|
+
options.abortSignal,
|
|
4418
|
+
)) {
|
|
4259
4419
|
// Pre-stream only: once SSE has begun, mid-stream quota stays terminal.
|
|
4260
|
-
|
|
4420
|
+
// ChatGPT sometimes wraps quota exhaustion in a generic 5xx. Normalize only
|
|
4421
|
+
// body-confirmed cases to quota evidence so cooldown and rotation both apply.
|
|
4422
|
+
poolRetryOutcome = upstreamResponse.status >= 500 ? 429 : upstreamResponse.status;
|
|
4261
4423
|
}
|
|
4262
4424
|
|
|
4263
4425
|
if (poolRetryOutcome !== undefined) {
|
|
@@ -4285,7 +4447,12 @@ async function handleResponsesInner(
|
|
|
4285
4447
|
passthroughEstimate,
|
|
4286
4448
|
stream: parsed.stream,
|
|
4287
4449
|
onResponse: (response, retryAuthCtx, retryRequest) => {
|
|
4288
|
-
captureAffinityResponse(
|
|
4450
|
+
captureAffinityResponse(
|
|
4451
|
+
response,
|
|
4452
|
+
retryAuthCtx,
|
|
4453
|
+
retryRequest,
|
|
4454
|
+
retryAuthCtx.kind !== "main",
|
|
4455
|
+
);
|
|
4289
4456
|
},
|
|
4290
4457
|
});
|
|
4291
4458
|
if (retry.kind === "transport") {
|
|
@@ -4432,6 +4599,12 @@ async function handleResponsesInner(
|
|
|
4432
4599
|
// The bounded reader owns the original body, deadline, abort settlement, and lock.
|
|
4433
4600
|
// Unsafe partial data falls back to #452's non-empty status-only JSON.
|
|
4434
4601
|
const errorText = await readDisplaySafeErrorText(upstreamResponse, upstream.signal, "");
|
|
4602
|
+
if (upstreamResponse.status === 413 && clientRequestedStream) {
|
|
4603
|
+
return streamingContextOverflowResponse(
|
|
4604
|
+
parsed._responseModelId ?? parsed.modelId,
|
|
4605
|
+
translatorBudget,
|
|
4606
|
+
);
|
|
4607
|
+
}
|
|
4435
4608
|
return formatPassthroughUpstreamError(upstreamResponse.status, errorText, {
|
|
4436
4609
|
statusText: upstreamResponse.statusText,
|
|
4437
4610
|
headers,
|
|
@@ -4478,6 +4651,8 @@ async function handleResponsesInner(
|
|
|
4478
4651
|
// Compose opt-in payload rewrites into one parse/stringify pass (image-gen restore first).
|
|
4479
4652
|
const payloadRewrites = [
|
|
4480
4653
|
createImageGenCallRestoreRewrite(imageGenCallAliases),
|
|
4654
|
+
// #3217: a call whose namespace repeats its own name is unroutable in codex-rs.
|
|
4655
|
+
createSelfNamedToolCallNamespaceScrubRewrite(selfNamedNamespaceScrubAuthorization),
|
|
4481
4656
|
routedNamespaceToolAliases.size > 0
|
|
4482
4657
|
? createRoutedNamespaceCallRestoreRewrite(routedNamespaceToolAliases)
|
|
4483
4658
|
: undefined,
|
|
@@ -4710,7 +4885,10 @@ async function handleResponsesInner(
|
|
|
4710
4885
|
inspectResponseLogJson(logCtx, text);
|
|
4711
4886
|
const clientJson = (() => {
|
|
4712
4887
|
const restoredNamespace = restoreRoutedNamespaceCallsInJson(
|
|
4713
|
-
|
|
4888
|
+
scrubSelfNamedToolCallNamespaceInJson(
|
|
4889
|
+
restoreImageGenCallsInJson(text, imageGenCallAliases),
|
|
4890
|
+
selfNamedNamespaceScrubAuthorization,
|
|
4891
|
+
),
|
|
4714
4892
|
routedNamespaceToolAliases,
|
|
4715
4893
|
);
|
|
4716
4894
|
const restoredAuthorizedBareNamespace = restoreRoutedNamespaceCallsInJson(
|
|
@@ -5986,6 +6164,12 @@ async function handleResponsesInner(
|
|
|
5986
6164
|
} finally {
|
|
5987
6165
|
cleanupUpstreamAbort();
|
|
5988
6166
|
}
|
|
6167
|
+
if (upstreamResponse.status === 413 && clientRequestedStream && !options.comboAttempt) {
|
|
6168
|
+
return streamingContextOverflowResponse(
|
|
6169
|
+
parsed._responseModelId ?? parsed.modelId,
|
|
6170
|
+
translatorBudget,
|
|
6171
|
+
);
|
|
6172
|
+
}
|
|
5989
6173
|
if (!isFixedCodexAccount(authCtx)) {
|
|
5990
6174
|
recordSubagentQuotaFailureForThreadSpawn(
|
|
5991
6175
|
req.headers,
|
|
@@ -76,7 +76,8 @@ export function providerFetch(
|
|
|
76
76
|
// transport (measured ~3s faster TTFT than the SSE POST queue); everything
|
|
77
77
|
// else keeps the provider's HTTP fetch. See ws-upstream.ts for the details.
|
|
78
78
|
const unpaced = async (input: Parameters<typeof globalThis.fetch>[0], init?: RequestInit) => {
|
|
79
|
-
|
|
79
|
+
const upstreamWebsocket = provider.upstreamWebsocket === true;
|
|
80
|
+
if (typeof input === "string" && init && shouldUseCodexWsUpstream(input, init, runtime, upstreamWebsocket)) {
|
|
80
81
|
// The fallback has to be the same HTTP fetch the non-WS branch would have
|
|
81
82
|
// used, protocol pin included: a WS turn that falls back is serving the
|
|
82
83
|
// request over HTTP, and dropping the provider's `upstreamHttpVersion`
|