@bitkyc08/opencodex 2.76.0 → 2.78.0-preview.20261005
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/bin/ocx.mjs +11 -10
- package/gui/dist/assets/App-CppMz-6w.js +54 -0
- package/gui/dist/assets/App-lCIslZ1L.css +1 -0
- package/gui/dist/assets/{Tray-DjNwwrWS.js → Tray-CJWQqtVv.js} +1 -1
- package/gui/dist/assets/index-DQt9srNi.js +94 -0
- package/gui/dist/assets/index-DmQB8D_R.css +1 -0
- package/gui/dist/assets/quota-summary-BMsyoZH2.js +1 -0
- package/gui/dist/index.html +4 -3
- package/package.json +1 -1
- package/src/adapters/anthropic/beta-allowlist.ts +19 -2
- package/src/adapters/anthropic/client-identity.ts +13 -4
- package/src/adapters/anthropic/native-client-preamble.ts +31 -0
- package/src/adapters/anthropic/passthrough.ts +103 -25
- package/src/adapters/antigravity-refusal.ts +28 -0
- package/src/adapters/cursor/protobuf-request.ts +6 -0
- package/src/adapters/cursor/request-builder.ts +3 -0
- package/src/adapters/cursor/structured-output.ts +13 -0
- package/src/adapters/cursor/types.ts +2 -0
- package/src/adapters/devin/cloud-direct/chat.ts +7 -2
- package/src/adapters/devin/trajectory.ts +49 -0
- package/src/adapters/devin.ts +13 -0
- package/src/adapters/ollama-native.ts +62 -19
- package/src/adapters/openai-responses/passthrough.ts +133 -103
- package/src/adapters/openai-responses/request-strips.ts +10 -4
- package/src/adapters/zed.ts +26 -4
- package/src/bridge/response-json.ts +14 -14
- package/src/bridge/sse.ts +15 -5
- package/src/chatgpt/app-server-shim/bundle-trust.ts +40 -10
- package/src/chatgpt/app-server-shim/gate-rewrite.ts +3 -0
- package/src/claude/agents-inject.ts +4 -43
- package/src/claude/gateway-cache.ts +16 -13
- package/src/claude/inbound-model-options.ts +9 -2
- package/src/claude/intercept/cli-catalog.ts +27 -16
- package/src/claude/intercept/client-hello.ts +103 -0
- package/src/claude/intercept/model-bindings.ts +3 -2
- package/src/claude/intercept/picker-bootstrap.ts +37 -16
- package/src/claude/intercept/picker-budget.ts +41 -0
- package/src/claude/intercept/picker-listener.ts +279 -66
- package/src/claude/outbound.ts +2 -0
- package/src/claude/subagent-force-status.ts +53 -0
- package/src/claude/subagent-model.ts +93 -0
- package/src/cli/access-audio-input.ts +84 -0
- package/src/cli/access-audio-live.ts +99 -0
- package/src/cli/access-audio.ts +123 -0
- package/src/cli/access-data-client.ts +127 -0
- package/src/cli/access-data-plane.ts +104 -0
- package/src/cli/access-data-response.ts +76 -0
- package/src/cli/access.ts +65 -2
- package/src/cli/account-api.ts +33 -6
- package/src/cli/account-auth.ts +111 -42
- package/src/cli/account-key-quota.ts +118 -0
- package/src/cli/account-main.ts +11 -4
- package/src/cli/account-next-actions.ts +63 -0
- package/src/cli/account-policy-dto.ts +68 -0
- package/src/cli/account-policy.ts +167 -0
- package/src/cli/account-target.ts +8 -0
- package/src/cli/account.ts +37 -9
- package/src/cli/agent-runtime-settings.ts +130 -0
- package/src/cli/agent-settings.ts +101 -0
- package/src/cli/agent.ts +29 -2
- package/src/cli/alias.ts +7 -2
- package/src/cli/aside-profiles.ts +2 -2
- package/src/cli/capabilities-access-remote.ts +147 -0
- package/src/cli/capabilities-accounts.ts +195 -0
- package/src/cli/capabilities-agents-routing.ts +414 -0
- package/src/cli/capabilities-base.ts +783 -0
- package/src/cli/capabilities-command.ts +1 -0
- package/src/cli/capabilities-integrations.ts +264 -0
- package/src/cli/capabilities-lab.ts +214 -0
- package/src/cli/capabilities-observe-system.ts +662 -0
- package/src/cli/capabilities-provider-models.ts +302 -0
- package/src/cli/capabilities.ts +30 -1190
- package/src/cli/capability-types.ts +54 -0
- package/src/cli/catalog-command-result.ts +57 -0
- package/src/cli/chatgpt-command.ts +35 -9
- package/src/cli/claude-agent-startup-sync.ts +17 -0
- package/src/cli/claude-desktop-profile.ts +100 -0
- package/src/cli/claude-desktop.ts +39 -6
- package/src/cli/claude.ts +22 -6
- package/src/cli/codex-shim-autorestore.ts +2 -1
- package/src/cli/codex-shim-readiness.ts +109 -0
- package/src/cli/combo-input.ts +88 -0
- package/src/cli/combo-result.ts +52 -0
- package/src/cli/combo-stats.ts +123 -0
- package/src/cli/combo.ts +37 -14
- package/src/cli/companion-timeline.ts +142 -0
- package/src/cli/companion-usage.ts +146 -0
- package/src/cli/companion.ts +12 -3
- package/src/cli/config-command.ts +24 -8
- package/src/cli/connect.ts +19 -0
- package/src/cli/dispatch.ts +55 -82
- package/src/cli/doctor.ts +22 -4
- package/src/cli/export-command.ts +20 -9
- package/src/cli/gui.ts +8 -3
- package/src/cli/help-catalog.ts +101 -0
- package/src/cli/help-models-context.ts +17 -0
- package/src/cli/help-navigation.ts +50 -0
- package/src/cli/help-recovery.ts +66 -0
- package/src/cli/help.ts +69 -11
- package/src/cli/index.ts +38 -27
- package/src/cli/injection-follow.ts +35 -0
- package/src/cli/inspect.ts +5 -0
- package/src/cli/integration-aside-sync.ts +69 -0
- package/src/cli/integration-cursor.ts +106 -0
- package/src/cli/integration-input.ts +60 -0
- package/src/cli/integration-journal.ts +49 -0
- package/src/cli/integration-plan-dto.ts +156 -0
- package/src/cli/integration-preview.ts +215 -0
- package/src/cli/integrations.ts +24 -18
- package/src/cli/json-input.ts +134 -0
- package/src/cli/link.ts +32 -4
- package/src/cli/local-sync-result.ts +41 -0
- package/src/cli/log-follow.ts +84 -0
- package/src/cli/log-view-filter.ts +146 -0
- package/src/cli/logout-command.ts +44 -0
- package/src/cli/model-picker-ordering.ts +137 -0
- package/src/cli/models-custom-input.ts +107 -0
- package/src/cli/models-custom-runtime.ts +112 -0
- package/src/cli/models-order.ts +194 -0
- package/src/cli/models-runtime-subcommands.ts +2 -0
- package/src/cli/models-runtime.ts +9 -1
- package/src/cli/models.ts +66 -81
- package/src/cli/observe-stream.ts +142 -0
- package/src/cli/observe.ts +75 -34
- package/src/cli/opencode.ts +54 -10
- package/src/cli/provider-batch.ts +77 -0
- package/src/cli/provider-lifecycle-runtime.ts +170 -0
- package/src/cli/provider-result.ts +124 -0
- package/src/cli/provider-runtime.ts +15 -12
- package/src/cli/provider-settings.ts +143 -0
- package/src/cli/provider.ts +116 -64
- package/src/cli/registry.ts +90 -24
- package/src/cli/remote-workspace-hub.ts +83 -0
- package/src/cli/remote-workspace.ts +6 -1
- package/src/cli/restart-failure.ts +27 -0
- package/src/cli/root.ts +42 -9
- package/src/cli/route-policy-write.ts +137 -0
- package/src/cli/route-policy.ts +8 -0
- package/src/cli/runtime-api.ts +57 -80
- package/src/cli/secret-args.ts +149 -0
- package/src/cli/settings-result.ts +30 -0
- package/src/cli/storage.ts +77 -7
- package/src/cli/system-command.ts +60 -2
- package/src/cli/system-restart-client.ts +20 -3
- package/src/cli/system-settings-parity.ts +83 -0
- package/src/cli/tray-proxy.ts +3 -2
- package/src/cli/update-restart-candidate.ts +21 -0
- package/src/cli/update-restart-child.ts +81 -0
- package/src/cli/update-restart-home.ts +38 -0
- package/src/cli/update-restart-transport.ts +193 -0
- package/src/cli/update-restart.ts +260 -0
- package/src/cli/usage-model-search.ts +58 -0
- package/src/cli/usage-report.ts +13 -5
- package/src/cli/v2-input.ts +66 -0
- package/src/cli/v2-local-output.ts +37 -0
- package/src/cli/v2-runtime.ts +90 -0
- package/src/cli/v2.ts +102 -144
- package/src/clients/config-export/contracts.ts +100 -0
- package/src/clients/config-export/droid.ts +54 -2
- package/src/clients/config-export/dsh.ts +21 -0
- package/src/clients/config-export/kilo.ts +12 -2
- package/src/clients/config-export/model-metadata.ts +10 -4
- package/src/clients/config-export/reasoning-metadata.ts +47 -0
- package/src/clients/config-export.ts +53 -67
- package/src/clients/omo-role-models.ts +17 -3
- package/src/codex/account-credit-use.ts +4 -2
- package/src/codex/account-runtime-state.ts +21 -0
- package/src/codex/account-store.ts +11 -47
- package/src/codex/account-usability.ts +11 -8
- package/src/codex/auth-api/account-list.ts +73 -63
- package/src/codex/auth-api/main-account-probe.ts +25 -12
- package/src/codex/auth-api/pool-mode-gate.ts +6 -9
- package/src/codex/auth-api/pool-quota-probe.ts +2 -4
- package/src/codex/auth-api/reset-credit-service.ts +3 -2
- package/src/codex/auth-context.ts +30 -11
- package/src/codex/catalog/aggregation.ts +3 -0
- package/src/codex/catalog/build-entries.ts +1 -1
- package/src/codex/catalog/bundled.ts +9 -2
- package/src/codex/catalog/heal-observation.ts +66 -0
- package/src/codex/catalog/model-hints.ts +2 -1
- package/src/codex/catalog/model-visibility.ts +6 -3
- package/src/codex/catalog/parsing.ts +2 -0
- package/src/codex/catalog/provider-models.ts +13 -4
- package/src/codex/catalog/publication-observer.ts +19 -0
- package/src/codex/catalog/remote.ts +13 -15
- package/src/codex/catalog/restore.ts +19 -6
- package/src/codex/catalog/retained-sync.ts +60 -11
- package/src/codex/catalog/routed-removal.ts +116 -0
- package/src/codex/catalog/write-audit-contract.ts +21 -0
- package/src/codex/catalog/write-audit.ts +171 -0
- package/src/codex/catalog-self-heal.ts +284 -0
- package/src/codex/catalog-write-serialization.ts +71 -7
- package/src/codex/chatgpt-refresh-failure.ts +56 -0
- package/src/codex/codex-home-owner.ts +124 -0
- package/src/codex/convergence.ts +57 -16
- package/src/codex/features.ts +8 -1
- package/src/codex/inject/restore.ts +46 -4
- package/src/codex/inject.ts +11 -1
- package/src/codex/internal/catalog-writer.ts +120 -13
- package/src/codex/journal.ts +49 -13
- package/src/codex/main-account.ts +144 -11
- package/src/codex/management-convergence.ts +3 -0
- package/src/codex/quota-refresh-outcome.ts +17 -2
- package/src/codex/quota-types.ts +36 -1
- package/src/codex/quota.ts +67 -16
- package/src/codex/refresh.ts +20 -9
- package/src/codex/routing/cooldown-math.ts +10 -4
- package/src/codex/routing/selection.ts +11 -4
- package/src/codex/routing.ts +9 -9
- package/src/codex/runtime.ts +7 -1
- package/src/codex/shim-diagnostics.ts +80 -0
- package/src/codex/shim-fingerprint.ts +7 -2
- package/src/codex/shim-migration.ts +153 -0
- package/src/codex/shim-overlay.ts +509 -0
- package/src/codex/shim-path-resolution.ts +145 -0
- package/src/codex/shim-probe.ts +4 -3
- package/src/codex/shim-state-file.ts +93 -7
- package/src/codex/shim.ts +54 -107
- package/src/codex/subagent-model-fallback.ts +2 -1
- package/src/codex/sync.ts +24 -4
- package/src/combos/failover.ts +101 -1
- package/src/combos/index.ts +1 -0
- package/src/combos/jev.ts +13 -3
- package/src/combos/resolve.ts +12 -0
- package/src/config/atomic-write.ts +13 -8
- package/src/config/diagnostics.ts +23 -1
- package/src/config/load-degrade.ts +7 -0
- package/src/config/provider-validation.ts +3 -0
- package/src/config/schema/chatgpt-desktop.ts +22 -0
- package/src/config/schema/compaction-triggers.ts +8 -0
- package/src/config/schema/config-schema.ts +43 -14
- package/src/config/schema/leaf-validators.ts +8 -3
- package/src/config/subagent-models.ts +12 -0
- package/src/generated/compatibility-version.json +645 -237
- package/src/grok/session-identity.ts +33 -0
- package/src/integrations/catalog-refresh.ts +1 -1
- package/src/integrations/merge.ts +13 -5
- package/src/integrations/mutation-plan.ts +50 -5
- package/src/integrations/omp-yaml-source.ts +181 -1
- package/src/integrations/ownership-policy.ts +66 -0
- package/src/integrations/registry.ts +50 -1
- package/src/integrations/state.ts +106 -19
- package/src/integrations/target.ts +25 -2
- package/src/integrations/writer.ts +107 -35
- package/src/lib/gui-pair-capability.ts +17 -0
- package/src/lib/package-tree-integrity.ts +42 -1
- package/src/lib/provider-client-headers.ts +62 -0
- package/src/lib/proxy-env.ts +3 -0
- package/src/lib/test-home-guard.ts +2 -1
- package/src/oauth/anthropic-account-refusal.ts +19 -8
- package/src/oauth/anthropic-continuity.ts +65 -0
- package/src/oauth/anthropic-identity.ts +75 -0
- package/src/oauth/anthropic-routing.ts +20 -15
- package/src/oauth/anthropic.ts +34 -3
- package/src/oauth/generic-account-failover.ts +21 -0
- package/src/oauth/health.ts +32 -5
- package/src/oauth/index.ts +42 -35
- package/src/oauth/local-token-detect.ts +7 -0
- package/src/oauth/pool-settings-capability.ts +9 -2
- package/src/oauth/refresh-policy.ts +2 -0
- package/src/oauth/store.ts +55 -6
- package/src/oauth/types.ts +9 -0
- package/src/protocols/encoders/messages.ts +4 -3
- package/src/protocols/plan-snapshot.ts +1 -1
- package/src/protocols/settings.ts +28 -4
- package/src/providers/antigravity-effort-families.ts +75 -0
- package/src/providers/antigravity-models.ts +54 -16
- package/src/providers/antigravity-wire-snapshot.ts +66 -0
- package/src/providers/derive.ts +2 -0
- package/src/providers/model-rename-fields.ts +3 -0
- package/src/providers/new-model-policy.ts +48 -10
- package/src/providers/registry/entries-extended.ts +3 -0
- package/src/providers/registry/model-ids.ts +2 -0
- package/src/providers/registry/model-seeds.ts +5 -0
- package/src/providers/registry/types.ts +4 -0
- package/src/responses/compaction.ts +19 -0
- package/src/router.ts +6 -0
- package/src/server/auth-cors.ts +9 -0
- package/src/server/caller-session-identity.ts +32 -0
- package/src/server/chat-completions.ts +10 -0
- package/src/server/claude-messages.ts +29 -15
- package/src/server/droid-reasoning-default.ts +50 -0
- package/src/server/github-copilot-responses-repair.ts +3 -6
- package/src/server/grok-responses-control-frame.ts +2 -2
- package/src/server/grok-responses-snapshot-repair.ts +4 -5
- package/src/server/gui-session.ts +33 -11
- package/src/server/index/claude-intercept-lifecycle.ts +4 -0
- package/src/server/index/serve-options.ts +13 -7
- package/src/server/index.ts +1 -1
- package/src/server/management/agent-settings-routes.ts +12 -134
- package/src/server/management/config-routes.ts +1 -1
- package/src/server/management/context.ts +8 -1
- package/src/server/management/cursor-integration-routes.ts +3 -3
- package/src/server/management/integration-routes.ts +69 -11
- package/src/server/management/link-routes.ts +20 -13
- package/src/server/management/model-routes.ts +3 -3
- package/src/server/management/model-row-export-metadata.ts +150 -0
- package/src/server/management/model-rows.ts +148 -9
- package/src/server/management/native-integration-routes.ts +2 -2
- package/src/server/management/oauth-account-routes.ts +96 -30
- package/src/server/management/route-registry.ts +29 -27
- package/src/server/management/shared.ts +19 -5
- package/src/server/management/subagent-model-routes.ts +214 -0
- package/src/server/messages-native-eligibility.ts +3 -6
- package/src/server/messages-native-oauth.ts +82 -35
- package/src/server/messages-native.ts +78 -16
- package/src/server/relay.ts +14 -44
- package/src/server/request-log-generation-window.ts +38 -0
- package/src/server/request-log.ts +13 -0
- package/src/server/responses/adapter-dispatch.ts +152 -28
- package/src/server/responses/codex-auth-error.ts +10 -2
- package/src/server/responses/combo-stream-preflight.ts +29 -3
- package/src/server/responses/compaction-routing.ts +10 -1
- package/src/server/responses/core-auth.ts +67 -30
- package/src/server/responses/core-codex-account.ts +6 -3
- package/src/server/responses/core-combo-failure.ts +5 -1
- package/src/server/responses/core-combo-native.ts +7 -1
- package/src/server/responses/core-combo.ts +64 -7
- package/src/server/responses/core-normalize.ts +7 -0
- package/src/server/responses/core-options.ts +5 -0
- package/src/server/responses/fetch-helpers.ts +9 -0
- package/src/server/responses/passthrough-delivery.ts +1 -1
- package/src/server/responses/policy-fallback.ts +1 -1
- package/src/server/responses/request-prepare.ts +5 -0
- package/src/server/responses/request-spend.ts +30 -30
- package/src/server/responses-custom-tool-repair.ts +2 -2
- package/src/server/responses-hosted-image-display.ts +70 -9
- package/src/server/responses-terminal-repair.ts +15 -2
- package/src/server/sse-frame-buffer.ts +38 -36
- package/src/server/sse-payload-rewrite.ts +78 -49
- package/src/server/ws-bridge.ts +1 -11
- package/src/service/cli.ts +6 -1
- package/src/service/ownership-mutation-lease.d.mts +26 -0
- package/src/service/ownership-mutation-lease.mjs +76 -1
- package/src/service/windows-taskxml.ts +1 -2
- package/src/service-manager-probe.ts +36 -2
- package/src/types/config.ts +6 -0
- package/src/types/provider.ts +28 -10
- package/src/update/index.ts +2 -1
- package/src/update/npm-invocation.mjs +35 -3
- package/src/update/proxy-liveness-probe.mjs +38 -8
- package/src/update/update-failure-guidance.d.mts +6 -0
- package/src/update/update-failure-guidance.mjs +30 -5
- package/src/usage/expected-prices.ts +9 -0
- package/src/usage/log.ts +7 -0
- package/src/usage/summary.ts +82 -1
- package/src/web-search/loop.ts +11 -8
- package/src/web-search/run-turn-loop.ts +10 -7
- package/gui/dist/assets/App-CddorHdA.js +0 -51
- package/gui/dist/assets/App-DMudtISW.css +0 -1
- package/gui/dist/assets/index-Btze1I3T.css +0 -1
- package/gui/dist/assets/index-CvnY7E6i.js +0 -86
- package/gui/dist/assets/quota-summary-B_AWpHfP.js +0 -1
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import { classifyAnthropic429 } from "../../oauth/anthropic-rate-limit-policy";
|
|
2
2
|
import { rotateAnthropicAccountOnResponse } from "../../oauth/anthropic-account-refusal";
|
|
3
|
+
import { authorizeResendForRecovery } from "../../lib/request-resend-gate";
|
|
4
|
+
import { ambiguousResendAllowanceFor, selfContainedResponsesBody } from "./reset-replay";
|
|
5
|
+
import { transientSendCapFor } from "./request-send-budget";
|
|
3
6
|
import { isNonReplayableResponse } from "../../lib/upstream-retry";
|
|
4
7
|
import { isLocalUpstream } from "../../lib/local-upstream";
|
|
5
8
|
import type { ResponsesRequestContext, ResponsesAdmissionState } from "./core-options";
|
|
@@ -25,6 +28,7 @@ import { withProviderRequestSlot } from "../../providers/request-pacing";
|
|
|
25
28
|
import { providerFetch, fetchWithHeaderTimeout, safeHostLabel } from "./fetch-helpers";
|
|
26
29
|
import {
|
|
27
30
|
transientRetryPolicyFor,
|
|
31
|
+
resetReplayPolicyFor,
|
|
28
32
|
rateLimitRetryPolicyFor,
|
|
29
33
|
hasKeyPoolFailover,
|
|
30
34
|
rotateProviderTransportOn401,
|
|
@@ -46,9 +50,10 @@ import type { OpaqueBlobRecoveryGuard } from "./core-opaque-recovery";
|
|
|
46
50
|
import type { AttemptRecoveryKind } from "../../usage/log";
|
|
47
51
|
import type { OAuthAccessSnapshot } from "../../oauth";
|
|
48
52
|
import { OAuthAccountPausedError, OAuthLoginRequiredError, publicOAuthAuthenticationErrorMessage } from "../../oauth";
|
|
49
|
-
import { getAccountSet } from "../../oauth/store";
|
|
53
|
+
import { getAccountSet, markAccountNeedsReauthIfGeneration } from "../../oauth/store";
|
|
50
54
|
import { tryKiroAlternateAfterTerminalRefresh } from "../../oauth/kiro-terminal-failover";
|
|
51
55
|
import { classifyKiroRefusal } from "../../adapters/kiro-refusal";
|
|
56
|
+
import { classifyAntigravityRefusal } from "../../adapters/antigravity-refusal";
|
|
52
57
|
import { normalizeFinalKiroHttpError } from "../../adapters/kiro-retry";
|
|
53
58
|
import { noteKiroMonthlyRefusal } from "../../providers/kiro-usage";
|
|
54
59
|
import { persistKiroAccountState } from "../../providers/kiro-account-state-disk";
|
|
@@ -155,6 +160,8 @@ export async function prepareAdapterExchange(
|
|
|
155
160
|
| "reserveCredentialHop"
|
|
156
161
|
| "pendingHopPermit"
|
|
157
162
|
| "workflowRootId"
|
|
163
|
+
| "claimAmbiguousResend"
|
|
164
|
+
| "sendsUsed"
|
|
158
165
|
>,
|
|
159
166
|
) {
|
|
160
167
|
const { options, config, logCtx, req } = requestContext;
|
|
@@ -188,9 +195,18 @@ export async function prepareAdapterExchange(
|
|
|
188
195
|
recoveryClassFor,
|
|
189
196
|
sendBudgetExhausted,
|
|
190
197
|
reserveCredentialHop,
|
|
198
|
+
claimAmbiguousResend,
|
|
191
199
|
} = sendBudgetState;
|
|
192
200
|
|
|
193
201
|
|
|
202
|
+
let selfContainedJudgment: boolean | undefined;
|
|
203
|
+
const requestIsSelfContained = (): boolean =>
|
|
204
|
+
selfContainedJudgment ??= selfContainedResponsesBody(parsed._rawBody);
|
|
205
|
+
const claimPreHeaderResend = (): boolean => authorizeResendForRecovery(
|
|
206
|
+
"pre-header", "connection-reset",
|
|
207
|
+
ambiguousResendAllowanceFor(route.provider, requestIsSelfContained, claimAmbiguousResend),
|
|
208
|
+
).allowed;
|
|
209
|
+
|
|
194
210
|
const upstream = new AbortController();
|
|
195
211
|
const cleanupUpstreamAbort = linkAbortSignal(upstream, options.abortSignal);
|
|
196
212
|
const connectMs = config.connectTimeoutMs ?? 200_000;
|
|
@@ -342,6 +358,13 @@ export async function prepareAdapterExchange(
|
|
|
342
358
|
const compactPrepaid = options.compactionRecoveryAttempted ? sendBudgetState.pendingHopPermit : undefined;
|
|
343
359
|
if (compactPrepaid) sendBudgetState.pendingHopPermit = undefined;
|
|
344
360
|
let compactPrepaidUsed = false;
|
|
361
|
+
// Combo and emergency compaction admission already book this target's first send.
|
|
362
|
+
// Its configured initial ceiling is target-local; the shared remainder below still
|
|
363
|
+
// accounts for earlier targets without deducting their sends from this target twice.
|
|
364
|
+
const initialSendCap = transientPolicy || resetReplayPolicyFor(route.provider)
|
|
365
|
+
? transientSendCapFor(transientPolicy?.attempts,
|
|
366
|
+
(options.comboAttempt || compactPrepaid) ? 0 : sendBudgetState.sendsUsed)
|
|
367
|
+
: 1;
|
|
345
368
|
const fetchWithRetryPolicy = (route.provider.adapter === "google" || transientPolicy)
|
|
346
369
|
? fetchWithTransientRetry
|
|
347
370
|
: fetchWithResetRetry;
|
|
@@ -366,15 +389,12 @@ export async function prepareAdapterExchange(
|
|
|
366
389
|
{
|
|
367
390
|
abortSignal: upstream.signal,
|
|
368
391
|
label: safeHostLabel(builtInitialRequest.url),
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
// holder but used to take a fresh full allowance on its own first send, so the
|
|
372
|
-
// shared counter was inherited without ever being read as a limit.
|
|
392
|
+
claimAmbiguousResend: claimPreHeaderResend,
|
|
393
|
+
...(transientPolicy || resetReplayPolicyFor(route.provider) || compactPrepaid
|
|
373
394
|
? {
|
|
374
|
-
//
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
remainingTransientSendBudget(transientPolicy?.attempts ?? 1) + (compactPrepaid ? 1 : 0)),
|
|
395
|
+
// A pending compaction permit already booked this leg's first physical send.
|
|
396
|
+
attempts: Math.min(initialSendCap,
|
|
397
|
+
remainingTransientSendBudget(initialSendCap) + (compactPrepaid ? 1 : 0)),
|
|
378
398
|
onSendsConsumed: noteTransientSends,
|
|
379
399
|
}
|
|
380
400
|
: {}),
|
|
@@ -544,14 +564,21 @@ export async function prepareAdapterExchange(
|
|
|
544
564
|
const refetchWithPolicy = (route.provider.adapter === "google" || refetchTransientPolicy)
|
|
545
565
|
? fetchWithTransientRetry
|
|
546
566
|
: fetchWithResetRetry;
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
const
|
|
551
|
-
?
|
|
552
|
-
|
|
553
|
-
|
|
567
|
+
const helperCountsSends = refetchTransientPolicy !== null || resetReplayPolicyFor(route.provider) !== null;
|
|
568
|
+
const prepaid = sendBudgetState.pendingHopPermit;
|
|
569
|
+
const configuredTotal = refetchTransientPolicy?.attempts;
|
|
570
|
+
const refetchCap = transientSendCapFor(configuredTotal,
|
|
571
|
+
sendBudgetState.sendsUsed - (prepaid ? 1 : 0));
|
|
572
|
+
// An exact total includes a booked hop, even when it consumed the last base slot.
|
|
573
|
+
// Keep that funded send while forbidding the final reserve from widening the total.
|
|
574
|
+
const prepaidLastSlot = helperCountsSends && configuredTotal !== undefined && prepaid
|
|
575
|
+
&& refetchCap > 0 && remainingTransientSendBudget(refetchCap) === 0;
|
|
576
|
+
if (prepaidLastSlot) sendBudgetState.pendingHopPermit = undefined;
|
|
577
|
+
const refetchAllowance = helperCountsSends
|
|
578
|
+
? prepaidLastSlot ? { attempts: 1, permit: prepaid } : recoverySendAllowance(
|
|
579
|
+
refetchCap, recoveryClassFor(recovery),
|
|
554
580
|
`${route.providerName}|${route.modelId}|${recovery}`,
|
|
581
|
+
{ allowFinalRecoveryReserve: configuredTotal === undefined },
|
|
555
582
|
)
|
|
556
583
|
: undefined;
|
|
557
584
|
try {
|
|
@@ -565,7 +592,7 @@ export async function prepareAdapterExchange(
|
|
|
565
592
|
// can be refused above before it does. use() past the first attempt is a no-op.
|
|
566
593
|
onDispatch?.();
|
|
567
594
|
if (preserveFailureResponse) replacementAdmitted = true;
|
|
568
|
-
if (!
|
|
595
|
+
if (!refetchAllowance) chargeFastDowngradeWorkflowSend();
|
|
569
596
|
return fetchWithHeaderTimeout(retryRequest.url,
|
|
570
597
|
applyUpstreamRecoveryInit({
|
|
571
598
|
method: retryRequest.method, headers: retryRequest.headers, body: retryRequest.body,
|
|
@@ -579,6 +606,7 @@ export async function prepareAdapterExchange(
|
|
|
579
606
|
{
|
|
580
607
|
abortSignal: upstream.signal,
|
|
581
608
|
label: safeHostLabel(retryRequest.url),
|
|
609
|
+
claimAmbiguousResend: claimPreHeaderResend,
|
|
582
610
|
...(refetchAllowance
|
|
583
611
|
? {
|
|
584
612
|
attempts: refetchAllowance.attempts,
|
|
@@ -637,7 +665,7 @@ export async function prepareAdapterExchange(
|
|
|
637
665
|
const adapterOwnsDispatch = transportState.activeAdapter.fetchResponse !== undefined;
|
|
638
666
|
const hop = reserveCredentialHop("auth-recovery",
|
|
639
667
|
`${route.providerName}|${route.modelId}|antigravity-auth`,
|
|
640
|
-
!adapterOwnsDispatch && transientRetryPolicyFor(route.provider) !== null);
|
|
668
|
+
!adapterOwnsDispatch && (transientRetryPolicyFor(route.provider) !== null || resetReplayPolicyFor(route.provider) !== null));
|
|
641
669
|
if (!hop.allowed) return null;
|
|
642
670
|
try {
|
|
643
671
|
const snapshot = await failoverAccountSnapshot(route.providerName, nextId);
|
|
@@ -707,7 +735,7 @@ export async function prepareAdapterExchange(
|
|
|
707
735
|
const adapterOwnsDispatch = transportState.activeAdapter.fetchResponse !== undefined;
|
|
708
736
|
const hop = reserveCredentialHop("auth-recovery",
|
|
709
737
|
`${route.providerName}|${route.modelId}|terminal-refresh-account`,
|
|
710
|
-
!adapterOwnsDispatch && transientRetryPolicyFor(route.provider) !== null);
|
|
738
|
+
!adapterOwnsDispatch && (transientRetryPolicyFor(route.provider) !== null || resetReplayPolicyFor(route.provider) !== null));
|
|
711
739
|
if (hop.allowed) {
|
|
712
740
|
try {
|
|
713
741
|
const admitted = await applyFailoverSnapshot(alternate);
|
|
@@ -860,7 +888,7 @@ export async function prepareAdapterExchange(
|
|
|
860
888
|
const hop = reserveCredentialHop(
|
|
861
889
|
"repair",
|
|
862
890
|
`${route.providerName}|${route.modelId}|anthropic-fast-downgrade`,
|
|
863
|
-
!adapterOwnsDispatch && transientRetryPolicyFor(route.provider) !== null,
|
|
891
|
+
!adapterOwnsDispatch && (transientRetryPolicyFor(route.provider) !== null || resetReplayPolicyFor(route.provider) !== null),
|
|
864
892
|
);
|
|
865
893
|
if (hop.allowed) {
|
|
866
894
|
anthropicFastDowngradeGuard.attempted = true;
|
|
@@ -1058,10 +1086,9 @@ export async function prepareAdapterExchange(
|
|
|
1058
1086
|
const hop = reserveCredentialHop(
|
|
1059
1087
|
"auth-recovery",
|
|
1060
1088
|
`${route.providerName}|${route.modelId}|adapter-recovery-oauth-429`,
|
|
1061
|
-
//
|
|
1062
|
-
//
|
|
1063
|
-
|
|
1064
|
-
!adapterOwnsDispatch && transientRetryPolicyFor(route.provider) !== null,
|
|
1089
|
+
// Transient and opted-in reset helpers report physical sends; an adapter-owned
|
|
1090
|
+
// ladder settles its own booking.
|
|
1091
|
+
!adapterOwnsDispatch && (transientRetryPolicyFor(route.provider) !== null || resetReplayPolicyFor(route.provider) !== null),
|
|
1065
1092
|
);
|
|
1066
1093
|
if (!hop.allowed) break;
|
|
1067
1094
|
const nextAccountId = rotateGenericOAuthAccountOnRefusal(
|
|
@@ -1163,10 +1190,9 @@ export async function prepareAdapterExchange(
|
|
|
1163
1190
|
const hop = reserveCredentialHop(
|
|
1164
1191
|
"auth-recovery",
|
|
1165
1192
|
`${route.providerName}|${route.modelId}|adapter-recovery-oauth-429`,
|
|
1166
|
-
//
|
|
1167
|
-
//
|
|
1168
|
-
|
|
1169
|
-
!adapterOwnsDispatch && transientRetryPolicyFor(route.provider) !== null,
|
|
1193
|
+
// Transient and opted-in reset helpers report physical sends; an adapter-owned
|
|
1194
|
+
// ladder settles its own booking.
|
|
1195
|
+
!adapterOwnsDispatch && (transientRetryPolicyFor(route.provider) !== null || resetReplayPolicyFor(route.provider) !== null),
|
|
1170
1196
|
);
|
|
1171
1197
|
if (!hop.allowed) break;
|
|
1172
1198
|
const nextAccountId = rotateGenericOAuthAccountOn429(
|
|
@@ -1243,6 +1269,104 @@ export async function prepareAdapterExchange(
|
|
|
1243
1269
|
break;
|
|
1244
1270
|
}
|
|
1245
1271
|
}
|
|
1272
|
+
// Antigravity verify-account quarantine: a 403 demanding account verification
|
|
1273
|
+
// is terminal for THAT account — its grant still refreshes, so only a manual
|
|
1274
|
+
// verification followed by a re-login clears it. Mark it needsReauth (durable,
|
|
1275
|
+
// shown as `needs-reauth(verify)` in `ocx account list` with a `verify_account`
|
|
1276
|
+
// health reason in the management API, excluded from the pool until re-login)
|
|
1277
|
+
// and replay the same request on the next eligible account so the pool keeps
|
|
1278
|
+
// serving.
|
|
1279
|
+
while (
|
|
1280
|
+
upstreamResponse.status === 403
|
|
1281
|
+
&& route.providerName === "google-antigravity"
|
|
1282
|
+
&& transportState.genericFailoverAccountId
|
|
1283
|
+
) {
|
|
1284
|
+
const refusal = classifyAntigravityRefusal(upstreamResponse.status,
|
|
1285
|
+
await readDisplaySafeErrorText(upstreamResponse.clone(), upstream.signal, ""));
|
|
1286
|
+
if (refusal.kind === "other") break;
|
|
1287
|
+
const sent = transportState.sentOAuthSnapshot;
|
|
1288
|
+
const failedAccountId = transportState.genericFailoverAccountId;
|
|
1289
|
+
// Fenced write: a 403 arriving after the credential rotated (refresh or
|
|
1290
|
+
// re-login) must not quarantine the new login. Without the matching sent
|
|
1291
|
+
// snapshot there is no owner for this write, so skip marking.
|
|
1292
|
+
if (!sent || sent.accountId !== failedAccountId) break;
|
|
1293
|
+
try {
|
|
1294
|
+
await markAccountNeedsReauthIfGeneration(route.providerName, failedAccountId, sent.generation, undefined, "verify_account");
|
|
1295
|
+
} catch {
|
|
1296
|
+
// A failed quarantine write must preserve the refusal, not authorize another send.
|
|
1297
|
+
break recovery;
|
|
1298
|
+
}
|
|
1299
|
+
if (!antigravityPoolActivated || transportState.genericFailovers >= transportState.genericFailoverLimit) break;
|
|
1300
|
+
const adapterOwnsDispatch = transportState.activeAdapter.fetchResponse !== undefined;
|
|
1301
|
+
const hop = reserveCredentialHop(
|
|
1302
|
+
"auth-recovery",
|
|
1303
|
+
`${route.providerName}|${route.modelId}|adapter-recovery-oauth-verify`,
|
|
1304
|
+
!adapterOwnsDispatch && (transientRetryPolicyFor(route.provider) !== null || resetReplayPolicyFor(route.provider) !== null),
|
|
1305
|
+
);
|
|
1306
|
+
if (!hop.allowed) break;
|
|
1307
|
+
// Auth-refusal rotation, not the rate-limit one: a verification refusal
|
|
1308
|
+
// must not record rate-limit cooldown semantics against the account.
|
|
1309
|
+
const nextAccountId = rotateAntigravityAccountOnAuthRefusal(
|
|
1310
|
+
antigravityPoolActivated,
|
|
1311
|
+
failedAccountId,
|
|
1312
|
+
sent.generation,
|
|
1313
|
+
route.modelId,
|
|
1314
|
+
);
|
|
1315
|
+
if (!nextAccountId) {
|
|
1316
|
+
hop.permit?.release();
|
|
1317
|
+
break;
|
|
1318
|
+
}
|
|
1319
|
+
// The refusal body stays alive until the replacement owns the outcome: it
|
|
1320
|
+
// is also the preserveFailureResponse below, so cancelling it early would
|
|
1321
|
+
// hand back a 403 with a destroyed body when the rebuild is refused.
|
|
1322
|
+
const failedResponse = upstreamResponse;
|
|
1323
|
+
try {
|
|
1324
|
+
// The FULL snapshot, not just the bearer: Antigravity pairs an
|
|
1325
|
+
// account-matched projectId with its token, so a token-only swap would mix
|
|
1326
|
+
// one account's credential with another's routing data.
|
|
1327
|
+
const snapshot = await failoverAccountSnapshot(route.providerName, nextAccountId);
|
|
1328
|
+
if (!await applyFailoverSnapshot(snapshot)) {
|
|
1329
|
+
hop.permit?.release();
|
|
1330
|
+
break;
|
|
1331
|
+
}
|
|
1332
|
+
invalidateSameTargetRequest();
|
|
1333
|
+
transportState.activeAdapter = resolveSelectionAdapter(
|
|
1334
|
+
resolveWireProtocolOverride(route.providerName, route.modelId, route.provider, inboundWire, route.staticPolicy),
|
|
1335
|
+
config.cacheRetention,
|
|
1336
|
+
);
|
|
1337
|
+
bindRouteReasoningReplayScope({
|
|
1338
|
+
parsed,
|
|
1339
|
+
providerName: route.providerName,
|
|
1340
|
+
provider: route.provider,
|
|
1341
|
+
adapterName: transportState.activeAdapter.name,
|
|
1342
|
+
oauthCredentialSnapshot: transportState.replayOAuthCredentialSnapshot,
|
|
1343
|
+
});
|
|
1344
|
+
sealRequestAttemptIdentity(logCtx.activeAttempt, logCtx.provider, transportState.activeAdapter.name, logCtx.accountLogLabel);
|
|
1345
|
+
recordAttemptCredentialSource(logCtx.activeAttempt, route.providerName, route.provider, transportState.activeAdapter.name);
|
|
1346
|
+
sendBudgetState.pendingHopPermit = hop.permit;
|
|
1347
|
+
let result: Response | { failed: Response };
|
|
1348
|
+
try {
|
|
1349
|
+
result = await rebuildAndRefetch("oauth-account-403", () => {
|
|
1350
|
+
if (!adapterOwnsDispatch) hop.permit?.use();
|
|
1351
|
+
}, failedResponse);
|
|
1352
|
+
} finally {
|
|
1353
|
+
sendBudgetState.pendingHopPermit = undefined;
|
|
1354
|
+
}
|
|
1355
|
+
if ("failed" in result) {
|
|
1356
|
+
hop.permit?.release();
|
|
1357
|
+
if (result.failed !== failedResponse) return result.failed;
|
|
1358
|
+
// Preserve common redaction and combo consumption without another recovery send.
|
|
1359
|
+
break recovery;
|
|
1360
|
+
}
|
|
1361
|
+
transportState.genericFailovers += 1;
|
|
1362
|
+
try { void failedResponse.body?.cancel().catch(() => {}); } catch { /* already consumed/closed */ }
|
|
1363
|
+
upstreamResponse = result;
|
|
1364
|
+
if (isNonReplayableResponse(upstreamResponse)) continue recovery;
|
|
1365
|
+
} catch {
|
|
1366
|
+
hop.permit?.release();
|
|
1367
|
+
break;
|
|
1368
|
+
}
|
|
1369
|
+
}
|
|
1246
1370
|
}
|
|
1247
1371
|
// Unknown provenance is deliberately fail-soft in pre-flight: after a restart, TTL expiry,
|
|
1248
1372
|
// or LRU eviction, a valid same-backend blob must survive. A decoder's own 4xx identity is
|
|
@@ -30,9 +30,15 @@ export function codexModelAvailabilityErrorResponse(error: CodexModelAvailabilit
|
|
|
30
30
|
return formatErrorResponse(400, "invalid_request_error", error.message);
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
+
/** Actionable sign-in guidance without guessing which upstream revocation cause occurred. */
|
|
34
|
+
export const CODEX_MAIN_SIGN_IN_REQUIRED_MESSAGE =
|
|
35
|
+
"Codex main account needs sign-in. Run `codex login` to sign in again.";
|
|
36
|
+
|
|
33
37
|
export function nativeMainRefreshFailureResponse(error: unknown): Response {
|
|
34
38
|
if (error instanceof MainAccountTokenRefreshError && error.reason === "reauth") {
|
|
35
|
-
|
|
39
|
+
// Same sentence the quarantine produces on every later request, so the first refusal and the
|
|
40
|
+
// ones after it do not describe one dead credential two different ways.
|
|
41
|
+
return formatErrorResponse(401, "authentication_error", CODEX_MAIN_SIGN_IN_REQUIRED_MESSAGE);
|
|
36
42
|
}
|
|
37
43
|
if (error instanceof MainAccountTokenRefreshError
|
|
38
44
|
|| error instanceof MainAuthJsonChangedDuringRefreshError
|
|
@@ -98,7 +104,9 @@ export function mapCodexAuthContextErrorToResponse(
|
|
|
98
104
|
return codexModelAvailabilityErrorResponse(error);
|
|
99
105
|
}
|
|
100
106
|
if (error instanceof CodexPoolAuthenticationError || error instanceof CodexDirectAuthenticationError) {
|
|
101
|
-
return formatErrorResponse(401, "authentication_error",
|
|
107
|
+
return formatErrorResponse(401, "authentication_error",
|
|
108
|
+
error instanceof CodexPoolAuthenticationError && error.quarantinedMain
|
|
109
|
+
? CODEX_MAIN_SIGN_IN_REQUIRED_MESSAGE : error.message);
|
|
102
110
|
}
|
|
103
111
|
if (error instanceof CodexMainSubstitutionUnavailableError) {
|
|
104
112
|
return formatErrorResponse(
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { codexAccountModelRefusal, codexAccountModelRefusalPayload, hasConflictingCodexModelRefusalEnvelopes, type CodexAccountModelRefusal } from "../../combos/failover";
|
|
1
2
|
import type { ResponsesTerminalStatus } from "../../bridge";
|
|
2
3
|
import { comboFailureDecision } from "../../combos";
|
|
3
4
|
import { httpStatusFromTerminalError } from "../../lib/errors";
|
|
@@ -68,6 +69,26 @@ function bareErrorStatus(payload: unknown): number | undefined {
|
|
|
68
69
|
});
|
|
69
70
|
}
|
|
70
71
|
|
|
72
|
+
/** Match the selected SSE error envelope before projection drops competing fields. */
|
|
73
|
+
function terminalModelRefusal(status: number, event: Record<string, unknown>): CodexAccountModelRefusal {
|
|
74
|
+
if (hasConflictingCodexModelRefusalEnvelopes(status, event)) return "ambiguous";
|
|
75
|
+
const outer = codexAccountModelRefusalPayload(status, event);
|
|
76
|
+
if (outer === "ambiguous") return outer;
|
|
77
|
+
const nested = event.response;
|
|
78
|
+
if (event.type !== "error" && nested && typeof nested === "object" && !Array.isArray(nested)) {
|
|
79
|
+
const nestedKind = codexAccountModelRefusalPayload(status, nested);
|
|
80
|
+
if (nestedKind === "ambiguous") return nestedKind;
|
|
81
|
+
const nestedError = (nested as Record<string, unknown>).error;
|
|
82
|
+
if (nestedError && typeof nestedError === "object" && !Array.isArray(nestedError)) {
|
|
83
|
+
return nestedKind;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
if (event.type === "error" && !Object.hasOwn(event, "error") && typeof event.message === "string") {
|
|
87
|
+
return codexAccountModelRefusal(status, event.message, { allowNestedResponse: false });
|
|
88
|
+
}
|
|
89
|
+
return outer;
|
|
90
|
+
}
|
|
91
|
+
|
|
71
92
|
function bareErrorIsRetryable(payload: unknown): boolean {
|
|
72
93
|
const status = bareErrorStatus(payload);
|
|
73
94
|
if (status === undefined || !payload || typeof payload !== "object" || Array.isArray(payload)) {
|
|
@@ -84,7 +105,9 @@ function bareErrorIsRetryable(payload: unknown): boolean {
|
|
|
84
105
|
const message = typeof error.message === "string"
|
|
85
106
|
? error.message
|
|
86
107
|
: typeof event.message === "string" ? event.message : "";
|
|
87
|
-
return comboFailureDecision(status, message, {
|
|
108
|
+
return comboFailureDecision(status, message, {
|
|
109
|
+
code, codexModelRefusal: terminalModelRefusal(status, event),
|
|
110
|
+
}) === "hop";
|
|
88
111
|
}
|
|
89
112
|
|
|
90
113
|
function retryableZeroOutputTerminal(payload: unknown): boolean {
|
|
@@ -226,7 +249,7 @@ function failedTerminalResponse(
|
|
|
226
249
|
|
|
227
250
|
export type ComboStreamPreflightResult =
|
|
228
251
|
| { kind: "accepted"; response: Response }
|
|
229
|
-
| { kind: "failed"; response: Response }
|
|
252
|
+
| { kind: "failed"; response: Response; codexModelRefusal?: CodexAccountModelRefusal }
|
|
230
253
|
/**
|
|
231
254
|
* The body errored mid-stream and `replayReadErrors` asked for the prefix back rather than
|
|
232
255
|
* a rethrow. `stage` is how far the inspection actually got; whether that permits a
|
|
@@ -322,7 +345,10 @@ export async function preflightComboStreamResponse(
|
|
|
322
345
|
|| retryableTerminalPayload?.type === "error")
|
|
323
346
|
&& !outputCommitted && retryableTerminalPayload) {
|
|
324
347
|
await reader.cancel("retrying zero-output combo stream terminal").catch(() => undefined);
|
|
325
|
-
|
|
348
|
+
const failed = failedTerminalResponse(response, retryableTerminalPayload, logCtx);
|
|
349
|
+
return { kind: "failed", response: failed,
|
|
350
|
+
...(failed.status === 400
|
|
351
|
+
? { codexModelRefusal: terminalModelRefusal(failed.status, retryableTerminalPayload) } : {}) };
|
|
326
352
|
}
|
|
327
353
|
if (next.done || terminalStatus !== undefined || outputCommitted
|
|
328
354
|
|| bufferedBytes >= COMBO_STREAM_PREFLIGHT_MAX_BYTES
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { OcxConfig } from "../../types";
|
|
2
2
|
import { isDeclaredReasoningEffort } from "../../reasoning-effort";
|
|
3
|
-
import { COMPACTION_TRIGGERS } from "../../config/schema/compaction-triggers";
|
|
3
|
+
import { COMPACTION_TRIGGERS, validCompactionSourceModels } from "../../config/schema/compaction-triggers";
|
|
4
4
|
import { routeConcreteModel, type RouteResult } from "../../router";
|
|
5
5
|
import { resolveComboId } from "../../combos/identifiers";
|
|
6
6
|
import { resolvePolicyProfileId } from "../../routing/profile";
|
|
@@ -57,6 +57,15 @@ export function applyCompactionRoutingOverride(
|
|
|
57
57
|
&& (typeof override.reasoningEffort !== "string" || !isDeclaredReasoningEffort(override.reasoningEffort))) return null;
|
|
58
58
|
const triggers = configuredTriggers(override.triggers);
|
|
59
59
|
if (!triggers) return null;
|
|
60
|
+
if (override.sourceModels !== undefined) {
|
|
61
|
+
if (!validCompactionSourceModels(override.sourceModels)) return null;
|
|
62
|
+
// Match the selector the conversation actually routed on: a synthetic `--fast` or
|
|
63
|
+
// effort suffix is ingress decoration, so `cheap--fast` is still the allowlisted `cheap`.
|
|
64
|
+
const source = sourceSelectorOf(config, raw.model);
|
|
65
|
+
if (!override.sourceModels.some(selector => selector.endsWith("/*")
|
|
66
|
+
? source.startsWith(selector.slice(0, -1)) && source.length > selector.length - 1
|
|
67
|
+
: source === selector)) return null;
|
|
68
|
+
}
|
|
60
69
|
if (options.endpoint !== "compact"
|
|
61
70
|
&& !(Array.isArray(raw.input) && raw.input.some(item => record(item)?.type === "compaction_trigger"))) return null;
|
|
62
71
|
|
|
@@ -41,13 +41,19 @@ import { markLocalRequestLogRefusal } from "../request-log";
|
|
|
41
41
|
import { CODEX_POOL_REFRESH_INCOMPLETE_LOG_REASON } from "../../codex/pool-refresh-backoff";
|
|
42
42
|
import { codexAuthContextLogLabel } from "../../codex/account-label";
|
|
43
43
|
import { forceRefreshMainAccountToken } from "../../codex/main-account";
|
|
44
|
+
import { safeCallerSessionId } from "../caller-session-identity";
|
|
44
45
|
|
|
45
|
-
/**
|
|
46
|
+
/** Normalize caller aliases only on native egress; preserve ingress and fallback identity. */
|
|
46
47
|
export function withClaudeNativeSession(headers: Headers, provider: OcxProviderConfig, sessionId?: string): Headers {
|
|
47
|
-
|
|
48
|
-
|
|
48
|
+
// Empty values count as absent, matching materializeCodexUpstreamAuth, so first attempts and
|
|
49
|
+
// retries rebuilt from caller headers pick the same identity.
|
|
50
|
+
if (!isCanonicalOpenAiForwardProvider(provider) || headers.get("session_id")) return headers;
|
|
51
|
+
const alias = ["session-id", "thread-id"].find(name => headers.get(name));
|
|
52
|
+
const nativeSession = alias ? safeCallerSessionId(headers.get(alias)) : sessionId;
|
|
53
|
+
// A non-empty invalid alias suppresses weaker metadata identity.
|
|
54
|
+
if (!nativeSession) return headers;
|
|
49
55
|
const forwarded = new Headers(headers);
|
|
50
|
-
forwarded.set("session_id",
|
|
56
|
+
forwarded.set("session_id", nativeSession);
|
|
51
57
|
return forwarded;
|
|
52
58
|
}
|
|
53
59
|
|
|
@@ -57,6 +63,9 @@ export type ResponsesAuthResolution =
|
|
|
57
63
|
| { ok: false; response: Response };
|
|
58
64
|
|
|
59
65
|
|
|
66
|
+
export type CodexCredentialOwnershipOptions = Pick<HandleResponsesOptions,
|
|
67
|
+
"admission" | "callerDirectAuth" | "stripClaudeMainAuthForNoncanonicalForward" | "trustedClaudeMainAuth">;
|
|
68
|
+
|
|
60
69
|
/**
|
|
61
70
|
* The caller credential the final Codex auth resolution will be given, as far as the ROUTE
|
|
62
71
|
* decides it: a route change that may cross a credential domain drops the raw caller credential,
|
|
@@ -67,15 +76,53 @@ export type ResponsesAuthResolution =
|
|
|
67
76
|
* an HMAC of exactly this Authorization header. Two copies of this rule would put preview and
|
|
68
77
|
* final auth in different scopes the first time one of them changed.
|
|
69
78
|
*/
|
|
79
|
+
/**
|
|
80
|
+
* The stored native-main credential this process injected itself, when it is the bearer the
|
|
81
|
+
* resolution will see.
|
|
82
|
+
*
|
|
83
|
+
* `handleClaudeMessages` reads `~/.codex/auth.json` and attaches it so OpenAI-backed sidecars stay
|
|
84
|
+
* reachable on a translated turn. It is OUR credential, not the caller's -- the ingress says so
|
|
85
|
+
* where it sets `nativeCallerAuth` and `callerDirectAuth` to null for exactly this reason -- and
|
|
86
|
+
* every reader of the resolution's input headers has to agree about that, or one of them conclud
|
|
87
|
+
* the request owns a credential that in fact belongs to the Pool.
|
|
88
|
+
*/
|
|
89
|
+
export function injectedClaudeMainBearer(
|
|
90
|
+
route: Pick<RouteResult, "provider">,
|
|
91
|
+
options: CodexCredentialOwnershipOptions,
|
|
92
|
+
): { authorization: string; chatgptAccountId?: string } | undefined {
|
|
93
|
+
return options.stripClaudeMainAuthForNoncanonicalForward === true
|
|
94
|
+
&& isCanonicalOpenAiForwardProvider(route.provider)
|
|
95
|
+
? options.trustedClaudeMainAuth : undefined;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function validatedCallerDirectHeaders(
|
|
99
|
+
config: OcxConfig,
|
|
100
|
+
route: Pick<RouteResult, "provider">,
|
|
101
|
+
options: CodexCredentialOwnershipOptions,
|
|
102
|
+
): Headers | undefined {
|
|
103
|
+
if (!options.callerDirectAuth || !isCanonicalOpenAiForwardProvider(route.provider)) return undefined;
|
|
104
|
+
const headers = new Headers({ authorization: options.callerDirectAuth.authorization });
|
|
105
|
+
if (options.callerDirectAuth.chatgptAccountId) headers.set("chatgpt-account-id", options.callerDirectAuth.chatgptAccountId);
|
|
106
|
+
return captureCallerDirectAuth(headers, config) ? headers : undefined;
|
|
107
|
+
}
|
|
108
|
+
|
|
70
109
|
export function codexRouteCredentialDomainHeaders(
|
|
71
110
|
req: Request,
|
|
72
111
|
route: RouteResult,
|
|
73
112
|
options: HandleResponsesOptions,
|
|
74
113
|
credentialDomainWasRewritten: boolean,
|
|
114
|
+
config: OcxConfig,
|
|
75
115
|
): Headers {
|
|
76
|
-
const
|
|
77
|
-
|
|
78
|
-
|
|
116
|
+
const callerDirect = validatedCallerDirectHeaders(config, route, options);
|
|
117
|
+
if (callerDirect) {
|
|
118
|
+
const headers = new Headers(req.headers);
|
|
119
|
+
headers.set("authorization", callerDirect.get("authorization")!);
|
|
120
|
+
const accountId = callerDirect.get("chatgpt-account-id");
|
|
121
|
+
if (accountId) headers.set("chatgpt-account-id", accountId);
|
|
122
|
+
else headers.delete("chatgpt-account-id");
|
|
123
|
+
return headers;
|
|
124
|
+
}
|
|
125
|
+
const trustedClaudeMainForFinalRoute = injectedClaudeMainBearer(route, options);
|
|
79
126
|
if (trustedClaudeMainForFinalRoute) {
|
|
80
127
|
const claudeMainHeaders = new Headers(req.headers);
|
|
81
128
|
claudeMainHeaders.set("authorization", trustedClaudeMainForFinalRoute.authorization);
|
|
@@ -117,15 +164,25 @@ export function codexRouteCredentialDomainHeaders(
|
|
|
117
164
|
export function codexRouteCredentialOwnership(
|
|
118
165
|
authInputHeaders: Headers,
|
|
119
166
|
config: OcxConfig,
|
|
120
|
-
route: RouteResult,
|
|
121
|
-
options:
|
|
167
|
+
route: Pick<RouteResult, "provider" | "codexAccountMode">,
|
|
168
|
+
options: CodexCredentialOwnershipOptions,
|
|
122
169
|
): { substituteMainCredential: boolean; requestScopedMainCredential: boolean } {
|
|
123
170
|
const substituteMainCredential = options.admission?.source === "bearer"
|
|
124
171
|
&& (route.codexAccountMode !== undefined || isCanonicalOpenAiForwardProvider(route.provider));
|
|
125
172
|
return {
|
|
126
173
|
substituteMainCredential,
|
|
174
|
+
// A request-scoped main credential is one the CALLER owns: it exists for this request only,
|
|
175
|
+
// owns no Pool state, and must never be quarantined or refreshed on the Pool's behalf. The
|
|
176
|
+
// stored credential this process injected is the opposite of that in every respect, and
|
|
177
|
+
// `hasForwardableCodexBearer` cannot tell them apart -- it sees a Codex JWT with an account
|
|
178
|
+
// id either way. Counting ours as the caller's made the native main slot caller-owned on
|
|
179
|
+
// every translated Claude turn: `main` instead of `main-pool`, so an upstream 401 on a
|
|
180
|
+
// revoked session recorded no outcome, refreshed nothing, and retired nothing, and the next
|
|
181
|
+
// request sent the same dead token again.
|
|
127
182
|
requestScopedMainCredential: route.codexAccountMode !== undefined
|
|
128
183
|
&& !substituteMainCredential
|
|
184
|
+
&& (validatedCallerDirectHeaders(config, route, options) !== undefined
|
|
185
|
+
|| injectedClaudeMainBearer(route, options) === undefined)
|
|
129
186
|
&& hasForwardableCodexBearer(authInputHeaders, config),
|
|
130
187
|
};
|
|
131
188
|
}
|
|
@@ -149,6 +206,7 @@ export async function resolveResponsesCodexAuth(
|
|
|
149
206
|
route,
|
|
150
207
|
options,
|
|
151
208
|
credentialDomainWasRewritten,
|
|
209
|
+
config,
|
|
152
210
|
);
|
|
153
211
|
// A caller-auth transport that is not canonical OpenAI (keyless Cursor) consumes the
|
|
154
212
|
// caller's Authorization as its own upstream token. Keep that contract only for a clean
|
|
@@ -171,27 +229,6 @@ export async function resolveResponsesCodexAuth(
|
|
|
171
229
|
authInputHeaders = scoped;
|
|
172
230
|
}
|
|
173
231
|
}
|
|
174
|
-
// The caller's own Direct credential may cross an internal route change only to the
|
|
175
|
-
// canonical OpenAI transport, under a predicate deliberately STRICTER than plain
|
|
176
|
-
// unchanged-route Direct forwarding: a clean non-proxy bearer whose ChatGPT-domain
|
|
177
|
-
// marker is valid, with any explicit account header matching that marker. Unchanged
|
|
178
|
-
// routes keep their legacy rules; sidecar enrichment grants no primary authority.
|
|
179
|
-
if (options.callerDirectAuth && isCanonicalOpenAiForwardProvider(route.provider)) {
|
|
180
|
-
const directHeaders = new Headers({
|
|
181
|
-
authorization: options.callerDirectAuth.authorization,
|
|
182
|
-
...(options.callerDirectAuth.chatgptAccountId
|
|
183
|
-
? { "chatgpt-account-id": options.callerDirectAuth.chatgptAccountId } : {}),
|
|
184
|
-
});
|
|
185
|
-
if (captureCallerDirectAuth(directHeaders, config)) {
|
|
186
|
-
authInputHeaders = new Headers(authInputHeaders);
|
|
187
|
-
authInputHeaders.set("authorization", options.callerDirectAuth.authorization);
|
|
188
|
-
if (options.callerDirectAuth.chatgptAccountId) {
|
|
189
|
-
authInputHeaders.set("chatgpt-account-id", options.callerDirectAuth.chatgptAccountId);
|
|
190
|
-
} else {
|
|
191
|
-
authInputHeaders.delete("chatgpt-account-id");
|
|
192
|
-
}
|
|
193
|
-
}
|
|
194
|
-
}
|
|
195
232
|
// #1686: a caller that proved admission with a BEARER presented one of our own secrets.
|
|
196
233
|
// Refusing it here is what made the codex-cli `env_key` contract unusable against Direct.
|
|
197
234
|
// Admitting it is only safe because the stored main credential is substituted below, so
|
|
@@ -59,7 +59,7 @@ import {
|
|
|
59
59
|
import { ACCOUNT_GATED_NATIVE_OPENAI_MODELS } from "../../codex/catalog/native-models";
|
|
60
60
|
import { isRequestExecutionBudget } from "../../lib/request-execution-budget";
|
|
61
61
|
import type { SingleUseDispatchPermit } from "../../lib/request-execution-budget";
|
|
62
|
-
import {
|
|
62
|
+
import { codexRouteCredentialOwnership, type CodexCredentialOwnershipOptions } from "./core-auth";
|
|
63
63
|
import { bindRouteReasoningReplayScope } from "./core-replay";
|
|
64
64
|
import {
|
|
65
65
|
conversationStateBindingFromAuth,
|
|
@@ -360,7 +360,7 @@ export interface CodexPoolAccountRetryArgs {
|
|
|
360
360
|
route: Pick<RouteResult, "providerName" | "modelId" | "provider" | "staticPolicy">;
|
|
361
361
|
parsed: OcxParsedRequest;
|
|
362
362
|
logCtx: RequestLogContext;
|
|
363
|
-
options: {
|
|
363
|
+
options: CodexCredentialOwnershipOptions & {
|
|
364
364
|
admission?: DataPlaneAdmission;
|
|
365
365
|
codexAuthPolicy?: CodexAuthPolicyConfig;
|
|
366
366
|
visionDescribeTerminal?: boolean;
|
|
@@ -641,10 +641,13 @@ export async function retryCodexPoolOnAlternateAccount(
|
|
|
641
641
|
"pool",
|
|
642
642
|
{
|
|
643
643
|
excludeAccountId: firstAuthCtx.accountId,
|
|
644
|
+
signal: options.abortSignal,
|
|
644
645
|
admission: options.admission,
|
|
645
646
|
codexAuthPolicy: options.codexAuthPolicy,
|
|
646
647
|
modelId: route.modelId,
|
|
647
|
-
requestScopedMainCredential:
|
|
648
|
+
requestScopedMainCredential: codexRouteCredentialOwnership(callerAuthHeaders, config, {
|
|
649
|
+
provider: route.provider, codexAccountMode: "pool",
|
|
650
|
+
}, options).requestScopedMainCredential,
|
|
648
651
|
beginCodexAccountSelection: codexAccountSelectionForTurn(options.turnAdmissionLease),
|
|
649
652
|
resolveCodexModelEntitlements: entitlementResolver,
|
|
650
653
|
},
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { codexAccountModelRefusal, codexAccountModelRefusalHardStopCode, type CodexAccountModelRefusal } from "../../combos/failover";
|
|
1
2
|
import { parseRetryAfterMs } from "../../combos";
|
|
2
3
|
import type { ConsumedComboFailure, HandleResponsesOptions } from "./core-options";
|
|
3
4
|
import type { OcxUsage } from "../../types";
|
|
@@ -36,6 +37,7 @@ export async function consumeComboFailure(
|
|
|
36
37
|
const nonReplayable = isNonReplayableResponse(response);
|
|
37
38
|
const fallback = `Provider error ${response.status}`;
|
|
38
39
|
let classificationText = fallback;
|
|
40
|
+
let codexModelRefusal: CodexAccountModelRefusal | undefined;
|
|
39
41
|
let usage: OcxUsage | undefined;
|
|
40
42
|
let upstreamCode: string | undefined;
|
|
41
43
|
let upstreamMessage: string | undefined;
|
|
@@ -60,10 +62,11 @@ export async function consumeComboFailure(
|
|
|
60
62
|
&& isRateLimitOrQuotaFailureMessage(quotaMessage);
|
|
61
63
|
}
|
|
62
64
|
if (body.displaySafe && !body.truncated) {
|
|
65
|
+
if (response.status === 400) codexModelRefusal = codexAccountModelRefusal(response.status, body.text);
|
|
63
66
|
const normalized = normalizeUpstreamErrorText(body.text, fallback);
|
|
64
67
|
if (utf8Trusted || isCyberPolicyCode(normalized.code) || isCyberPolicyMessage(normalized.safeText)) {
|
|
65
68
|
classificationText = normalized.safeText;
|
|
66
|
-
upstreamCode = normalized.code;
|
|
69
|
+
upstreamCode = codexAccountModelRefusalHardStopCode(response.status, body.text) ?? normalized.code;
|
|
67
70
|
upstreamMessage = normalized.message;
|
|
68
71
|
upstreamType = normalized.type;
|
|
69
72
|
}
|
|
@@ -116,6 +119,7 @@ export async function consumeComboFailure(
|
|
|
116
119
|
response: failureResponse,
|
|
117
120
|
...(nonReplayable ? { nonReplayable: true } : {}),
|
|
118
121
|
classificationText,
|
|
122
|
+
...(codexModelRefusal !== undefined ? { codexModelRefusal } : {}),
|
|
119
123
|
...(normalizedUpstreamCode !== undefined ? { upstreamCode: normalizedUpstreamCode } : {}),
|
|
120
124
|
...(upstreamType !== undefined ? { upstreamType } : {}),
|
|
121
125
|
...(!cyberFailure && cooldownRetryAfter !== undefined ? { retryAfter: cooldownRetryAfter } : {}),
|