@bitkyc08/opencodex 2.88.0 → 2.89.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 +11 -1
- package/bin/ocx.mjs +15 -4
- package/gui/dist/assets/App-BnRSujOq.js +58 -0
- package/gui/dist/assets/App-dnkJ85te.css +1 -0
- package/gui/dist/assets/{Tray-B5bC7AWL.js → Tray-7z1COnmz.js} +1 -1
- package/gui/dist/assets/{index-B5bsvxor.js → index-uchD-ngf.js} +13 -13
- package/gui/dist/assets/{quota-summary-D8y2wTsB.js → quota-summary-BY_10IEz.js} +1 -1
- package/gui/dist/index.html +1 -1
- package/package.json +1 -1
- package/src/adapters/anthropic/cache-retention.ts +117 -0
- package/src/adapters/anthropic/passthrough.ts +7 -4
- package/src/adapters/anthropic.ts +10 -3
- package/src/adapters/coding-agent/protocol.ts +6 -0
- package/src/adapters/google-http.ts +3 -2
- package/src/adapters/google-vertex-endpoint.ts +67 -0
- package/src/adapters/google.ts +35 -29
- package/src/adapters/openai-responses/compaction-search-history.ts +27 -0
- package/src/adapters/openai-responses/passthrough.ts +17 -12
- package/src/adapters/openai-responses/reasoning.ts +68 -1
- package/src/adapters/registry.ts +1 -1
- package/src/adapters/upstream-http-error.ts +3 -2
- package/src/adapters/zed.ts +7 -4
- package/src/bridge/internal.ts +11 -1
- package/src/bridge/response-json.ts +2 -2
- package/src/bridge/sse.ts +10 -4
- package/src/claude/inbound.ts +12 -26
- package/src/claude/outbound.ts +5 -0
- package/src/cli/account-api.ts +13 -1
- package/src/cli/account-extended.ts +17 -5
- package/src/cli/account-key-quota.ts +4 -1
- package/src/cli/account.ts +1 -1
- package/src/cli/capabilities-agents-routing.ts +45 -8
- package/src/cli/capabilities-base.ts +5 -5
- package/src/cli/capabilities-provider-models.ts +5 -4
- package/src/cli/combo-result.ts +8 -2
- package/src/cli/combo.ts +17 -0
- package/src/cli/help.ts +1 -1
- package/src/cli/message-command.ts +4 -1
- package/src/cli/message-remote-args.ts +69 -0
- package/src/cli/message-remote-command.ts +77 -0
- package/src/cli/provider-runtime.ts +69 -1
- package/src/cli/registry.ts +3 -3
- package/src/codex/account-pause.ts +6 -1
- package/src/codex/account-runtime-state.ts +5 -0
- package/src/codex/account-store.ts +30 -3
- package/src/codex/auth-api/account-list.ts +3 -0
- package/src/codex/auth-api/main-account-probe.ts +3 -2
- package/src/codex/auth-context.ts +9 -4
- package/src/codex/catalog/aggregation.ts +15 -0
- package/src/codex/catalog/auto-review.ts +2 -1
- package/src/codex/catalog/build-entries.ts +17 -7
- package/src/codex/catalog/derive-entry.ts +3 -1
- package/src/codex/catalog/metadata.ts +2 -0
- package/src/codex/catalog/model-visibility.ts +0 -2
- package/src/codex/catalog/ownership.ts +71 -0
- package/src/codex/catalog/parsing.ts +12 -3
- package/src/codex/catalog/retained-sync.ts +2 -1
- package/src/codex/catalog/routed-removal.ts +4 -4
- package/src/codex/convergence.ts +2 -1
- package/src/codex/low-quota-desktop-notifier.ts +125 -0
- package/src/codex/low-quota-protection.ts +57 -31
- package/src/codex/main-account-cache.ts +77 -3
- package/src/codex/main-account.ts +6 -2
- package/src/codex/native-residue.ts +2 -3
- package/src/codex/quota.ts +26 -2
- package/src/combos/failover.ts +2 -1
- package/src/combos/jev-model-backend.ts +30 -7
- package/src/combos/jev-model-request.ts +20 -0
- package/src/combos/jev-quota-activation.ts +153 -0
- package/src/combos/jev-quota-config.ts +62 -0
- package/src/combos/jev-quota-route.ts +26 -0
- package/src/combos/jev-quota.ts +88 -0
- package/src/combos/jev.ts +25 -12
- package/src/combos/types.ts +27 -0
- package/src/compatibility/index.ts +3 -0
- package/src/compatibility/openai-responses.ts +8 -1
- package/src/compatibility/opengateway-responses.ts +61 -0
- package/src/config/account-auto-pause-revisions.ts +136 -0
- package/src/config/diagnostics.ts +13 -0
- package/src/config/live-reconcile.ts +9 -0
- package/src/config/persist-unlocked.ts +10 -1
- package/src/config/removed-opencode-free.ts +135 -0
- package/src/config/schema/account-auto-pause.ts +14 -0
- package/src/config/schema/config-schema.ts +15 -0
- package/src/config/schema/leaf-validators.ts +7 -0
- package/src/config.ts +10 -3
- package/src/generated/compatibility-version.json +344 -144
- package/src/images/loop.ts +32 -9
- package/src/lib/account-auto-pause-response.ts +16 -0
- package/src/lib/bun-runtime.ts +18 -1
- package/src/lib/bun-transpiler-cache-scope.d.mts +8 -0
- package/src/lib/bun-transpiler-cache-scope.mjs +68 -0
- package/src/lib/decision-quota-sink.ts +76 -0
- package/src/lib/state-store-registrations.ts +25 -0
- package/src/link/ssh-argv.ts +3 -0
- package/src/messaging/envelope.ts +6 -3
- package/src/messaging/remote-auth.ts +83 -0
- package/src/messaging/remote-bridge.ts +270 -0
- package/src/messaging/remote-contract.ts +108 -0
- package/src/messaging/remote-enrollment-recovery.ts +59 -0
- package/src/messaging/remote-enrollment.ts +244 -0
- package/src/messaging/remote-envelope.ts +40 -0
- package/src/messaging/remote-files.ts +117 -0
- package/src/messaging/remote-owner.ts +169 -0
- package/src/messaging/remote-ports.ts +56 -0
- package/src/messaging/remote-process.ts +131 -0
- package/src/messaging/remote-replies.ts +98 -0
- package/src/messaging/remote-rpc-admission.ts +32 -0
- package/src/messaging/remote-send.ts +72 -0
- package/src/messaging/remote-store.ts +111 -0
- package/src/messaging/remote-tunnels.ts +65 -0
- package/src/messaging/rpc.ts +13 -1
- package/src/oauth/account-auto-pause-policy.ts +66 -0
- package/src/oauth/account-auto-pause.ts +171 -0
- package/src/oauth/account-quota-rank.ts +2 -1
- package/src/oauth/anthropic-account-refusal.ts +6 -0
- package/src/oauth/anthropic-model-family.ts +8 -0
- package/src/oauth/anthropic-model-quota.ts +2 -6
- package/src/oauth/api-key-pool-preserve.ts +37 -0
- package/src/oauth/index.ts +5 -35
- package/src/oauth/store.ts +35 -8
- package/src/oauth/types.ts +2 -0
- package/src/plugins/upstream-hooks.ts +14 -1
- package/src/providers/api-key-pause.ts +32 -0
- package/src/providers/api-key-resolve.ts +10 -4
- package/src/providers/api-key-selection.ts +6 -1
- package/src/providers/api-keys.ts +8 -4
- package/src/providers/claude-cli-usage.ts +5 -0
- package/src/providers/key-failover.ts +11 -11
- package/src/providers/model-rename-fields.ts +1 -0
- package/src/providers/opencode-zen-rate-limit.ts +6 -72
- package/src/providers/quota/account-cache.ts +15 -1
- package/src/providers/quota/anthropic-account-quota.ts +2 -1
- package/src/providers/quota/anthropic-cooldown-recovery.ts +22 -4
- package/src/providers/quota/anthropic-decision-windows.ts +47 -0
- package/src/providers/quota/antigravity.ts +39 -6
- package/src/providers/quota/report-cache.ts +14 -0
- package/src/providers/quota/vendor-probes-oauth.ts +3 -3
- package/src/providers/quota-decision-publication.ts +77 -0
- package/src/providers/quota-decision-snapshot.ts +145 -0
- package/src/providers/quota-routing-cache.ts +3 -0
- package/src/providers/quota-types.ts +6 -1
- package/src/providers/quota.ts +3 -2
- package/src/providers/registry/entries-extended.ts +8 -52
- package/src/providers/registry/model-seeds.ts +1 -1
- package/src/providers/registry.ts +1 -2
- package/src/providers/stale-vision-classification-migration.ts +1 -1
- package/src/responses/parser.ts +18 -10
- package/src/responses/web-search-replay.ts +138 -0
- package/src/routing/compatibility/behavior.ts +4 -1
- package/src/server/auth-cors.ts +7 -0
- package/src/server/background-lifecycle.ts +2 -1
- package/src/server/chat-native.ts +28 -13
- package/src/server/claude-messages.ts +2 -1
- package/src/server/index/optional-listeners.ts +6 -0
- package/src/server/management/combo-routes.ts +17 -2
- package/src/server/management/oauth-account-routes.ts +10 -1
- package/src/server/management/provider-completion-test.ts +289 -0
- package/src/server/management/provider-routes.ts +9 -1
- package/src/server/management/route-registry.ts +2 -0
- package/src/server/messages-native.ts +18 -4
- package/src/server/messages-shared.ts +4 -1
- package/src/server/request-log.ts +7 -2
- package/src/server/responses/account-auto-pause-recovery.ts +26 -0
- package/src/server/responses/adapter-dispatch.ts +77 -28
- package/src/server/responses/combo-session-recall.ts +25 -0
- package/src/server/responses/combo-stream-preflight.ts +3 -1
- package/src/server/responses/combo-tool-loop-pin.ts +70 -0
- package/src/server/responses/compact.ts +8 -5
- package/src/server/responses/context-overflow.ts +141 -2
- package/src/server/responses/core-codex-account.ts +7 -1
- package/src/server/responses/core-combo.ts +31 -5
- package/src/server/responses/core.ts +12 -9
- package/src/server/responses/empty-completion-guard.ts +2 -0
- package/src/server/responses/fetch-helpers.ts +35 -4
- package/src/server/responses/jev-model-invoke.ts +13 -14
- package/src/server/responses/native-steering-log.ts +2 -2
- package/src/server/responses/passthrough-delivery.ts +3 -2
- package/src/server/responses/passthrough-dispatch.ts +30 -33
- package/src/server/responses/passthrough-observations.ts +38 -0
- package/src/server/responses/request-compression.ts +98 -0
- package/src/server/responses/request-transport.ts +32 -3
- package/src/server/responses/run-turn-execution.ts +2 -0
- package/src/server/responses/sidecar-execution.ts +16 -3
- package/src/server/responses/terminal-guard.ts +2 -0
- package/src/server/startup-action-control.ts +2 -2
- package/src/server/startup-health-cache.ts +2 -2
- package/src/server/windows-tray-control.ts +2 -2
- package/src/service/definition.ts +9 -1
- package/src/types/account-auto-pause.ts +15 -0
- package/src/types/config.ts +23 -1
- package/src/types/provider.ts +9 -5
- package/src/types/request.ts +20 -2
- package/src/types.ts +1 -0
- package/src/usage/cost.ts +17 -1
- package/src/usage/expected-prices.ts +3 -1
- package/src/usage/jev-stats.ts +4 -0
- package/src/usage/log.ts +2 -0
- package/src/usage/summary.ts +7 -0
- package/src/web-search/loop.ts +37 -11
- package/src/web-search/progress-stream.ts +4 -1
- package/src/web-search/run-turn-loop.ts +9 -3
- package/src/web-search/sidecar-search-replay.ts +96 -0
- package/gui/dist/assets/App-CHC89EXQ.js +0 -58
- package/gui/dist/assets/App-wT3mXqFz.css +0 -1
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { recoverAutoPausedAccount } from "./account-auto-pause-recovery";
|
|
2
|
+
import { adoptCodexWsStage, capturePassthroughAffinity } from "./passthrough-observations";
|
|
1
3
|
import { spendLedgerStorageErrorResponse } from "./spend-storage-error";
|
|
2
4
|
import { requestPacingOverloadResponse } from "./pacing-overload";
|
|
3
5
|
import { recordDistributedResponseFailure } from "../../providers/api-key-response-ownership";
|
|
@@ -99,7 +101,6 @@ import {
|
|
|
99
101
|
upstreamHostCircuitOpenResponse,
|
|
100
102
|
usesCodexForwardPoolAuth,
|
|
101
103
|
codexWsQuotaObserver,
|
|
102
|
-
isFixedCodexAccount,
|
|
103
104
|
codexPoolAccountModel400Denial,
|
|
104
105
|
shouldRetryCodexPoolAccountQuota,
|
|
105
106
|
shouldRetryCodexPoolAccountTransient,
|
|
@@ -111,7 +112,7 @@ import {
|
|
|
111
112
|
clearCodexModelDenialEvidence,
|
|
112
113
|
recordCodexModelDenialEvidence,
|
|
113
114
|
} from "../../codex/model-entitlements";
|
|
114
|
-
import { codexWsSocketDeathStage, isCodexWsUpstreamResponse
|
|
115
|
+
import { codexWsSocketDeathStage, isCodexWsUpstreamResponse } from "./codex-ws-wire";
|
|
115
116
|
import { linkAbortSignal } from "./core-lifetime";
|
|
116
117
|
import type { CodexAuthContext } from "../../codex/auth-context";
|
|
117
118
|
import { checkOutboundBodySize, describeOutboundBodyRefusal } from "./outbound-body-guard";
|
|
@@ -130,6 +131,7 @@ import {
|
|
|
130
131
|
} from "../../lib/upstream-retry";
|
|
131
132
|
import { mapCodexAuthContextErrorToResponse } from "./codex-auth-error";
|
|
132
133
|
import { classifyTransportFailureKind, transportErrorCode } from "../../lib/upstream-reachability";
|
|
134
|
+
import { captureAccountAutoPauseAttempt, wasAccountAutoPaused, wasPermanentAccountRefusal } from "../../oauth/account-auto-pause";
|
|
133
135
|
import { recordCodexUpstreamOutcome } from "../../codex/routing";
|
|
134
136
|
import { describeUpstreamConnectFailure } from "./upstream-error";
|
|
135
137
|
import type { OpaqueBlobRecoveryGuard } from "./core-opaque-recovery";
|
|
@@ -152,7 +154,6 @@ import {
|
|
|
152
154
|
rotateGenericOAuthAccountOn429,
|
|
153
155
|
failoverAccountSnapshot,
|
|
154
156
|
} from "../../oauth/generic-account-failover";
|
|
155
|
-
import { captureCodexAffinityDiagnostic } from "../../codex/affinity-debug";
|
|
156
157
|
import { ACCOUNT_GATED_NATIVE_OPENAI_MODELS } from "../../codex/catalog/native-models";
|
|
157
158
|
import {
|
|
158
159
|
attemptOpaqueBlobRecovery,
|
|
@@ -201,6 +202,7 @@ export async function preparePassthroughExchange(
|
|
|
201
202
|
| "genericFailoverAccountId"
|
|
202
203
|
| "passiveQuotaWriterGeneration"
|
|
203
204
|
| "oauthDispatch"
|
|
205
|
+
| "recoverAutoPausedSelection"
|
|
204
206
|
| "resolveSelectionAdapter"
|
|
205
207
|
| "resolveCopilotSelection"
|
|
206
208
|
| "copilotRefusalResponse"
|
|
@@ -733,22 +735,11 @@ export async function preparePassthroughExchange(
|
|
|
733
735
|
}
|
|
734
736
|
nativeHostState.lease = null;
|
|
735
737
|
};
|
|
736
|
-
/**
|
|
737
|
-
* #4191: a Codex WS exchange pins its content-free stage record on the
|
|
738
|
-
* Response it resolves (markCodexWsStage). Adopting the record here, at
|
|
739
|
-
* the single funnel every physical upstream response passes through,
|
|
740
|
-
* binds it to the attempt that actually served it — including the 502/504
|
|
741
|
-
* pre-response JSON settles that never reach the SSE relay.
|
|
742
|
-
*/
|
|
743
|
-
const adoptCodexWsStage = (response: Response): void => {
|
|
744
|
-
const stage = readCodexWsStage(response);
|
|
745
|
-
if (stage && logCtx.activeAttempt) logCtx.activeAttempt.codexWsStage = stage;
|
|
746
|
-
};
|
|
747
738
|
let upstreamResponse: Response, lastDistributedResponse: Response | undefined;
|
|
748
739
|
const adoptObservedResponse = <T extends Response>(response: T): T => {
|
|
749
740
|
if (!isNonReplayableResponse(response)) lastDistributedResponse = response.status === 401 || response.status === 429 ? response : undefined;
|
|
750
741
|
settleObservedHostResponse();
|
|
751
|
-
adoptCodexWsStage(response);
|
|
742
|
+
adoptCodexWsStage(response, logCtx);
|
|
752
743
|
return response;
|
|
753
744
|
};
|
|
754
745
|
let passthroughEstimate = typeof request.usageLog?.inputTokens === "number"
|
|
@@ -1050,6 +1041,7 @@ export async function preparePassthroughExchange(
|
|
|
1050
1041
|
providerName: route.providerName,
|
|
1051
1042
|
modelId: route.modelId,
|
|
1052
1043
|
onPhysicalDispatch: receiptMode ? sendBudgetState.noteInitialDispatch : undefined,
|
|
1044
|
+
accountAutoPause: (name, provider, headers) => captureAccountAutoPauseAttempt(name, provider, { config, codex: admissionState.authCtx }, headers),
|
|
1053
1045
|
onCodexWsQuota: codexWsQuotaObserver(admissionState.authCtx, route.provider, route.modelId),
|
|
1054
1046
|
beforeDispatch: isCanonicalOpenAiForwardProvider(route.provider)
|
|
1055
1047
|
? createCodexAuthDispatchGuard(admissionState.authCtx, options.codexAuthPolicy ?? config, route.modelId, options.admission, options.visionDescribeTerminal === true) : undefined,
|
|
@@ -1187,6 +1179,7 @@ export async function preparePassthroughExchange(
|
|
|
1187
1179
|
dispatchOverride: oauthDispatch(request, parsed, "passthrough"),
|
|
1188
1180
|
providerName: route.providerName,
|
|
1189
1181
|
modelId: route.modelId,
|
|
1182
|
+
accountAutoPause: (name, provider, headers) => captureAccountAutoPauseAttempt(name, provider, { config, codex: admissionState.authCtx }, headers),
|
|
1190
1183
|
onCodexWsQuota: codexWsQuotaObserver(admissionState.authCtx, route.provider, route.modelId),
|
|
1191
1184
|
beforeDispatch: isCanonicalOpenAiForwardProvider(route.provider)
|
|
1192
1185
|
? createCodexAuthDispatchGuard(admissionState.authCtx, options.codexAuthPolicy ?? config, route.modelId, options.admission, options.visionDescribeTerminal === true) : undefined,
|
|
@@ -1209,8 +1202,18 @@ export async function preparePassthroughExchange(
|
|
|
1209
1202
|
|
|
1210
1203
|
// Keep recovery kinds in sync with the generic `recovery:` loop below.
|
|
1211
1204
|
passthroughRecovery: for (;;) {
|
|
1205
|
+
if (wasAccountAutoPaused(upstreamResponse) && route.provider.authMode !== "forward") {
|
|
1206
|
+
const recovered = await recoverAutoPausedAccount({ requestKey: transportState, reserve: reserveCredentialHop,
|
|
1207
|
+
remaining: transientSendCapFor(transientRetryPolicyFor(route.provider)?.attempts, sendBudgetState.targetSendsUsed),
|
|
1208
|
+
refresh: () => transportState.recoverAutoPausedSelection(parsed), refetch: () => rebuildAndRefetch("oauth-account-429"), setPermit: permit => { sendBudgetState.pendingHopPermit = permit; } });
|
|
1209
|
+
if (!recovered) break;
|
|
1210
|
+
if ("failed" in recovered) return recovered.failed;
|
|
1211
|
+
upstreamResponse = recovered; continue passthroughRecovery;
|
|
1212
|
+
}
|
|
1213
|
+
|
|
1214
|
+
|
|
1212
1215
|
if (
|
|
1213
|
-
upstreamResponse.status === 401
|
|
1216
|
+
upstreamResponse.status === 401 && !wasPermanentAccountRefusal(upstreamResponse)
|
|
1214
1217
|
&& (admissionState.authCtx.kind === "main-pool" || admissionState.authCtx.kind === "pool")
|
|
1215
1218
|
&& usesCodexForwardPoolAuth(admissionState.authCtx, route.provider)
|
|
1216
1219
|
&& codex401ReplayKind === null
|
|
@@ -1293,6 +1296,7 @@ export async function preparePassthroughExchange(
|
|
|
1293
1296
|
dispatchOverride: oauthDispatch(request, parsed, "passthrough"),
|
|
1294
1297
|
providerName: route.providerName,
|
|
1295
1298
|
modelId: route.modelId,
|
|
1299
|
+
accountAutoPause: (name, provider, headers) => captureAccountAutoPauseAttempt(name, provider, { config, codex: admissionState.authCtx }, headers),
|
|
1296
1300
|
onCodexWsQuota: codexWsQuotaObserver(admissionState.authCtx, route.provider, route.modelId),
|
|
1297
1301
|
beforeDispatch: headers => {
|
|
1298
1302
|
replayGuard?.(headers);
|
|
@@ -1336,14 +1340,14 @@ export async function preparePassthroughExchange(
|
|
|
1336
1340
|
continue passthroughRecovery;
|
|
1337
1341
|
}
|
|
1338
1342
|
|
|
1339
|
-
if (codex401ReplayKind !== null && upstreamResponse.status === 401) break;
|
|
1343
|
+
if (codex401ReplayKind !== null && upstreamResponse.status === 401 && !wasPermanentAccountRefusal(upstreamResponse)) break;
|
|
1340
1344
|
|
|
1341
1345
|
// Native Responses providers return before the generic adapter recovery loop below. Keep
|
|
1342
1346
|
// their OAuth contract identical: one pre-stream 401 forces a credential refresh and one
|
|
1343
1347
|
// rebuilt replay. xAI's current subscription models use this branch now that their official
|
|
1344
1348
|
// Grok CLI catalog declares the Responses backend.
|
|
1345
1349
|
if (
|
|
1346
|
-
upstreamResponse.status === 401
|
|
1350
|
+
upstreamResponse.status === 401 && !wasPermanentAccountRefusal(upstreamResponse)
|
|
1347
1351
|
&& isOAuth401ReplayProvider
|
|
1348
1352
|
&& transportState.sentOAuthSnapshot
|
|
1349
1353
|
&& !oauth401ReplayAttempted
|
|
@@ -1440,6 +1444,7 @@ export async function preparePassthroughExchange(
|
|
|
1440
1444
|
dispatchOverride: oauthDispatch(request, parsed, "passthrough"),
|
|
1441
1445
|
providerName: route.providerName,
|
|
1442
1446
|
modelId: route.modelId,
|
|
1447
|
+
accountAutoPause: (name, provider, headers) => captureAccountAutoPauseAttempt(name, provider, { config, codex: admissionState.authCtx }, headers),
|
|
1443
1448
|
onCodexWsQuota: codexWsQuotaObserver(admissionState.authCtx, route.provider, route.modelId),
|
|
1444
1449
|
beforeDispatch: isCanonicalOpenAiForwardProvider(route.provider)
|
|
1445
1450
|
? createCodexAuthDispatchGuard(admissionState.authCtx, options.codexAuthPolicy ?? config, route.modelId, options.admission, options.visionDescribeTerminal === true) : undefined,
|
|
@@ -1586,6 +1591,7 @@ export async function preparePassthroughExchange(
|
|
|
1586
1591
|
dispatchOverride: oauthDispatch(request, parsed, "passthrough"),
|
|
1587
1592
|
providerName: route.providerName,
|
|
1588
1593
|
modelId: route.modelId,
|
|
1594
|
+
accountAutoPause: (name, provider, headers) => captureAccountAutoPauseAttempt(name, provider, { config, codex: admissionState.authCtx }, headers),
|
|
1589
1595
|
onCodexWsQuota: codexWsQuotaObserver(admissionState.authCtx, route.provider, route.modelId),
|
|
1590
1596
|
beforeDispatch: isCanonicalOpenAiForwardProvider(route.provider)
|
|
1591
1597
|
? createCodexAuthDispatchGuard(admissionState.authCtx, options.codexAuthPolicy ?? config, route.modelId, options.admission, options.visionDescribeTerminal === true) : undefined,
|
|
@@ -1608,18 +1614,7 @@ export async function preparePassthroughExchange(
|
|
|
1608
1614
|
|| captureAuthCtx.kind === "pool"
|
|
1609
1615
|
|| captureAuthCtx.kind === "main-pool",
|
|
1610
1616
|
): void => {
|
|
1611
|
-
|
|
1612
|
-
captureCodexAffinityDiagnostic({
|
|
1613
|
-
inboundHeaders: req.headers,
|
|
1614
|
-
outboundHeaders: captureRequest.headers,
|
|
1615
|
-
authKind: captureAuthCtx.kind,
|
|
1616
|
-
accountMode: route.codexAccountMode,
|
|
1617
|
-
fixedAccount: isFixedCodexAccount(captureAuthCtx),
|
|
1618
|
-
credentialSubstituted,
|
|
1619
|
-
accountGatedModel: ACCOUNT_GATED_NATIVE_OPENAI_MODELS.has(route.modelId),
|
|
1620
|
-
wireModelNormalized: parsed.modelId !== route.modelId,
|
|
1621
|
-
status: response.status,
|
|
1622
|
-
});
|
|
1617
|
+
capturePassthroughAffinity(response, req.headers, captureRequest.headers, captureAuthCtx, route, parsed.modelId, credentialSubstituted);
|
|
1623
1618
|
};
|
|
1624
1619
|
recordDistributedResponseFailure(lastDistributedResponse ?? upstreamResponse, config, route.providerName, options.abortSignal);
|
|
1625
1620
|
captureAffinityResponse(upstreamResponse);
|
|
@@ -1646,7 +1641,9 @@ export async function preparePassthroughExchange(
|
|
|
1646
1641
|
options.abortSignal,
|
|
1647
1642
|
parsed.modelId,
|
|
1648
1643
|
);
|
|
1649
|
-
if (
|
|
1644
|
+
if (wasAccountAutoPaused(upstreamResponse)) {
|
|
1645
|
+
poolRetryOutcome = upstreamResponse.status;
|
|
1646
|
+
} else if (model400Denial !== undefined) {
|
|
1650
1647
|
// Spend this refusal on more than one retry. It is the account's own authenticated
|
|
1651
1648
|
// answer about this model, and the roster cache that selection otherwise reads expires
|
|
1652
1649
|
// five minutes after a catalog sync fills it -- so without remembering this, the next
|
|
@@ -1679,7 +1676,7 @@ export async function preparePassthroughExchange(
|
|
|
1679
1676
|
// ladder does have one — retrying the account the refreshed roster still grants — and
|
|
1680
1677
|
// keeps it. An earlier revision also broke here on a non-400 outcome, which no test could
|
|
1681
1678
|
// justify because this flag already produced the identical result.
|
|
1682
|
-
const storedReplaySpent = codex401ReplayKind === "stored";
|
|
1679
|
+
const storedReplaySpent = codex401ReplayKind === "stored" && !wasAccountAutoPaused(upstreamResponse);
|
|
1683
1680
|
const retry = await retryCodexPoolOnAlternateAccount({
|
|
1684
1681
|
callerAuthHeaders,
|
|
1685
1682
|
config,
|
|
@@ -1698,7 +1695,7 @@ export async function preparePassthroughExchange(
|
|
|
1698
1695
|
httpOnly: canonicalBufferedJson,
|
|
1699
1696
|
preOutputRefusalHold: preOutputRefusalHold(),
|
|
1700
1697
|
onResponse: (response, retryAuthCtx, retryRequest) => {
|
|
1701
|
-
adoptCodexWsStage(response);
|
|
1698
|
+
adoptCodexWsStage(response, logCtx);
|
|
1702
1699
|
captureAffinityResponse(
|
|
1703
1700
|
response,
|
|
1704
1701
|
retryAuthCtx,
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { readCodexWsStage } from "./codex-ws-wire";
|
|
2
|
+
import type { RequestLogContext } from "../request-log";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* #4191: a Codex WS exchange pins its content-free stage record on the
|
|
6
|
+
* Response it resolves (markCodexWsStage). Adopting the record here, at
|
|
7
|
+
* the single funnel every physical upstream response passes through,
|
|
8
|
+
* binds it to the attempt that actually served it — including the 502/504
|
|
9
|
+
* pre-response JSON settles that never reach the SSE relay.
|
|
10
|
+
*/
|
|
11
|
+
export function adoptCodexWsStage(response: Response, logCtx: RequestLogContext): void {
|
|
12
|
+
const stage = readCodexWsStage(response);
|
|
13
|
+
if (stage && logCtx.activeAttempt) logCtx.activeAttempt.codexWsStage = stage;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
import type { CodexAuthContext } from "../../codex/auth-context";
|
|
17
|
+
import type { RouteResult } from "../../router";
|
|
18
|
+
import { captureCodexAffinityDiagnostic } from "../../codex/affinity-debug";
|
|
19
|
+
import { ACCOUNT_GATED_NATIVE_OPENAI_MODELS } from "../../codex/catalog/native-models";
|
|
20
|
+
import { isCanonicalOpenAiForwardProvider } from "../../providers/openai-tiers";
|
|
21
|
+
import { isFixedCodexAccount } from "./core-codex-account";
|
|
22
|
+
|
|
23
|
+
export function capturePassthroughAffinity(response: Response, inboundHeaders: Headers, outboundHeaders: Record<string, string>,
|
|
24
|
+
captureAuthCtx: CodexAuthContext, route: Pick<RouteResult, "provider" | "modelId" | "codexAccountMode">,
|
|
25
|
+
parsedModelId: string, credentialSubstituted: boolean): void {
|
|
26
|
+
if (!isCanonicalOpenAiForwardProvider(route.provider)) return;
|
|
27
|
+
captureCodexAffinityDiagnostic({
|
|
28
|
+
inboundHeaders: inboundHeaders,
|
|
29
|
+
outboundHeaders: outboundHeaders,
|
|
30
|
+
authKind: captureAuthCtx.kind,
|
|
31
|
+
accountMode: route.codexAccountMode,
|
|
32
|
+
fixedAccount: isFixedCodexAccount(captureAuthCtx),
|
|
33
|
+
credentialSubstituted,
|
|
34
|
+
accountGatedModel: ACCOUNT_GATED_NATIVE_OPENAI_MODELS.has(route.modelId),
|
|
35
|
+
wireModelNormalized: parsedModelId !== route.modelId,
|
|
36
|
+
status: response.status,
|
|
37
|
+
});
|
|
38
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { Buffer } from "node:buffer";
|
|
2
|
+
import { constants, gzipSync, zstdCompressSync } from "node:zlib";
|
|
3
|
+
import type { OcxConfig } from "../../types";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Opt-in compression of outbound native ChatGPT Responses HTTP request bodies (#7247).
|
|
7
|
+
*
|
|
8
|
+
* Codex re-sends the whole conversation on every turn. When that turn travels over HTTP the body
|
|
9
|
+
* reaches `chatgpt.com` identity-encoded, which on a metered egress is the dominant cost of
|
|
10
|
+
* running Codex through the proxy. The backend already accepts coded request bodies: Codex CLI
|
|
11
|
+
* itself sends `content-encoding: zstd` there (see `src/server/request-decompress.ts`).
|
|
12
|
+
*
|
|
13
|
+
* Applied only at the final HTTP send (`sendWithConnectionPolicy`), after the WebSocket decision
|
|
14
|
+
* and plugin rewrites, so WebSocket frames and every other destination are untouched.
|
|
15
|
+
*
|
|
16
|
+
* Compression is synchronous on purpose. An await at the physical-send boundary would open a gap
|
|
17
|
+
* between dispatch admission (credential selection, attempt receipts, live reserve policy) and the
|
|
18
|
+
* send itself. The cost is bounded instead: bodies above {@link MAX_COMPRESSIBLE_BODY_BYTES} are sent
|
|
19
|
+
* as they are, which keeps the event-loop block to tens of milliseconds on ordinary hardware.
|
|
20
|
+
*/
|
|
21
|
+
export type UpstreamRequestCompression = "off" | "gzip" | "zstd";
|
|
22
|
+
|
|
23
|
+
/** Below this the header overhead outweighs any saving. */
|
|
24
|
+
export const MIN_COMPRESSIBLE_BODY_BYTES = 1024;
|
|
25
|
+
/** Above this the synchronous compression block is no longer negligible; send identity. */
|
|
26
|
+
export const MAX_COMPRESSIBLE_BODY_BYTES = 16 * 1024 * 1024;
|
|
27
|
+
|
|
28
|
+
const CANONICAL_CODEX_RESPONSES = /^https:\/\/chatgpt\.com\/backend-api\/codex\/responses(?:\/compact)?$/;
|
|
29
|
+
|
|
30
|
+
/** True for the native ChatGPT Responses and compact endpoints, the only compressed destinations. */
|
|
31
|
+
export function isCanonicalCodexResponsesUrl(url: string): boolean {
|
|
32
|
+
return CANONICAL_CODEX_RESPONSES.test(url);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Anything other than an explicit codec, including a malformed hand edit, means off. */
|
|
36
|
+
export function resolveUpstreamRequestCompression(value: unknown): UpstreamRequestCompression {
|
|
37
|
+
return value === "gzip" || value === "zstd" ? value : "off";
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// The live server config object, installed once at startup by `setLiveStateStoreConfig`. Live
|
|
41
|
+
// reconciliation mutates that object in place, so the field is read per send rather than copied.
|
|
42
|
+
let boundConfig: Pick<OcxConfig, "upstreamRequestCompression"> | null = null;
|
|
43
|
+
|
|
44
|
+
export function bindUpstreamRequestCompressionConfig(config: Pick<OcxConfig, "upstreamRequestCompression"> | null): void {
|
|
45
|
+
boundConfig = config;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function currentUpstreamRequestCompression(): UpstreamRequestCompression {
|
|
49
|
+
return resolveUpstreamRequestCompression(boundConfig?.upstreamRequestCompression);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// ArrayBuffer-backed, which is what node:zlib returns and what a fetch BodyInit accepts.
|
|
53
|
+
type CodedBytes = Buffer<ArrayBuffer>;
|
|
54
|
+
type Compressor = (mode: Exclude<UpstreamRequestCompression, "off">, bytes: Buffer) => CodedBytes;
|
|
55
|
+
|
|
56
|
+
const defaultCompressor: Compressor = (mode, bytes) => mode === "gzip"
|
|
57
|
+
? gzipSync(bytes, { level: 1 }) as CodedBytes
|
|
58
|
+
: zstdCompressSync(bytes, { params: { [constants.ZSTD_c_compressionLevel]: 3 } }) as CodedBytes;
|
|
59
|
+
|
|
60
|
+
let compressor: Compressor = defaultCompressor;
|
|
61
|
+
let maxBodyBytes = MAX_COMPRESSIBLE_BODY_BYTES;
|
|
62
|
+
|
|
63
|
+
/** Test seam: replace the codec (failure injection) and the size ceiling; null restores defaults. */
|
|
64
|
+
export function setUpstreamRequestCompressionForTests(next: { compressor?: Compressor | null; maxBodyBytes?: number | null }): void {
|
|
65
|
+
if (next.compressor !== undefined) compressor = next.compressor ?? defaultCompressor;
|
|
66
|
+
if (next.maxBodyBytes !== undefined) maxBodyBytes = next.maxBodyBytes ?? MAX_COMPRESSIBLE_BODY_BYTES;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export interface CompressedUpstreamBody {
|
|
70
|
+
body: CodedBytes;
|
|
71
|
+
encoding: Exclude<UpstreamRequestCompression, "off">;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Compress one final HTTP send body when the operator opted in and the send is eligible.
|
|
76
|
+
*
|
|
77
|
+
* Returns null — send unchanged — for any other destination, a non-string body (an already coded
|
|
78
|
+
* buffer or a stream, which also makes nested passes idempotent), a request that already declares
|
|
79
|
+
* a `content-encoding`, a body outside the size window, or a codec failure. A failure never fails
|
|
80
|
+
* the request: identity is what the proxy sent before this option existed.
|
|
81
|
+
*/
|
|
82
|
+
export function compressUpstreamRequestBody(
|
|
83
|
+
target: string,
|
|
84
|
+
body: unknown,
|
|
85
|
+
headers: Headers,
|
|
86
|
+
mode: UpstreamRequestCompression = currentUpstreamRequestCompression(),
|
|
87
|
+
): CompressedUpstreamBody | null {
|
|
88
|
+
if (mode === "off" || typeof body !== "string" || !isCanonicalCodexResponsesUrl(target)) return null;
|
|
89
|
+
if (headers.has("content-encoding")) return null;
|
|
90
|
+
const size = Buffer.byteLength(body, "utf8");
|
|
91
|
+
if (size < MIN_COMPRESSIBLE_BODY_BYTES || size > maxBodyBytes) return null;
|
|
92
|
+
try {
|
|
93
|
+
return { body: compressor(mode, Buffer.from(body, "utf8")), encoding: mode };
|
|
94
|
+
} catch {
|
|
95
|
+
// Deliberately silent: the error could only describe the body, and identity is still correct.
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
@@ -71,7 +71,7 @@ import {
|
|
|
71
71
|
resolveCurrentProviderApiKeyTransport,
|
|
72
72
|
} from "../../providers/api-key-selection";
|
|
73
73
|
import { resolveAdapter, resolveWireProtocolOverride } from "../adapter-resolve";
|
|
74
|
-
import { providerFetch, sendWithConnectionPolicy } from "./fetch-helpers";
|
|
74
|
+
import { physicalSendObserverInit, providerFetch, sendWithConnectionPolicy } from "./fetch-helpers";
|
|
75
75
|
import type { ProviderFetchOptions } from "./fetch-helpers";
|
|
76
76
|
import { captureConfigGeneration } from "../../lib/state-store-sweeper";
|
|
77
77
|
import { recordAnthropicAccountQuotaFromHeadersForInstance, hasPassiveAccountQuota } from "../../providers/quota";
|
|
@@ -80,6 +80,7 @@ import { formatErrorResponse } from "../../bridge";
|
|
|
80
80
|
import { bindRouteReasoningReplayScope } from "./core-replay";
|
|
81
81
|
import { sessionIdHeaderFromRequest, normalizeLogConversationId } from "../request-log-conversation";
|
|
82
82
|
import { redactSecretString } from "../../lib/redact";
|
|
83
|
+
import { captureAccountAutoPauseAttempt } from "../../oauth/account-auto-pause";
|
|
83
84
|
import { selectProactiveApiKeyTransport } from "../../providers/key-failover";
|
|
84
85
|
import { distributedKeyPhysicalFetch } from "../../providers/api-key-response-ownership";
|
|
85
86
|
import { isCanonicalOpenAiForwardProvider } from "../../providers/openai-tiers";
|
|
@@ -612,6 +613,10 @@ export async function prepareResponsesTransport(
|
|
|
612
613
|
// Pacing may outlive admission. Preserve local pause/cooldown reasons even when
|
|
613
614
|
// a strict route rejects the old proposal before another bearer can be resolved.
|
|
614
615
|
let candidate = servingOAuthSnapshot;
|
|
616
|
+
if (anthropicInstance === undefined && candidate && getAccountSet(route.providerName)?.accounts.find(row => row.id === candidate!.accountId)?.paused) {
|
|
617
|
+
oauthSelection = captureOAuthAccountSelection(route.providerName);
|
|
618
|
+
candidate = oauthSelection ? await getValidAccessSnapshotForAccount(route.providerName, oauthSelection.accountId, { requireUsableAccount: true }) : undefined;
|
|
619
|
+
}
|
|
615
620
|
if (anthropicInstance !== undefined) {
|
|
616
621
|
oauthSelection = captureOAuthAccountSelection(route.providerName);
|
|
617
622
|
const accountId = await anthropicRouting!.resolveAnthropicDispatchAccountId(config, anthropicSessionKey, anthropicRouteDecision, route.modelId);
|
|
@@ -644,6 +649,24 @@ export async function prepareResponsesTransport(
|
|
|
644
649
|
invalidateSameTargetRequest();
|
|
645
650
|
return adapter;
|
|
646
651
|
};
|
|
652
|
+
const recoverAutoPausedSelection = async (requestParsed: OcxParsedRequest): Promise<boolean> => {
|
|
653
|
+
if (route.provider.authMode === "forward") return false;
|
|
654
|
+
const available = route.provider.authMode === "oauth"
|
|
655
|
+
? getAccountSet(route.providerName)?.accounts.some(row => !row.paused && !row.needsReauth)
|
|
656
|
+
: !!config.providers[route.providerName]?.apiKey;
|
|
657
|
+
if (!available) return false;
|
|
658
|
+
await refreshDispatchAdapter(requestParsed);
|
|
659
|
+
// Bind before the recovery caller rebuilds. A terminal retry can be a clone;
|
|
660
|
+
// the outer request also owns continuation persistence and replay publication.
|
|
661
|
+
for (const replayParsed of new Set([parsed, requestParsed])) {
|
|
662
|
+
bindRouteReasoningReplayScope({
|
|
663
|
+
parsed: replayParsed, providerName: route.providerName, provider: route.provider,
|
|
664
|
+
adapterName: activeAdapter.name, oauthCredentialSnapshot: replayOAuthCredentialSnapshot,
|
|
665
|
+
forwardHeaders: requestState.selectedForwardHeaders,
|
|
666
|
+
});
|
|
667
|
+
}
|
|
668
|
+
return true;
|
|
669
|
+
};
|
|
647
670
|
const refreshRunTurnAdapter = async (requestParsed: OcxParsedRequest): Promise<ProviderAdapter> => {
|
|
648
671
|
requestParsed._cursorIdentityScope = undefined;
|
|
649
672
|
requestParsed._cursorConversationId = undefined;
|
|
@@ -704,6 +727,7 @@ export async function prepareResponsesTransport(
|
|
|
704
727
|
providerName: route.providerName, modelId: route.modelId, pacingSlotAcquired: true,
|
|
705
728
|
pacingSlot: attemptSlot,
|
|
706
729
|
turnScopedPacing: true,
|
|
730
|
+
accountAutoPause: (name, provider, headers) => captureAccountAutoPauseAttempt(name, provider, { config, oauth: binding?.kind === "oauth" ? binding.snapshot : undefined }, headers),
|
|
707
731
|
beforeDispatch: beforePhysicalSend,
|
|
708
732
|
});
|
|
709
733
|
await run(requestParsed, { ...incoming, pacingSlot: attemptSlot, providerFetch: fetch, beforePhysicalSend }, event => { if (!refused) emit(event); });
|
|
@@ -756,6 +780,8 @@ export async function prepareResponsesTransport(
|
|
|
756
780
|
// apply a host-scoped decision to a different host.
|
|
757
781
|
const releaseFamily = snapshot ? anthropicQuota!.claimAnthropicFamilyRevalidation(snapshot.accountId, route.modelId) : () => {};
|
|
758
782
|
if (!releaseFamily) throw new AnthropicAccountCooldownError(1);
|
|
783
|
+
const observeAutoPause = captureAccountAutoPauseAttempt(route.providerName, binding?.kind === "api-key" ? binding.provider : route.provider,
|
|
784
|
+
{ config, oauth: binding?.kind === "oauth" ? binding.snapshot : undefined }, new Headers(dispatchInit.headers));
|
|
759
785
|
let response: Response;
|
|
760
786
|
let physicalOwner: AnthropicPhysicalSendOwnership | null = null;
|
|
761
787
|
try {
|
|
@@ -775,7 +801,8 @@ export async function prepareResponsesTransport(
|
|
|
775
801
|
response = await sendWithConnectionPolicy(
|
|
776
802
|
distributedKeyPhysicalFetch(anthropicBillingPhysicalFetch(fetchImpl, route.providerName, route.provider), route.providerName, route.provider, target),
|
|
777
803
|
destination,
|
|
778
|
-
{ ...dispatchInit, redirect: "manual"
|
|
804
|
+
{ ...dispatchInit, redirect: "manual",
|
|
805
|
+
...physicalSendObserverInit(observeAutoPause, destination instanceof Request ? destination.url : String(destination)) },
|
|
779
806
|
{ providerName: route.providerName, provider: route.provider },
|
|
780
807
|
);
|
|
781
808
|
} finally { releaseFamily(); }
|
|
@@ -790,11 +817,12 @@ export async function prepareResponsesTransport(
|
|
|
790
817
|
&& current.credential.access === snapshot.accessToken
|
|
791
818
|
&& credentialGeneration(current.credential) === snapshot.generation) {
|
|
792
819
|
bindAnthropicRefusalCredentialForSend(response, physicalOwner);
|
|
793
|
-
recordAnthropicAccountQuotaFromHeadersForInstance(anthropicInstance!, snapshot.accountId, response.headers, writerGeneration, response.status, route.modelId);
|
|
820
|
+
recordAnthropicAccountQuotaFromHeadersForInstance(anthropicInstance!, snapshot.accountId, response.headers, writerGeneration, response.status, route.modelId, snapshot.generation);
|
|
794
821
|
}
|
|
795
822
|
} catch { /* best-effort observation cannot fail the response */ }
|
|
796
823
|
}
|
|
797
824
|
if (!response.ok) await recordKeyAttemptFailure(logCtx, response, dispatchInit.signal ?? options.abortSignal);
|
|
825
|
+
if (observeAutoPause) response = await observeAutoPause(response);
|
|
798
826
|
return response;
|
|
799
827
|
}
|
|
800
828
|
let nextAdapter: ProviderAdapter;
|
|
@@ -1280,6 +1308,7 @@ export async function prepareResponsesTransport(
|
|
|
1280
1308
|
oauthDispatch,
|
|
1281
1309
|
noteRoutedAttemptSend,
|
|
1282
1310
|
commitKeyAttemptSend,
|
|
1311
|
+
recoverAutoPausedSelection,
|
|
1283
1312
|
bindKeyUsageFromBridge,
|
|
1284
1313
|
anthropicSessionKey,
|
|
1285
1314
|
isPassthrough,
|
|
@@ -53,6 +53,7 @@ import { emptyToolCatalogRefusal, guardEmptyToolCatalogEvents, guardEmptyToolCat
|
|
|
53
53
|
// LOCAL PATCH (runturn-websearch)
|
|
54
54
|
import { planWebSearch } from "../../web-search";
|
|
55
55
|
import { runTurnWebSearchInitialParsed, runTurnWebSearchLoop } from "../../web-search/run-turn-loop";
|
|
56
|
+
import { stripReplayedSearchPairs } from "../../web-search/sidecar-search-replay";
|
|
56
57
|
import { WEB_SEARCH_TOOL_NAME } from "../../web-search/synthetic-tool";
|
|
57
58
|
import { orderDevinMessagesOutput } from "../../claude/devin-output-order";
|
|
58
59
|
import { unboundPoolSpendRefusalMessage } from "../workflow-refusal";
|
|
@@ -217,6 +218,7 @@ export async function executeResponsesRunTurn(
|
|
|
217
218
|
releaseSearchProbeLease();
|
|
218
219
|
throw error;
|
|
219
220
|
}
|
|
221
|
+
if (!wsPlan) stripReplayedSearchPairs(parsed);
|
|
220
222
|
|
|
221
223
|
const runTurnAbort = new AbortController();
|
|
222
224
|
const cleanupRequestAbort = linkAbortSignal(runTurnAbort, req.signal);
|
|
@@ -39,6 +39,7 @@ import { AdmissionModelDeniedError, admissionModelDeniedResponse } from "../admi
|
|
|
39
39
|
import { noteKiroMonthlyRefusal, noteKiroServedSuccess } from "../../providers/kiro-usage";
|
|
40
40
|
import { persistKiroAccountState } from "../../providers/kiro-account-state-disk";
|
|
41
41
|
import { anthropicPoolCooldownResponse, readDisplaySafeErrorText } from "./core-errors";
|
|
42
|
+
import { sidecarContextOverflowResponse } from "./context-overflow";
|
|
42
43
|
import {
|
|
43
44
|
ANTHROPIC_POOL_MAX_FAILOVERS_PER_REQUEST,
|
|
44
45
|
anthropicRoutingFor,
|
|
@@ -56,6 +57,7 @@ import { rememberResponseState } from "../../responses/state";
|
|
|
56
57
|
import { trackStreamLifetime } from "../lifecycle";
|
|
57
58
|
import { isAntigravityRawValidationRefusal } from "./antigravity-validation-refusal";
|
|
58
59
|
import { createSidecarSendBudget } from "./sidecar-send-budget";
|
|
60
|
+
import { stripReplayedSearchPairs } from "../../web-search/sidecar-search-replay";
|
|
59
61
|
|
|
60
62
|
/** One responsibility of the Responses request pipeline; state owners are explicit. */
|
|
61
63
|
export async function executeResponsesSidecars(
|
|
@@ -216,6 +218,8 @@ export async function executeResponsesSidecars(
|
|
|
216
218
|
})
|
|
217
219
|
: undefined;
|
|
218
220
|
const imgPlan = !routedCompaction ? await planImageBridge(config, parsed, route.provider) : undefined;
|
|
221
|
+
// Replayed search pairs call the synthetic web_search tool only the sidecar declares (#7269).
|
|
222
|
+
if (!wsPlan) stripReplayedSearchPairs(parsed);
|
|
219
223
|
const vidPlan = !routedCompaction ? await planVideoBridge(config, parsed, route.provider) : undefined;
|
|
220
224
|
const canRunWebSearch = !!wsPlan && !transportState.adapter.runTurn;
|
|
221
225
|
let sidecarOutputStarted = false;
|
|
@@ -548,7 +552,7 @@ export async function executeResponsesSidecars(
|
|
|
548
552
|
options.codexWsRuntimeIdentity,
|
|
549
553
|
{ providerName: route.providerName, modelId: route.modelId, ...sidecarBudget.fetchOptions(transportState.adapter) },
|
|
550
554
|
);
|
|
551
|
-
const
|
|
555
|
+
const imgLoopResponse = await runWithImageBridge({
|
|
552
556
|
parsed, adapter: transportState.adapter,
|
|
553
557
|
...(transportState.adapter.name === "anthropic" ? { thinkingRecovery } : {}),
|
|
554
558
|
clientToolCatalogRestriction: route.provider.authMode === "forward" ? undefined : requestState.clientToolCatalogRestriction,
|
|
@@ -619,8 +623,12 @@ export async function executeResponsesSidecars(
|
|
|
619
623
|
notifyResponseComplete(response);
|
|
620
624
|
},
|
|
621
625
|
});
|
|
622
|
-
const imgScopeRefusal = sidecarScopeRefusal(
|
|
626
|
+
const imgScopeRefusal = sidecarScopeRefusal(imgLoopResponse);
|
|
623
627
|
if (imgScopeRefusal) return imgScopeRefusal;
|
|
628
|
+
// A provider overflow on the first iteration answers as the main path's context failure (#7316).
|
|
629
|
+
const imgResponse = options.comboAttempt ? imgLoopResponse : sidecarContextOverflowResponse(imgLoopResponse, {
|
|
630
|
+
stream: parsed.stream, modelId: parsed._responseModelId ?? parsed.modelId, translatorBudget,
|
|
631
|
+
});
|
|
624
632
|
if (imgResponse.body) {
|
|
625
633
|
const imgTurnAc = new AbortController();
|
|
626
634
|
imgTurnAc.signal.addEventListener("abort", cancelResponseCompletion, { once: true });
|
|
@@ -732,7 +740,12 @@ export async function executeResponsesSidecars(
|
|
|
732
740
|
? anthropicRoutingFor(anthropicInstance).anthropicPoolUsageLimitAfterRefusal(Date.now(), transportState.anthropicRouteDecision, route.modelId)
|
|
733
741
|
: null;
|
|
734
742
|
if (poolUsageLimit) void loopResponse.body?.cancel().catch(() => {});
|
|
735
|
-
|
|
743
|
+
// A provider overflow on the first iteration answers as the main path's context failure (#7316).
|
|
744
|
+
const wsResponse = poolUsageLimit ? anthropicPoolCooldownResponse(poolUsageLimit, { logCtx })
|
|
745
|
+
: options.comboAttempt ? loopResponse
|
|
746
|
+
: sidecarContextOverflowResponse(loopResponse, {
|
|
747
|
+
stream: parsed.stream, modelId: parsed._responseModelId ?? parsed.modelId, translatorBudget,
|
|
748
|
+
});
|
|
736
749
|
// Register the sidecar stream as an active turn so drainAndShutdown waits for (or aborts)
|
|
737
750
|
// in-flight web-search turns instead of skipping them during graceful shutdown.
|
|
738
751
|
if (wsResponse.body) {
|
|
@@ -170,6 +170,7 @@ export function mergeUsage(first: OcxUsage | undefined, second: OcxUsage | undef
|
|
|
170
170
|
const cachedInputTokens = sumOptional("cachedInputTokens");
|
|
171
171
|
const cacheReadInputTokens = sumOptional("cacheReadInputTokens");
|
|
172
172
|
const cacheCreationInputTokens = sumOptional("cacheCreationInputTokens");
|
|
173
|
+
const cacheCreation1hInputTokens = sumOptional("cacheCreation1hInputTokens");
|
|
173
174
|
const reasoningOutputTokens = sumOptional("reasoningOutputTokens");
|
|
174
175
|
const providerCredits = sumOptional("providerCredits");
|
|
175
176
|
const inputTokens = first.inputTokens + second.inputTokens;
|
|
@@ -181,6 +182,7 @@ export function mergeUsage(first: OcxUsage | undefined, second: OcxUsage | undef
|
|
|
181
182
|
...(cachedInputTokens !== undefined ? { cachedInputTokens } : {}),
|
|
182
183
|
...(cacheReadInputTokens !== undefined ? { cacheReadInputTokens } : {}),
|
|
183
184
|
...(cacheCreationInputTokens !== undefined ? { cacheCreationInputTokens } : {}),
|
|
185
|
+
...(cacheCreation1hInputTokens !== undefined ? { cacheCreation1hInputTokens } : {}),
|
|
184
186
|
...(reasoningOutputTokens !== undefined ? { reasoningOutputTokens } : {}),
|
|
185
187
|
...(providerCredits !== undefined ? { providerCredits } : {}),
|
|
186
188
|
...(first.estimated || second.estimated ? { estimated: true } : {}),
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
2
|
import { execFile } from "node:child_process";
|
|
3
3
|
import { join } from "node:path";
|
|
4
|
-
import { durableBunPath } from "../lib/bun-runtime";
|
|
4
|
+
import { durableBunChildEnv, durableBunPath } from "../lib/bun-runtime";
|
|
5
5
|
import { selfLaunchArgv } from "../lib/self-launch-argv";
|
|
6
6
|
import { isDesktopSupervised } from "../lib/system-restart-contract";
|
|
7
7
|
import { withLoginShellPath } from "./login-shell-path";
|
|
@@ -196,7 +196,7 @@ async function runCliInstall(
|
|
|
196
196
|
isStandaloneExecutable: installLaunch.isStandaloneExecutable,
|
|
197
197
|
});
|
|
198
198
|
const run = installLaunch.execFile ?? execFile;
|
|
199
|
-
const env = await installChildEnv(action);
|
|
199
|
+
const env = durableBunChildEnv(await installChildEnv(action));
|
|
200
200
|
return new Promise((resolve, reject) => {
|
|
201
201
|
const timeout = process.platform === "win32" && action === "install-service"
|
|
202
202
|
? 0
|
|
@@ -4,7 +4,7 @@ import { codexAutoStartEnabled } from "../config";
|
|
|
4
4
|
import { deriveStartupHealth, startupHealthProbeBudgetMs, type StartupHealth } from "../codex/autostart-health";
|
|
5
5
|
import { getCodexRoutingKind } from "../codex/inject";
|
|
6
6
|
import { diagnoseCodexShim } from "../codex/shim";
|
|
7
|
-
import { durableBunPath } from "../lib/bun-runtime";
|
|
7
|
+
import { durableBunChildEnv, durableBunPath } from "../lib/bun-runtime";
|
|
8
8
|
import { selfLaunchArgv } from "../lib/self-launch-argv";
|
|
9
9
|
import { desktopStartupOwnership } from "../service/desktop-startup";
|
|
10
10
|
import type { OcxConfig } from "../types";
|
|
@@ -129,7 +129,7 @@ function runProbe(config: Pick<OcxConfig, "codexAutoStart">): Promise<StartupHea
|
|
|
129
129
|
return new Promise(resolve => {
|
|
130
130
|
execFile(bun, selfLaunchArgv(["__startup-health"], { sourceEntrypoint: cli }), {
|
|
131
131
|
encoding: "utf8",
|
|
132
|
-
env: process.env,
|
|
132
|
+
env: durableBunChildEnv(process.env),
|
|
133
133
|
timeout: PROBE_TIMEOUT_MS,
|
|
134
134
|
windowsHide: true,
|
|
135
135
|
maxBuffer: 256 * 1024,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { execFile } from "node:child_process";
|
|
2
2
|
import { join } from "node:path";
|
|
3
|
-
import { durableBunPath } from "../lib/bun-runtime";
|
|
3
|
+
import { durableBunChildEnv, durableBunPath } from "../lib/bun-runtime";
|
|
4
4
|
import type { WindowsTrayStatus } from "../tray/windows";
|
|
5
5
|
|
|
6
6
|
export type WindowsTrayAction = "install" | "start" | "stop" | "status" | "uninstall";
|
|
@@ -15,7 +15,7 @@ export function runWindowsTrayAction(action: WindowsTrayAction): Promise<Windows
|
|
|
15
15
|
return new Promise((resolve, reject) => {
|
|
16
16
|
execFile(bun, [cli, "tray", action, "--json"], {
|
|
17
17
|
encoding: "utf8",
|
|
18
|
-
env: process.env,
|
|
18
|
+
env: durableBunChildEnv(process.env),
|
|
19
19
|
timeout: 30_000,
|
|
20
20
|
windowsHide: true,
|
|
21
21
|
maxBuffer: 256 * 1024,
|
|
@@ -90,8 +90,16 @@ export function parseBakedListenPort(read: () => string): number | null {
|
|
|
90
90
|
export function resolvedProxyEnv(env: NodeJS.ProcessEnv = process.env): { name: string; value: string }[] {
|
|
91
91
|
const resolved: { name: string; value: string }[] = [];
|
|
92
92
|
for (const key of PROXY_ENV_KEYS) {
|
|
93
|
-
const value = env[key]?.trim()
|
|
93
|
+
const value = [env[key]?.trim(), env[key.toLowerCase()]?.trim()].find(v => v && !OWN_CLAUDE_INTERCEPT_PROXY.test(v));
|
|
94
94
|
if (value) resolved.push({ name: key, value });
|
|
95
95
|
}
|
|
96
96
|
return resolved;
|
|
97
97
|
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* The URL Claude first-party writes into `~/.claude/settings.json` (`claudeInterceptProxyUrl`),
|
|
101
|
+
* which every process Claude Code starts inherits. Baked into the service, it would send the
|
|
102
|
+
* proxy's own outbound traffic back through its intercept listener. Only the token-bearing form
|
|
103
|
+
* is matched: a plain `http://127.0.0.1:<port>` is a user's local proxy client and is kept.
|
|
104
|
+
*/
|
|
105
|
+
const OWN_CLAUDE_INTERCEPT_PROXY = /^http:\/\/opencodex:[^@/]+@127\.0\.0\.1:\d{1,5}\/?$/i;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export const ACCOUNT_PAUSE_REASONS = [
|
|
2
|
+
"oauth_organization_disabled", "account_permission_denied", "subscription_inactive",
|
|
3
|
+
"subscription_expired", "plan_downgraded",
|
|
4
|
+
] as const;
|
|
5
|
+
export type AccountPauseReason = typeof ACCOUNT_PAUSE_REASONS[number];
|
|
6
|
+
export interface AccountPauseDetails {
|
|
7
|
+
reason: AccountPauseReason;
|
|
8
|
+
pausedAt: number;
|
|
9
|
+
}
|
|
10
|
+
export interface AccountAutoPausePolicy {
|
|
11
|
+
/** Explicit opt-in. Omitted and false both leave account eligibility unchanged. */
|
|
12
|
+
enabled?: boolean;
|
|
13
|
+
/** Consecutive permanent refusals; defaults to 2. */
|
|
14
|
+
consecutiveFailures?: number;
|
|
15
|
+
}
|