@bitkyc08/opencodex 2.83.0 → 2.86.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 +0 -4
- package/bin/ocx.mjs +61 -3
- package/gui/dist/assets/App-BjlttVLy.js +58 -0
- package/gui/dist/assets/Tray-DyR9yBee.js +1 -0
- package/gui/dist/assets/{index-BAbcj51J.css → index-BB6uK6tU.css} +1 -1
- package/gui/dist/assets/index-BMssP4k3.js +94 -0
- package/gui/dist/assets/quota-summary-D8M1bc_R.js +1 -0
- package/gui/dist/index.html +2 -2
- package/package.json +2 -3
- package/src/adapters/anthropic/beta-allowlist.ts +87 -8
- package/src/adapters/anthropic/client-identity.ts +1 -1
- package/src/adapters/anthropic/passthrough.ts +33 -4
- package/src/adapters/azure.ts +19 -1
- package/src/adapters/client-fingerprint.ts +24 -0
- package/src/adapters/coding-agent/protocol.ts +15 -8
- package/src/adapters/coding-agent/turn.ts +22 -9
- package/src/adapters/cursor/arg-codec.ts +17 -5
- package/src/adapters/cursor/catalog.ts +8 -0
- package/src/adapters/cursor/effort-map.ts +3 -0
- package/src/adapters/cursor/freeform-args.ts +75 -0
- package/src/adapters/cursor/protobuf-events.ts +36 -23
- package/src/adapters/google-antigravity-wire.ts +22 -1
- package/src/adapters/google.ts +2 -2
- package/src/adapters/ollama-native.ts +29 -1
- package/src/adapters/openai-chat/messages.ts +8 -4
- package/src/adapters/openai-chat/response-events.ts +59 -1
- package/src/adapters/openai-chat.ts +8 -5
- package/src/adapters/openai-responses/empty-assistant.ts +32 -0
- package/src/adapters/openai-responses/passthrough.ts +3 -0
- package/src/adapters/openai-responses/request-strips.ts +10 -2
- package/src/adapters/qoder/adapter.ts +56 -15
- package/src/advisor/consult.ts +220 -0
- package/src/advisor/context.ts +226 -0
- package/src/advisor/disclosure.ts +20 -0
- package/src/advisor/runtime.ts +305 -0
- package/src/advisor/settings.ts +135 -0
- package/src/advisor/state.ts +369 -0
- package/src/advisor/synthetic-tool.ts +33 -0
- package/src/bridge/errors.ts +24 -0
- package/src/bridge.ts +1 -1
- package/src/chat/inbound.ts +30 -6
- package/src/chat/outbound.ts +5 -0
- package/src/claude/context-windows.ts +54 -1
- package/src/claude/desktop-3p-library.ts +43 -3
- package/src/claude/desktop-3p.ts +22 -8
- package/src/claude/desktop-picker-profile.ts +11 -4
- package/src/claude/desktop-profile.ts +10 -1
- package/src/claude/desktop-remote-store-io.ts +5 -3
- package/src/claude/inbound.ts +121 -50
- package/src/claude/intercept/connect-proxy.ts +10 -3
- package/src/claude/intercept/local-ca.ts +8 -1
- package/src/claude/intercept/picker-ca-persistence.ts +3 -1
- package/src/claude/intercept/picker-listener.ts +49 -1
- package/src/claude/intercept/settings-no-proxy.ts +146 -0
- package/src/claude/intercept/settings.ts +101 -9
- package/src/claude/model-default-settings.ts +154 -0
- package/src/claude/model-info.ts +7 -7
- package/src/claude/outbound.ts +13 -6
- package/src/cli/account-api.ts +65 -0
- package/src/cli/account-auth.ts +26 -2
- package/src/cli/account.ts +41 -5
- package/src/cli/advisor.ts +151 -0
- package/src/cli/auxiliary-listener-preflight.ts +37 -0
- package/src/cli/capabilities-access-remote.ts +8 -0
- package/src/cli/capabilities-agents-routing.ts +18 -0
- package/src/cli/capabilities-base.ts +14 -2
- package/src/cli/catalog-command-result.ts +2 -1
- package/src/cli/chatgpt-command.ts +35 -14
- package/src/cli/claude.ts +60 -11
- package/src/cli/config-command.ts +8 -2
- package/src/cli/debug.ts +8 -0
- package/src/cli/dispatch.ts +21 -4
- package/src/cli/doctor.ts +9 -0
- package/src/cli/ensure-desktop-cold-start.ts +329 -0
- package/src/cli/export-command.ts +15 -2
- package/src/cli/help.ts +2 -1
- package/src/cli/hub-disable.ts +99 -0
- package/src/cli/hub.ts +4 -2
- package/src/cli/index.ts +26 -8
- package/src/cli/integration-plan-dto.ts +2 -0
- package/src/cli/link-child.ts +104 -0
- package/src/cli/link-enrollment.ts +96 -0
- package/src/cli/link.ts +39 -24
- package/src/cli/observe-stream.ts +6 -1
- package/src/cli/observe.ts +10 -2
- package/src/cli/provider.ts +12 -0
- package/src/cli/registry.ts +20 -7
- package/src/cli/restart-scope.ts +3 -1
- package/src/cli/runtime-api.ts +27 -2
- package/src/cli/status.ts +104 -20
- package/src/client/connect.ts +39 -4
- package/src/client/hub-client.ts +14 -7
- package/src/client/hub-state.ts +25 -14
- package/src/client/link-home.ts +248 -0
- package/src/client/link-management-relay.ts +61 -0
- package/src/client/link-relay.ts +6 -1
- package/src/client/link-status.ts +14 -0
- package/src/client/machine-api.ts +14 -7
- package/src/client/machine-listener.ts +29 -4
- package/src/clients/config-export/contracts.ts +1 -0
- package/src/clients/config-export/model-metadata.ts +3 -2
- package/src/clients/config-export/qoder.ts +93 -0
- package/src/clients/config-export.ts +59 -16
- package/src/clients/omo-role-models.ts +6 -3
- package/src/codex/agent-role-models.ts +26 -8
- package/src/codex/auth-api/account-list.ts +17 -2
- package/src/codex/auth-api/account-selection.ts +17 -7
- package/src/codex/auth-api/pool-mode-gate.ts +5 -2
- package/src/codex/auth-api/routes.ts +5 -1
- package/src/codex/auth-context.ts +79 -5
- package/src/codex/autostart-health.ts +7 -1
- package/src/codex/catalog/build-entries.ts +11 -2
- package/src/codex/catalog/bundled.ts +5 -1
- package/src/codex/catalog/configured-tiers.ts +50 -0
- package/src/codex/catalog/effort.ts +6 -3
- package/src/codex/catalog/metadata.ts +4 -2
- package/src/codex/catalog/model-hints.ts +21 -3
- package/src/codex/catalog/native-models.ts +16 -2
- package/src/codex/catalog/native-ultrafast.ts +106 -0
- package/src/codex/catalog/parsing.ts +5 -0
- package/src/codex/catalog/provider-models.ts +4 -0
- package/src/codex/catalog/retained-sync.ts +2 -0
- package/src/codex/catalog/routed-gather.ts +22 -4
- package/src/codex/catalog/vendor-context.ts +82 -0
- package/src/codex/config-write-lock.ts +40 -2
- package/src/codex/convergence.ts +2 -0
- package/src/codex/desktop-app/types.ts +2 -0
- package/src/codex/desktop-app/windows.ts +29 -16
- package/src/codex/desktop-app-restart.ts +4 -2
- package/src/codex/desktop-compatibility/certificate-service.ts +99 -0
- package/src/codex/desktop-compatibility/certificate-store.ts +193 -0
- package/src/codex/desktop-compatibility/connection-store.ts +87 -0
- package/src/codex/desktop-compatibility/installed-build.ts +55 -0
- package/src/codex/desktop-compatibility/json-body.ts +29 -0
- package/src/codex/desktop-compatibility/native-identity.ts +62 -0
- package/src/codex/desktop-compatibility/relay-listener.ts +93 -0
- package/src/codex/desktop-compatibility/routing-binding.ts +10 -0
- package/src/codex/desktop-compatibility/routing-preflight.ts +69 -0
- package/src/codex/desktop-compatibility/runtime-ownership.ts +28 -0
- package/src/codex/desktop-compatibility/runtime.ts +173 -0
- package/src/codex/desktop-compatibility/service.ts +30 -0
- package/src/codex/desktop-compatibility/startup-settings.ts +50 -0
- package/src/codex/desktop-compatibility/usage-activation.ts +86 -0
- package/src/codex/desktop-compatibility/usage-controlled-fetch.ts +59 -0
- package/src/codex/desktop-compatibility/usage-controller.ts +107 -0
- package/src/codex/desktop-compatibility/usage-policy.ts +59 -0
- package/src/codex/desktop-compatibility/usage-refresh.ts +33 -0
- package/src/codex/desktop-compatibility/usage-sse-controller.ts +86 -0
- package/src/codex/desktop-compatibility/windows-activation-source.ts +44 -0
- package/src/codex/desktop-compatibility/windows-certificate-trust.ts +81 -0
- package/src/codex/desktop-compatibility/windows-key-protection.ts +59 -0
- package/src/codex/desktop-compatibility/windows-package-command.ts +86 -0
- package/src/codex/desktop-compatibility/windows-package-launch.ts +28 -0
- package/src/codex/history-provider.ts +1 -1
- package/src/codex/inject/config-write-section.ts +4 -1
- package/src/codex/inject/remove.ts +18 -6
- package/src/codex/journal.ts +16 -1
- package/src/codex/main-account.ts +99 -15
- package/src/codex/native-main-refresh-mode.ts +217 -0
- package/src/codex/native-profile-startup.ts +4 -1
- package/src/codex/prompt-journal.ts +20 -2
- package/src/codex/prompt-layers/encoding.ts +49 -0
- package/src/codex/prompt-layers/import-source.ts +30 -0
- package/src/codex/prompt-layers/toml-edit.ts +269 -37
- package/src/codex/prompt-layers/toml-read.ts +52 -4
- package/src/codex/prompt-layers.ts +383 -20
- package/src/codex/prompt-lock-claim.ts +13 -3
- package/src/codex/prompt-lock-evidence.ts +3 -3
- package/src/codex/prompt-lock-owner.ts +14 -6
- package/src/codex/prompt-text-probe.ts +34 -5
- package/src/codex/quota.ts +13 -6
- package/src/codex/role-auto-assign.ts +2 -5
- package/src/codex/routing/active-account.ts +21 -0
- package/src/codex/routing/health-store.ts +1 -1
- package/src/codex/routing/pool-recovery.ts +79 -0
- package/src/codex/routing.ts +1 -1
- package/src/codex/runtime.ts +196 -8
- package/src/codex/shim-diagnostics.ts +6 -0
- package/src/codex/shim-overlay.ts +15 -6
- package/src/codex/shim.ts +1 -1
- package/src/combos/failover.ts +3 -0
- package/src/config/api-key-reconcile.ts +34 -0
- package/src/config/diagnostics.ts +18 -1
- package/src/config/feature-flags.ts +5 -0
- package/src/config/live-reconcile.ts +56 -29
- package/src/config/live-topology.ts +29 -0
- package/src/config/load-degrade.ts +8 -2
- package/src/config/paths.ts +13 -3
- package/src/config/salvage.ts +3 -1
- package/src/config/schema/config-schema.ts +19 -0
- package/src/config/schema/desktop-compatibility.ts +9 -0
- package/src/config/schema/leaf-validators.ts +1 -0
- package/src/generated/compatibility-version.json +713 -285
- package/src/generated/model-metadata.ts +1 -1
- package/src/grok/grpc-web.ts +23 -1
- package/src/grok/reset-coupon-ledger.ts +75 -5
- package/src/grok/reset-coupons.ts +4 -2
- package/src/integrations/catalog-refresh.ts +1 -1
- package/src/integrations/config-io.ts +20 -2
- package/src/integrations/journal.ts +15 -1
- package/src/integrations/mutation-plan.ts +14 -7
- package/src/integrations/registry.ts +52 -11
- package/src/integrations/state.ts +19 -7
- package/src/integrations/store.ts +3 -0
- package/src/integrations/target.ts +42 -5
- package/src/integrations/writer.ts +24 -7
- package/src/lib/advisor-activation.ts +16 -0
- package/src/lib/app-owned-memory-stores.ts +4 -0
- package/src/lib/app-owned-memory.ts +11 -0
- package/src/lib/background-work.ts +65 -0
- package/src/lib/bounded-file-read.ts +58 -0
- package/src/lib/bun-path-runtime.d.mts +10 -1
- package/src/lib/bun-path-runtime.mjs +62 -3
- package/src/lib/desktop-proxy-route.ts +30 -0
- package/src/lib/desktop-upstream-tunnel.ts +107 -0
- package/src/lib/errors.ts +7 -0
- package/src/lib/json-byte-size.ts +31 -0
- package/src/lib/local-internal-call-capability.ts +55 -0
- package/src/lib/optional-shutdown-hooks.ts +35 -0
- package/src/lib/outbound-credential-registry.ts +210 -0
- package/src/lib/package-tree-integrity.ts +2 -2
- package/src/lib/passive-read.ts +35 -0
- package/src/lib/shadow-call.ts +44 -5
- package/src/lib/socks5-fetch.ts +48 -71
- package/src/lib/socks5-handshake.ts +112 -0
- package/src/lib/translator-budget.ts +7 -1
- package/src/link/codex-settings.ts +155 -0
- package/src/link/management-routes.ts +89 -0
- package/src/link/remote-probe.ts +38 -0
- package/src/link/ssh-argv.ts +26 -5
- package/src/link/ssh-runner.ts +58 -27
- package/src/link/status-projection.ts +22 -4
- package/src/link/supervisor.ts +144 -49
- package/src/link/tunnel-state.ts +11 -4
- package/src/oauth/anthropic-continuity.ts +4 -3
- package/src/oauth/anthropic-identity.ts +13 -2
- package/src/oauth/anthropic-model-quota.ts +13 -1
- package/src/oauth/anthropic-routing.ts +230 -23
- package/src/oauth/anthropic.ts +18 -4
- package/src/oauth/generic-account-failover.ts +29 -5
- package/src/oauth/google-antigravity.ts +24 -7
- package/src/oauth/index.ts +28 -17
- package/src/oauth/kiro-account-identity.ts +106 -0
- package/src/oauth/kiro-device-login.ts +2 -1
- package/src/oauth/kiro.ts +55 -6
- package/src/oauth/plan.ts +29 -0
- package/src/oauth/store.ts +40 -3
- package/src/oauth/types.ts +2 -0
- package/src/plugins/loader.ts +11 -0
- package/src/protocols/codecs/chat-ir.ts +467 -0
- package/src/protocols/codecs/chat.ts +10 -1
- package/src/protocols/codecs/messages-ir.ts +500 -0
- package/src/protocols/features.ts +10 -2
- package/src/protocols/path.ts +11 -1
- package/src/protocols/plan-snapshot.ts +39 -16
- package/src/protocols/plan.ts +9 -1
- package/src/protocols/settings.ts +8 -0
- package/src/providers/anthropic-billing-ownership.ts +74 -0
- package/src/providers/anthropic-billing-recovery.ts +31 -0
- package/src/providers/anthropic-billing-refusal.ts +55 -0
- package/src/providers/anthropic-reset-admission.ts +62 -0
- package/src/providers/anthropic-reset-grant-ledger.ts +28 -6
- package/src/providers/antigravity-account-roster.ts +102 -0
- package/src/providers/antigravity-models.ts +85 -7
- package/src/providers/api-key-resolve.ts +17 -2
- package/src/providers/key-failover.ts +52 -5
- package/src/providers/model-rename-fields.ts +2 -0
- package/src/providers/model-rename-migration.ts +5 -3
- package/src/providers/model-rename-startup.ts +5 -3
- package/src/providers/quota/account-cache.ts +11 -0
- package/src/providers/quota/anthropic-account-quota.ts +4 -2
- package/src/providers/quota/report-cache.ts +14 -0
- package/src/providers/quota/vendor-probes-oauth.ts +3 -3
- package/src/providers/quota.ts +4 -1
- package/src/providers/registry/entries-core.ts +21 -20
- package/src/providers/registry/entries-extended.ts +19 -1
- package/src/providers/registry/model-seeds.ts +17 -0
- package/src/providers/request-pacing.ts +9 -0
- package/src/providers/stale-no-vision-seed-migration.ts +72 -0
- package/src/responses/compaction.ts +14 -0
- package/src/responses/parser.ts +37 -20
- package/src/responses/state.ts +32 -29
- package/src/routing/history/build-file.ts +92 -0
- package/src/routing/history/indexer.ts +248 -216
- package/src/routing/history/ingest.ts +238 -0
- package/src/routing/history/rebuild-job.ts +111 -0
- package/src/routing/history/rebuild-protocol.ts +15 -0
- package/src/routing/history/rebuild-worker.ts +34 -0
- package/src/server/admission-model-scope.ts +4 -1
- package/src/server/antigravity-direct-eligibility.ts +208 -0
- package/src/server/auth-cors.ts +46 -0
- package/src/server/chat-completions.ts +75 -9
- package/src/server/claude-messages.ts +110 -20
- package/src/server/configured-credentials.ts +194 -0
- package/src/server/index/desktop-compatibility-startup.ts +56 -0
- package/src/server/index/link-management.ts +58 -0
- package/src/server/index/optional-listeners.ts +5 -1
- package/src/server/index/serve-options.ts +15 -2
- package/src/server/index/startup-warnings.ts +9 -0
- package/src/server/index/websocket-handler.ts +9 -0
- package/src/server/index.ts +13 -10
- package/src/server/inference/client-encoder-delivery.ts +6 -5
- package/src/server/lifecycle.ts +2 -1
- package/src/server/login-shell-path.ts +146 -0
- package/src/server/management/advisor-routes.ts +191 -0
- package/src/server/management/agent-settings-routes.ts +50 -14
- package/src/server/management/anthropic-reset-grant-routes.ts +17 -1
- package/src/server/management/codex-agent-role-routes.ts +19 -6
- package/src/server/management/codex-prompt-routes.ts +136 -5
- package/src/server/management/codex-role-auto-assign.ts +1 -1
- package/src/server/management/companion-routes.ts +4 -0
- package/src/server/management/config-routes.ts +2 -2
- package/src/server/management/context.ts +4 -0
- package/src/server/management/desktop-compatibility-routes.ts +46 -0
- package/src/server/management/desktop-compatibility-runtime-routes.ts +46 -0
- package/src/server/management/desktop-compatibility-settings-routes.ts +27 -0
- package/src/server/management/grok-coupon-routes.ts +78 -27
- package/src/server/management/link-enrollment.ts +192 -0
- package/src/server/management/link-route-state.ts +185 -0
- package/src/server/management/link-routes.ts +82 -402
- package/src/server/management/model-routes.ts +2 -2
- package/src/server/management/model-rows.ts +3 -1
- package/src/server/management/protocol-settings-patch.ts +1 -0
- package/src/server/management/provider-overwrite-carry.ts +10 -0
- package/src/server/management/provider-routes.ts +1 -0
- package/src/server/management/route-registry.ts +16 -3
- package/src/server/management/shared.ts +3 -0
- package/src/server/management/sibling-guard.ts +1 -0
- package/src/server/management-api.ts +9 -1
- package/src/server/messages-antigravity-direct.ts +195 -0
- package/src/server/messages-native.ts +73 -43
- package/src/server/relay-eager.ts +2 -0
- package/src/server/request-log-filter.ts +6 -1
- package/src/server/request-log-terminal-status.ts +63 -6
- package/src/server/request-log.ts +7 -0
- package/src/server/responses/adapter-continuation.ts +35 -18
- package/src/server/responses/adapter-delivery.ts +24 -2
- package/src/server/responses/adapter-dispatch.ts +261 -81
- package/src/server/responses/advisor-plan-slot.ts +27 -0
- package/src/server/responses/advisor-slot.ts +336 -0
- package/src/server/responses/code-mode-collab-surface.ts +313 -0
- package/src/server/responses/codex-ws-exchange.ts +147 -6
- package/src/server/responses/codex-ws-wire.ts +203 -0
- package/src/server/responses/collaboration.ts +36 -7
- package/src/server/responses/compact.ts +126 -48
- package/src/server/responses/context-overflow.ts +13 -1
- package/src/server/responses/core-codex-account.ts +105 -1
- package/src/server/responses/core-combo-failure.ts +24 -1
- package/src/server/responses/core-combo.ts +28 -2
- package/src/server/responses/core-errors.ts +29 -1
- package/src/server/responses/core-lifetime.ts +2 -0
- package/src/server/responses/core-normalize.ts +11 -0
- package/src/server/responses/core-options.ts +48 -0
- package/src/server/responses/core-replay.ts +7 -4
- package/src/server/responses/core.ts +1 -1
- package/src/server/responses/deferred-collab-surface.ts +158 -0
- package/src/server/responses/deferred-send-reporter.ts +25 -0
- package/src/server/responses/fetch-helpers.ts +12 -1
- package/src/server/responses/passthrough-delivery.ts +20 -11
- package/src/server/responses/passthrough-dispatch.ts +29 -7
- package/src/server/responses/request-prepare.ts +74 -9
- package/src/server/responses/request-transport.ts +22 -8
- package/src/server/responses/run-turn-execution.ts +70 -8
- package/src/server/responses/sidecar-execution.ts +89 -5
- package/src/server/responses/terminal-guard.ts +2 -1
- package/src/server/responses/ws-upstream.ts +9 -1
- package/src/server/responses-undeclared-tool-guard.ts +30 -9
- package/src/server/startup-action-control.ts +38 -4
- package/src/server/startup-health-cache.ts +5 -0
- package/src/server/system-env-shell.ts +7 -2
- package/src/server/system-env.ts +11 -1
- package/src/service/cli.ts +5 -1
- package/src/service/desktop-startup.ts +161 -34
- package/src/service/desktop-supervision.d.mts +27 -1
- package/src/service/desktop-supervision.mjs +111 -5
- package/src/service/health-sentinel-launch.ts +176 -0
- package/src/service/health-sentinel-main.ts +466 -0
- package/src/service/health-sentinel-policy.ts +241 -0
- package/src/service/health-sentinel-probe.ts +71 -0
- package/src/service/health-sentinel-windows.ts +87 -0
- package/src/service/linux-linger.ts +20 -0
- package/src/service/logging.ts +72 -0
- package/src/tray/windows-tray.ps1 +43 -24
- package/src/types/config.ts +68 -6
- package/src/types/provider.ts +7 -0
- package/src/types/request.ts +15 -0
- package/src/types/tools.ts +2 -0
- package/src/update/async-check.ts +1 -1
- package/src/update/badge.ts +2 -2
- package/src/update/index.ts +88 -15
- package/src/update/job-supervised-restart.ts +107 -0
- package/src/update/job.ts +32 -32
- package/src/update/notify.ts +1 -1
- package/src/update/refresh-scheduler.ts +2 -2
- package/src/update/supervised-recovery.d.mts +27 -0
- package/src/update/supervised-recovery.mjs +157 -0
- package/src/usage/cache-diagnostic.ts +2 -0
- package/src/usage/expected-prices.ts +16 -0
- package/src/usage/log.ts +19 -1
- package/src/web-search/gemini-executor.ts +2 -2
- package/assets/sponsors/packycode.png +0 -0
- package/gui/dist/assets/App-Ba8uG696.js +0 -54
- package/gui/dist/assets/Tray-IImbNPRy.js +0 -1
- package/gui/dist/assets/index-BHRZdJuv.js +0 -94
- package/gui/dist/assets/quota-summary-DiJO6sYY.js +0 -1
|
@@ -200,6 +200,7 @@ import {
|
|
|
200
200
|
usesCodexForwardPoolAuth,
|
|
201
201
|
} from "./core";
|
|
202
202
|
import { fetchWithHeaderTimeout, providerFetch, safeHostLabel, safeOriginLabel } from "./fetch-helpers";
|
|
203
|
+
import { shouldRetryCodexPoolAccountQuota, shouldRetryCodexPoolAccountTransient } from "./core-codex-account";
|
|
203
204
|
import { mapCodexAuthContextErrorToResponse, nativeMainRefreshFailureResponse } from "./codex-auth-error";
|
|
204
205
|
import { linkRequestSessionLane, sessionLaneIdFromRequest } from "../request-log-conversation";
|
|
205
206
|
import { recallComboForLane } from "./combo-session-recall";
|
|
@@ -1048,7 +1049,11 @@ export async function handleResponsesCompact(
|
|
|
1048
1049
|
targetKey: compactTargetKey,
|
|
1049
1050
|
});
|
|
1050
1051
|
if (!decision.allowed) return Promise.reject(new SendBudgetExhaustedError(safeHostLabel(compactUrl)));
|
|
1051
|
-
if (!decision.permit.use())
|
|
1052
|
+
if (!decision.permit.use()) {
|
|
1053
|
+
// Nothing was sent, so the reservation goes back rather than holding a send against the request.
|
|
1054
|
+
decision.permit.release();
|
|
1055
|
+
return Promise.reject(new SendBudgetExhaustedError(safeHostLabel(compactUrl)));
|
|
1056
|
+
}
|
|
1052
1057
|
return doFetch();
|
|
1053
1058
|
};
|
|
1054
1059
|
const sendCompactAttempt = (
|
|
@@ -1056,6 +1061,7 @@ export async function handleResponsesCompact(
|
|
|
1056
1061
|
sendHeaders: Headers,
|
|
1057
1062
|
recovery: "normal" | "single",
|
|
1058
1063
|
sendAuthCtx: CodexAuthContext,
|
|
1064
|
+
onPhysicalDispatch?: () => void,
|
|
1059
1065
|
): Promise<Response> => {
|
|
1060
1066
|
const doFetch = (upstreamRecovery?: UpstreamSendRecovery) => fetchWithHeaderTimeout(
|
|
1061
1067
|
compactUrl,
|
|
@@ -1072,6 +1078,7 @@ export async function handleResponsesCompact(
|
|
|
1072
1078
|
modelId: route.modelId,
|
|
1073
1079
|
beforeDispatch: isCanonicalOpenAiForwardProvider(sendProvider)
|
|
1074
1080
|
? createCodexAuthDispatchGuard(sendAuthCtx, config, selectedModelId, admission) : undefined,
|
|
1081
|
+
...(onPhysicalDispatch ? { onPhysicalDispatch } : {}),
|
|
1075
1082
|
}),
|
|
1076
1083
|
// Every credential-bearing forward send gets manual redirects, not only
|
|
1077
1084
|
// pool sends: direct mode carries the caller's credential too (#914).
|
|
@@ -1217,6 +1224,53 @@ export async function handleResponsesCompact(
|
|
|
1217
1224
|
}
|
|
1218
1225
|
}
|
|
1219
1226
|
|
|
1227
|
+
// The settle for an alternate send that threw, shared by the quota and transient moves. By the
|
|
1228
|
+
// time it runs the request already names the alternate (outcomeCtx), so health goes to it.
|
|
1229
|
+
const failedAlternateCompactSend = (err: unknown): Response => {
|
|
1230
|
+
if (req.signal.aborted) {
|
|
1231
|
+
recordCompactPoolOutcome(outcomeCtx, 499);
|
|
1232
|
+
return formatErrorResponse(499, "client_cancelled", "Client cancelled compact request");
|
|
1233
|
+
}
|
|
1234
|
+
const localRefusal = localDispatchRefusal(err);
|
|
1235
|
+
if (localRefusal) return localRefusal;
|
|
1236
|
+
const outcome = classifyTransportFailureKind(err);
|
|
1237
|
+
// Host-level evidence stands regardless of pool membership (#914 review).
|
|
1238
|
+
if (outcome === "connect_neutral") {
|
|
1239
|
+
if (compactHostCircuitEnabled) {
|
|
1240
|
+
recordUpstreamHostFailure(actualCompactHostKey, {
|
|
1241
|
+
code: transportErrorCode(err),
|
|
1242
|
+
threshold: config.upstreamHostCircuitThreshold,
|
|
1243
|
+
lease: compactHostAdmissionLease,
|
|
1244
|
+
});
|
|
1245
|
+
} else {
|
|
1246
|
+
recordUpstreamHostFailure(actualCompactHostKey, { code: transportErrorCode(err) });
|
|
1247
|
+
}
|
|
1248
|
+
} else {
|
|
1249
|
+
releaseUpstreamHostAdmission(compactHostAdmissionLease);
|
|
1250
|
+
}
|
|
1251
|
+
compactHostAdmissionLease = null;
|
|
1252
|
+
recordCompactPoolOutcome(outcomeCtx, outcome);
|
|
1253
|
+
return formatErrorResponse(502, "upstream_error", "Failed to connect to compact upstream");
|
|
1254
|
+
};
|
|
1255
|
+
const scrubCompactStateForAlternate = (alternateAuthCtx: CodexAuthContext): void => {
|
|
1256
|
+
const binding = conversationStateBindingFromAuth(
|
|
1257
|
+
alternateAuthCtx,
|
|
1258
|
+
(authCtx.kind === "pool" || authCtx.kind === "main-pool")
|
|
1259
|
+
? authCtx.affinityKey
|
|
1260
|
+
: codexPoolAffinityKey(req.headers),
|
|
1261
|
+
);
|
|
1262
|
+
if (!binding) return;
|
|
1263
|
+
for (const body of [raw, compactBody]) {
|
|
1264
|
+
applyAccountChangeConversationStateScrub({
|
|
1265
|
+
body,
|
|
1266
|
+
bindingKey: binding.bindingKey,
|
|
1267
|
+
servingAccountId: binding.accountId,
|
|
1268
|
+
priorAccountId: authCtx.accountId,
|
|
1269
|
+
logCtx,
|
|
1270
|
+
});
|
|
1271
|
+
}
|
|
1272
|
+
};
|
|
1273
|
+
|
|
1220
1274
|
// Bounded same-request alternate: the regular /v1/responses path already does this
|
|
1221
1275
|
// (core.ts:319-423) and recognizes exactly 429/402. Without it a pool rejection
|
|
1222
1276
|
// surfaces to the client, which retries the compact task OUTSIDE the logical request
|
|
@@ -1317,57 +1371,81 @@ export async function handleResponsesCompact(
|
|
|
1317
1371
|
await upstream.body?.cancel().catch(() => undefined);
|
|
1318
1372
|
outcomeCtx = alternate.authCtx;
|
|
1319
1373
|
logCtx.accountLogLabel = codexAuthContextLogLabel(alternate.authCtx, config);
|
|
1320
|
-
|
|
1321
|
-
const binding = conversationStateBindingFromAuth(
|
|
1322
|
-
alternate.authCtx,
|
|
1323
|
-
(authCtx.kind === "pool" || authCtx.kind === "main-pool")
|
|
1324
|
-
? authCtx.affinityKey
|
|
1325
|
-
: codexPoolAffinityKey(req.headers),
|
|
1326
|
-
);
|
|
1327
|
-
if (binding) {
|
|
1328
|
-
applyAccountChangeConversationStateScrub({
|
|
1329
|
-
body: raw,
|
|
1330
|
-
bindingKey: binding.bindingKey,
|
|
1331
|
-
servingAccountId: binding.accountId,
|
|
1332
|
-
priorAccountId: authCtx.accountId,
|
|
1333
|
-
logCtx,
|
|
1334
|
-
});
|
|
1335
|
-
applyAccountChangeConversationStateScrub({
|
|
1336
|
-
body: compactBody,
|
|
1337
|
-
bindingKey: binding.bindingKey,
|
|
1338
|
-
servingAccountId: binding.accountId,
|
|
1339
|
-
priorAccountId: authCtx.accountId,
|
|
1340
|
-
logCtx,
|
|
1341
|
-
});
|
|
1342
|
-
}
|
|
1343
|
-
}
|
|
1374
|
+
scrubCompactStateForAlternate(alternate.authCtx);
|
|
1344
1375
|
try {
|
|
1345
1376
|
upstream = await sendCompactAttempt(alternate.provider, alternate.headers, "single", alternate.authCtx);
|
|
1346
1377
|
} catch (err) {
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1378
|
+
return failedAlternateCompactSend(err);
|
|
1379
|
+
}
|
|
1380
|
+
}
|
|
1381
|
+
}
|
|
1382
|
+
// A transient 5xx that survived the same-account ladder above (#6969). The regular path moves
|
|
1383
|
+
// it to another Pool account; compact used to move only on 429/402, so the client retried the
|
|
1384
|
+
// compact task outside this request while another account sat idle. One bounded alternate send,
|
|
1385
|
+
// the same guards as the quota move, and a 5xx whose body confirms quota keeps today's path.
|
|
1386
|
+
if (
|
|
1387
|
+
shouldRetryCodexPoolAccountTransient(upstream)
|
|
1388
|
+
&& !storedPool401ReplayAttempted
|
|
1389
|
+
&& usesCodexForwardPoolAuth(authCtx, route.provider)
|
|
1390
|
+
&& !authCtx.fixedAccount
|
|
1391
|
+
&& route.codexAccountMode
|
|
1392
|
+
&& outcomeCtx === authCtx
|
|
1393
|
+
&& !req.signal.aborted
|
|
1394
|
+
&& !conversationCarriesUploadedFiles(raw)
|
|
1395
|
+
&& !await shouldRetryCodexPoolAccountQuota(upstream, req.signal)
|
|
1396
|
+
) {
|
|
1397
|
+
const firstStatus = upstream.status;
|
|
1398
|
+
const firstRetryAfter = upstream.headers.get("retry-after");
|
|
1399
|
+
const firstResetAt = [
|
|
1400
|
+
upstream.headers.get("x-codex-primary-reset-at"),
|
|
1401
|
+
upstream.headers.get("x-codex-secondary-reset-at"),
|
|
1402
|
+
upstream.headers.get("x-codex-tertiary-reset-at"),
|
|
1403
|
+
].filter(Boolean);
|
|
1404
|
+
const alternate = await resolveAlternateCompactContext({
|
|
1405
|
+
req,
|
|
1406
|
+
admission,
|
|
1407
|
+
config,
|
|
1408
|
+
route,
|
|
1409
|
+
selectedModelId,
|
|
1410
|
+
excludeAccountId: authCtx.accountId,
|
|
1411
|
+
turnAdmissionLease,
|
|
1412
|
+
});
|
|
1413
|
+
if (alternate && req.signal.aborted) {
|
|
1414
|
+
releaseCodexAuthContextProbeLease(alternate.authCtx);
|
|
1415
|
+
recordCompactPoolOutcome(outcomeCtx, 499);
|
|
1416
|
+
void upstream.body?.cancel(req.signal.reason).catch(() => undefined);
|
|
1417
|
+
return formatErrorResponse(499, "client_cancelled", "Client cancelled compact request");
|
|
1418
|
+
}
|
|
1419
|
+
if (alternate) {
|
|
1420
|
+
const firstLabel = logCtx.accountLogLabel;
|
|
1421
|
+
// B's identity before B's admission, so every budget and spend check runs against B. A's
|
|
1422
|
+
// response stays intact until B is actually sent.
|
|
1423
|
+
logCtx.accountLogLabel = codexAuthContextLogLabel(alternate.authCtx, config);
|
|
1424
|
+
scrubCompactStateForAlternate(alternate.authCtx);
|
|
1425
|
+
const recordFirstMove = () => {
|
|
1426
|
+
recordCompactPoolOutcome(authCtx, firstStatus, { retryAfter: firstRetryAfter, resetAt: firstResetAt });
|
|
1427
|
+
void upstream.body?.cancel().catch(() => undefined);
|
|
1428
|
+
outcomeCtx = alternate.authCtx;
|
|
1429
|
+
};
|
|
1430
|
+
let alternateResponse: Response | undefined;
|
|
1431
|
+
// Whether B's send actually left this process. Every local refusal before that point -- a
|
|
1432
|
+
// spent budget, a dispatch guard such as disabled credits, a pacing refusal -- keeps A.
|
|
1433
|
+
let alternateDispatched = false;
|
|
1434
|
+
try {
|
|
1435
|
+
alternateResponse = await sendCompactAttempt(alternate.provider, alternate.headers, "single", alternate.authCtx,
|
|
1436
|
+
() => { alternateDispatched = true; });
|
|
1437
|
+
} catch (err) {
|
|
1438
|
+
if (alternateDispatched) {
|
|
1439
|
+
recordFirstMove();
|
|
1440
|
+
return failedAlternateCompactSend(err);
|
|
1367
1441
|
}
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1442
|
+
// Refused before anything left: A's answer and identity stand, recorded once below.
|
|
1443
|
+
logCtx.accountLogLabel = firstLabel;
|
|
1444
|
+
releaseCodexAuthContextProbeLease(alternate.authCtx);
|
|
1445
|
+
}
|
|
1446
|
+
if (alternateResponse) {
|
|
1447
|
+
recordFirstMove();
|
|
1448
|
+
upstream = alternateResponse;
|
|
1371
1449
|
}
|
|
1372
1450
|
}
|
|
1373
1451
|
}
|
|
@@ -1,7 +1,19 @@
|
|
|
1
|
-
import { bridgeToResponsesSSE } from "../../bridge";
|
|
1
|
+
import { bridgeToResponsesSSE, formatErrorResponse } from "../../bridge";
|
|
2
|
+
import { OLLAMA_LATE_ATTRIBUTION_LIMIT_MESSAGE } from "../../adapters/ollama-native";
|
|
2
3
|
import type { TranslatorBudget } from "../../lib/translator-budget";
|
|
3
4
|
import type { AdapterEvent } from "../../types";
|
|
4
5
|
|
|
6
|
+
/**
|
|
7
|
+
* The Ollama late-attribution refusal as a client response: the existing translator-budget
|
|
8
|
+
* wire shape (413 request_too_large) with only the proxy-owned constant as its message.
|
|
9
|
+
* It is deliberately not a context overflow: compacting on the same route cannot shrink it.
|
|
10
|
+
*/
|
|
11
|
+
export function lateAttributionLimitResponse(): Response {
|
|
12
|
+
return formatErrorResponse(413, "request_too_large", OLLAMA_LATE_ATTRIBUTION_LIMIT_MESSAGE, {
|
|
13
|
+
code: "translation_buffer_limit",
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
|
|
5
17
|
export const PROVIDER_INPUT_TOO_LARGE_MESSAGE =
|
|
6
18
|
"The provider rejected this turn because its input exceeds the provider size or context limit. Reduce the current input or compact the conversation before retrying.";
|
|
7
19
|
|
|
@@ -22,7 +22,15 @@ import { classifyPoolRecoveryDispatch } from "../../routing/probe-lease";
|
|
|
22
22
|
import { formatErrorResponse } from "../../bridge";
|
|
23
23
|
import { readBoundedResponseBody } from "../../lib/bounded-body";
|
|
24
24
|
import { upstreamErrorMessageFromPayload, isRateLimitOrQuotaFailureMessage } from "../../lib/errors";
|
|
25
|
-
import { isNonReplayableResponse, isTransientUpstreamStatus } from "../../lib/upstream-retry";
|
|
25
|
+
import { isNonReplayableResponse, isTransientUpstreamStatus, SendBudgetExhaustedError } from "../../lib/upstream-retry";
|
|
26
|
+
import {
|
|
27
|
+
codexWsPreOutputRefusalHold,
|
|
28
|
+
codexWsPreOutputRefusalOutcomeRecorded,
|
|
29
|
+
isCodexWsPreOutputRefusal,
|
|
30
|
+
noteCodexWsPreOutputRefusalOutcome,
|
|
31
|
+
restoreCodexWsPreOutputRefusal,
|
|
32
|
+
type CodexWsPreOutputRefusalHold,
|
|
33
|
+
} from "./codex-ws-wire";
|
|
26
34
|
import type { RequestLogContext } from "../request-log";
|
|
27
35
|
import type { DataPlaneAdmission } from "../auth-cors";
|
|
28
36
|
import type { InboundWire } from "../../providers/registry";
|
|
@@ -121,6 +129,90 @@ export function usesCodexForwardPoolAuth(
|
|
|
121
129
|
&& provider.authMode === "forward" && provider.adapter === "openai-responses";
|
|
122
130
|
}
|
|
123
131
|
|
|
132
|
+
/**
|
|
133
|
+
* Write a pre-output refusal projection's account outcome once (#6969), for an exit that no other
|
|
134
|
+
* owner records: a caller abort or a local send-budget refusal during the same-account backoff,
|
|
135
|
+
* where the transport-failure path writes no account health and the restored stream never reaches
|
|
136
|
+
* delivery. A projection already recorded by the alternate-account helper is left alone.
|
|
137
|
+
*/
|
|
138
|
+
export function recordCodexPreOutputRefusalOutcome(
|
|
139
|
+
config: OcxConfig,
|
|
140
|
+
authCtx: CodexAuthContext,
|
|
141
|
+
provider: OcxProviderConfig,
|
|
142
|
+
response: Response,
|
|
143
|
+
modelId: string,
|
|
144
|
+
): void {
|
|
145
|
+
if (!isCodexWsPreOutputRefusal(response) || codexWsPreOutputRefusalOutcomeRecorded(response)) return;
|
|
146
|
+
if (!usesCodexForwardPoolAuth(authCtx, provider)) return;
|
|
147
|
+
noteCodexWsPreOutputRefusalOutcome(response);
|
|
148
|
+
recordCodexUpstreamOutcome(config, authCtx.accountId, response.status, {
|
|
149
|
+
threadId: authCtx.affinityKey,
|
|
150
|
+
fixedAccount: authCtx.fixedAccount,
|
|
151
|
+
modelId,
|
|
152
|
+
probeLeaseId: codexProbeLeaseId(authCtx),
|
|
153
|
+
probeQuotaScope: codexProbeQuotaScope(authCtx),
|
|
154
|
+
transientProbe: codexTransientProbeGrant(authCtx),
|
|
155
|
+
writerGeneration: authCtx.writerGeneration,
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* The pre-output refusal hold for one Pool send starting now (#6969), or undefined. Pool forward
|
|
161
|
+
* sends only, and never a combo child, whose own stream preflight already advances on a zero-output
|
|
162
|
+
* terminal. Every projection a send produces must later pass through
|
|
163
|
+
* {@link PoolPreOutputRefusals.restore}.
|
|
164
|
+
*/
|
|
165
|
+
export function poolPreOutputRefusalHold(
|
|
166
|
+
authCtx: CodexAuthContext,
|
|
167
|
+
provider: OcxProviderConfig,
|
|
168
|
+
connectMs: number,
|
|
169
|
+
comboAttempt: boolean | undefined,
|
|
170
|
+
): CodexWsPreOutputRefusalHold | undefined {
|
|
171
|
+
return !comboAttempt && usesCodexForwardPoolAuth(authCtx, provider) ? codexWsPreOutputRefusalHold(connectMs) : undefined;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* One dispatch's view of its pre-output refusal projections (#6969): the last one the initial
|
|
176
|
+
* transient ladder observed, the single exit where the dispatch itself must record it, and the
|
|
177
|
+
* restore that hands the client the original stream.
|
|
178
|
+
*/
|
|
179
|
+
export class PoolPreOutputRefusals {
|
|
180
|
+
private last: Response | undefined;
|
|
181
|
+
/** True when the response handed to delivery is a restored projection already recorded. */
|
|
182
|
+
outcomeRecorded = false;
|
|
183
|
+
|
|
184
|
+
readonly observe = (response: Response): Response => {
|
|
185
|
+
if (isCodexWsPreOutputRefusal(response)) this.last = response;
|
|
186
|
+
return response;
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* One owner per exceptional exit of the initial ladder. A caller abort or a local budget refusal
|
|
191
|
+
* during the backoff leaves the transport-failure path writing no account health, so the refusal
|
|
192
|
+
* already observed is recorded here; any other exception keeps that path as its single writer.
|
|
193
|
+
*/
|
|
194
|
+
recordOnLocalExit(
|
|
195
|
+
error: unknown,
|
|
196
|
+
aborted: boolean,
|
|
197
|
+
config: OcxConfig,
|
|
198
|
+
authCtx: CodexAuthContext,
|
|
199
|
+
provider: OcxProviderConfig,
|
|
200
|
+
modelId: string,
|
|
201
|
+
): void {
|
|
202
|
+
if (!this.last || !(aborted || error instanceof SendBudgetExhaustedError)) return;
|
|
203
|
+
recordCodexPreOutputRefusalOutcome(config, authCtx, provider, this.last, modelId);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Called right after the Pool block on every recovery pass: a projection that survived the ladder
|
|
208
|
+
* and the account move becomes the original event stream before any later reader sees it.
|
|
209
|
+
*/
|
|
210
|
+
restore(response: Response): Response {
|
|
211
|
+
this.outcomeRecorded = isCodexWsPreOutputRefusal(response) && codexWsPreOutputRefusalOutcomeRecorded(response);
|
|
212
|
+
return restoreCodexWsPreOutputRefusal(response);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
124
216
|
|
|
125
217
|
/** Live proof that this plain-main response used the observed main credential. */
|
|
126
218
|
export function liveMainQuotaDispatch(
|
|
@@ -368,6 +460,10 @@ export async function shouldRetryCodexScopedQuotaOnAlternate(
|
|
|
368
460
|
* could duplicate a turn the origin may still be running. A 5xx whose body confirms quota is not
|
|
369
461
|
* routed here either — {@link shouldRetryCodexPoolAccountQuota} classifies that one first and
|
|
370
462
|
* carries the cooldown with it.
|
|
463
|
+
*
|
|
464
|
+
* A WebSocket refusal reaches this predicate only as the opt-in pre-output projection (#6969): the
|
|
465
|
+
* upstream itself ended the turn with a structured 5xx while nothing but empty lifecycle frames had
|
|
466
|
+
* been relayed. It carries no non-replayable marker because nothing could have been observed.
|
|
371
467
|
*/
|
|
372
468
|
export function shouldRetryCodexPoolAccountTransient(response: Response): boolean {
|
|
373
469
|
return !isNonReplayableResponse(response) && isTransientUpstreamStatus(response.status);
|
|
@@ -420,6 +516,8 @@ export interface CodexPoolAccountRetryArgs {
|
|
|
420
516
|
stream: boolean;
|
|
421
517
|
/** Keep a buffered canonical client on HTTP/SSE after moving to another Pool account. */
|
|
422
518
|
httpOnly?: boolean;
|
|
519
|
+
/** Pre-output refusal hold for the alternate's WebSocket send (#6969); the caller restores. */
|
|
520
|
+
preOutputRefusalHold?: CodexWsPreOutputRefusalHold;
|
|
423
521
|
onResponse?: (
|
|
424
522
|
response: Response,
|
|
425
523
|
authCtx: CodexAuthContext,
|
|
@@ -571,6 +669,10 @@ export async function retryCodexPoolOnAlternateAccount(
|
|
|
571
669
|
// keep handing it the next request.
|
|
572
670
|
const recordUnmovedTransientOutcome = (): void => {
|
|
573
671
|
if (!isTransientUpstreamStatus(outcomeStatus)) return;
|
|
672
|
+
// #6969: one write per projection. After recordFirstOutcome, a late window or permit denial
|
|
673
|
+
// must not record the same refusal again, and a recorded projection tells delivery to skip it.
|
|
674
|
+
if (codexWsPreOutputRefusalOutcomeRecorded(firstResponse)) return;
|
|
675
|
+
noteCodexWsPreOutputRefusalOutcome(firstResponse);
|
|
574
676
|
recordCodexUpstreamOutcome(config, firstAuthCtx.accountId, outcomeStatus, {
|
|
575
677
|
threadId: firstAuthCtx.affinityKey,
|
|
576
678
|
fixedAccount: firstAuthCtx.fixedAccount,
|
|
@@ -763,6 +865,7 @@ export async function retryCodexPoolOnAlternateAccount(
|
|
|
763
865
|
// Retry already advanced the RR ring via excludeAccountId — reuse for promotion.
|
|
764
866
|
...(retryAuthCtx.accountId ? { promoteAccountId: retryAuthCtx.accountId } : {}),
|
|
765
867
|
});
|
|
868
|
+
noteCodexWsPreOutputRefusalOutcome(firstResponse);
|
|
766
869
|
};
|
|
767
870
|
// Only a combo reset-derived outcome is deferred. Retry-After, defaults, and
|
|
768
871
|
// ordinary requests must block the first account before the alternate send.
|
|
@@ -918,6 +1021,7 @@ export async function retryCodexPoolOnAlternateAccount(
|
|
|
918
1021
|
httpOnly,
|
|
919
1022
|
providerName: route.providerName,
|
|
920
1023
|
modelId: route.modelId,
|
|
1024
|
+
preOutputRefusalHold: args.preOutputRefusalHold,
|
|
921
1025
|
onCodexWsQuota: codexWsQuotaObserver(retryAuthCtx, route.provider, route.modelId),
|
|
922
1026
|
beforeDispatch: isCanonicalOpenAiForwardProvider(route.provider)
|
|
923
1027
|
? createCodexAuthDispatchGuard(retryAuthCtx, options.codexAuthPolicy ?? config, route.modelId, options.admission, options.visionDescribeTerminal === true) : undefined,
|
|
@@ -14,7 +14,9 @@ import {
|
|
|
14
14
|
import { normalizeUpstreamErrorText } from "./core-errors";
|
|
15
15
|
import { resolveClientRetryAfter } from "../../lib/retry-after";
|
|
16
16
|
import { formatErrorResponse } from "../../bridge";
|
|
17
|
+
import { OLLAMA_LATE_ATTRIBUTION_LIMIT_MESSAGE } from "../../adapters/ollama-native";
|
|
17
18
|
import { isNonReplayableResponse, markResponseNonReplayable } from "../../lib/upstream-retry";
|
|
19
|
+
import { MODEL_NOT_ALLOWED_FOR_KEY } from "../admission-model-scope";
|
|
18
20
|
import { usageFromResponsesPayload } from "../request-log";
|
|
19
21
|
import type { ResponsesTerminalStatus } from "../../bridge";
|
|
20
22
|
|
|
@@ -48,6 +50,9 @@ export async function consumeComboFailure(
|
|
|
48
50
|
// display-safe body carries a recognized quota message.
|
|
49
51
|
let quotaConfirmedByBody = false;
|
|
50
52
|
const serverError = response.status >= 500 && response.status < 600;
|
|
53
|
+
// A local key-scope refusal is already the client's answer: rebuilding it as an upstream_error
|
|
54
|
+
// would hide the stable model_not_allowed_for_key type clients branch on.
|
|
55
|
+
let scopeRefusalBody: string | undefined;
|
|
51
56
|
try {
|
|
52
57
|
const body = await readBoundedResponseBody(response, { signal, reportUtf8Validity: serverError });
|
|
53
58
|
// A 5xx body counts as quota or classification evidence only when it decoded as valid
|
|
@@ -70,6 +75,9 @@ export async function consumeComboFailure(
|
|
|
70
75
|
upstreamMessage = normalized.message;
|
|
71
76
|
upstreamType = normalized.type;
|
|
72
77
|
}
|
|
78
|
+
if (nonReplayable && response.status === 403 && upstreamType === MODEL_NOT_ALLOWED_FOR_KEY) {
|
|
79
|
+
scopeRefusalBody = body.text;
|
|
80
|
+
}
|
|
73
81
|
}
|
|
74
82
|
} catch (error) {
|
|
75
83
|
if (signal?.aborted) throw error;
|
|
@@ -105,7 +113,9 @@ export async function consumeComboFailure(
|
|
|
105
113
|
now,
|
|
106
114
|
includeDefault: false,
|
|
107
115
|
});
|
|
108
|
-
const failureResponse =
|
|
116
|
+
const failureResponse = scopeRefusalBody !== undefined
|
|
117
|
+
? new Response(scopeRefusalBody, { status: 403, headers: { "Content-Type": "application/json" } })
|
|
118
|
+
: formatErrorResponse(
|
|
109
119
|
response.status,
|
|
110
120
|
cyberFailure ? (upstreamType ?? CYBER_POLICY_ERROR_CODE) : "upstream_error",
|
|
111
121
|
message,
|
|
@@ -121,6 +131,7 @@ export async function consumeComboFailure(
|
|
|
121
131
|
classificationText,
|
|
122
132
|
...(codexModelRefusal !== undefined ? { codexModelRefusal } : {}),
|
|
123
133
|
...(normalizedUpstreamCode !== undefined ? { upstreamCode: normalizedUpstreamCode } : {}),
|
|
134
|
+
...(upstreamMessage !== undefined ? { upstreamMessage } : {}),
|
|
124
135
|
...(upstreamType !== undefined ? { upstreamType } : {}),
|
|
125
136
|
...(!cyberFailure && cooldownRetryAfter !== undefined ? { retryAfter: cooldownRetryAfter } : {}),
|
|
126
137
|
// The EFFECTIVE classification decides, not the raw status. An upstream that wraps a quota
|
|
@@ -221,3 +232,15 @@ export function buildComboChildHeaders(parentHeaders: HeadersInit): Headers {
|
|
|
221
232
|
childHeaders.delete("content-encoding");
|
|
222
233
|
return childHeaders;
|
|
223
234
|
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* A member's local Ollama late-attribution refusal: the exact proxy-owned constant on the exact
|
|
238
|
+
* local wire shape, from a replayable attempt. The caller replaces only the client response with
|
|
239
|
+
* the literal constant envelope; classification inputs are never rewritten.
|
|
240
|
+
*/
|
|
241
|
+
export function isLateAttributionRefusal(failure: ConsumedComboFailure): boolean {
|
|
242
|
+
return !failure.nonReplayable
|
|
243
|
+
&& failure.response.status === 413
|
|
244
|
+
&& failure.upstreamCode === "request_too_large"
|
|
245
|
+
&& failure.upstreamMessage === OLLAMA_LATE_ATTRIBUTION_LIMIT_MESSAGE;
|
|
246
|
+
}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { effectiveAzureOutputBudget } from "../../adapters/azure";
|
|
2
|
+
import { resolveWireProtocolOverride } from "../adapter-resolve";
|
|
1
3
|
import type { CodexAccountModelRefusal } from "../../combos/failover";
|
|
2
4
|
import {
|
|
3
5
|
isCodexReasoningEffort,
|
|
@@ -80,6 +82,7 @@ import {
|
|
|
80
82
|
buildComboChildHeaders,
|
|
81
83
|
createChildPassthroughCallbackGate,
|
|
82
84
|
consumeComboFailure,
|
|
85
|
+
isLateAttributionRefusal,
|
|
83
86
|
} from "./core-combo-failure";
|
|
84
87
|
import {
|
|
85
88
|
linkRequestSessionLane,
|
|
@@ -100,7 +103,7 @@ import {
|
|
|
100
103
|
markEagerRelaySseResponse,
|
|
101
104
|
} from "../relay";
|
|
102
105
|
import { preflightComboStreamResponse } from "./combo-stream-preflight";
|
|
103
|
-
import { streamingContextOverflowResponse, jsonContextOverflowResponse } from "./context-overflow";
|
|
106
|
+
import { streamingContextOverflowResponse, jsonContextOverflowResponse, lateAttributionLimitResponse } from "./context-overflow";
|
|
104
107
|
import { mandatoryResponsesReasoningReplayUnavailable } from "./core-replay";
|
|
105
108
|
import { settleOperatorReplacement } from "../../lib/upstream-retry";
|
|
106
109
|
import { createComboProtocolLanes, dispatchNativeComboChild } from "./core-combo-native";
|
|
@@ -711,12 +714,31 @@ export async function executeComboResponses(
|
|
|
711
714
|
retryAfter: String(Math.max(1, Math.ceil(remainingQuotaCooldownMs / 1000))),
|
|
712
715
|
}) : lastFailure;
|
|
713
716
|
};
|
|
717
|
+
// The caller's output ceiling, captured once so a floor applied to one target never
|
|
718
|
+
// leaks into the next. An established parent estimate wins; otherwise (combo dispatch
|
|
719
|
+
// precedes request preparation) the caller's own positive cap is reserved.
|
|
720
|
+
const callerBudget = isPlainObject(body) ? body.max_output_tokens : undefined;
|
|
721
|
+
const callerOutputCeilingTokens = logCtx.spendOutputCeilingTokens
|
|
722
|
+
?? (typeof callerBudget === "number" && Number.isFinite(callerBudget) && callerBudget > 0
|
|
723
|
+
? Math.trunc(callerBudget) : undefined);
|
|
714
724
|
while (pick) {
|
|
715
725
|
if (options.abortSignal?.aborted) return clientCancelledResponse();
|
|
716
726
|
const firstComboTarget = comboTargetsDispatched === 0;
|
|
717
727
|
const targetRoute = routeConcreteModel(config, `${pick.target.provider}/${pick.target.model}`);
|
|
718
728
|
// The inherited spend tracker observes this parent log, before the child has its own label.
|
|
719
729
|
logCtx.spendPoolId = targetRoute.providerName;
|
|
730
|
+
// Combo prepays before its child normalizes the route; the parent tracker must
|
|
731
|
+
// already see this target's effective cap rather than a previous target's cap.
|
|
732
|
+
const spendProvider = resolveWireProtocolOverride(targetRoute.providerName, targetRoute.modelId,
|
|
733
|
+
targetRoute.provider, options.inboundWire ?? "responses", targetRoute.staticPolicy);
|
|
734
|
+
const outputBudget = effectiveAzureOutputBudget(spendProvider, callerBudget);
|
|
735
|
+
if (outputBudget !== callerBudget && typeof outputBudget === "number" && outputBudget > 0) {
|
|
736
|
+
logCtx.spendOutputCeilingTokens = Math.trunc(outputBudget);
|
|
737
|
+
} else if (callerOutputCeilingTokens === undefined) {
|
|
738
|
+
delete logCtx.spendOutputCeilingTokens;
|
|
739
|
+
} else {
|
|
740
|
+
logCtx.spendOutputCeilingTokens = callerOutputCeilingTokens;
|
|
741
|
+
}
|
|
720
742
|
// Derive the target's allowance before booking its initial send; the booking occupies it.
|
|
721
743
|
const targetSendBudget = comboSendScope
|
|
722
744
|
? comboTargetSendBudget(comboSendScope, combo.targets.length - 1 - comboTargetsDispatched)
|
|
@@ -1007,6 +1029,10 @@ export async function executeComboResponses(
|
|
|
1007
1029
|
retainCancelledAttempt();
|
|
1008
1030
|
return clientCancelledResponse();
|
|
1009
1031
|
}
|
|
1032
|
+
// A member's local late-attribution refusal reaches the client only as the literal constant
|
|
1033
|
+
// envelope: swap the response, keep every classification input, and skip the overflow rewrite.
|
|
1034
|
+
const lateAttributionRefusal = isLateAttributionRefusal(failure);
|
|
1035
|
+
if (lateAttributionRefusal) failure = { ...failure, response: lateAttributionLimitResponse() };
|
|
1010
1036
|
sealRequestAttemptIdentity(
|
|
1011
1037
|
attempt,
|
|
1012
1038
|
childLog.provider,
|
|
@@ -1047,7 +1073,7 @@ export async function executeComboResponses(
|
|
|
1047
1073
|
});
|
|
1048
1074
|
const wantsStream = (rawBody as { stream?: unknown } | null)?.stream === true;
|
|
1049
1075
|
// Local byte admission has its own diagnostic; do not relabel it as an upstream refusal.
|
|
1050
|
-
const classifyOverflow = failure.response.status === 413
|
|
1076
|
+
const classifyOverflow = failure.response.status === 413 && !lateAttributionRefusal
|
|
1051
1077
|
&& (wantsStream || (failure.upstreamCode !== "outbound_body_too_large"
|
|
1052
1078
|
&& failure.upstreamCode !== "translation_buffer_limit"));
|
|
1053
1079
|
lastFailureClassifiesOverflow = classifyOverflow;
|
|
@@ -2,7 +2,7 @@ import { readBoundedResponseBody } from "../../lib/bounded-body";
|
|
|
2
2
|
import { redactSecretString } from "../../lib/redact";
|
|
3
3
|
import { isCyberPolicyMessage, isCyberPolicyCode } from "../../lib/errors";
|
|
4
4
|
import { isTranslatorBudgetExceededError } from "../../lib/translator-budget";
|
|
5
|
-
import { formatErrorResponse } from "../../bridge";
|
|
5
|
+
import { formatErrorResponse, formatUsageLimitReachedResponse } from "../../bridge";
|
|
6
6
|
import {
|
|
7
7
|
UnsupportedContentEncodingError,
|
|
8
8
|
DecompressedBodyTooLargeError,
|
|
@@ -132,6 +132,34 @@ export function clientCancelledResponse(): Response {
|
|
|
132
132
|
return formatErrorResponse(499, "client_cancelled", "Client cancelled request");
|
|
133
133
|
}
|
|
134
134
|
|
|
135
|
+
/**
|
|
136
|
+
* The Responses answer to a local Anthropic pool refusal (`AnthropicAccountCooldownError`).
|
|
137
|
+
*
|
|
138
|
+
* When every held account is out on spent quota the refusal is a usage limit (#6991), which the
|
|
139
|
+
* Codex client stops on and renders with its reset time; anything else stays the retryable
|
|
140
|
+
* `rate_limit_error` it always was. A combo child keeps the retryable form and wording: the combo
|
|
141
|
+
* hops on any 429 and rebuilds the body anyway, and `isAnthropicPoolLocalRefusal` in
|
|
142
|
+
* `core-combo.ts` deliberately cools the target for the short local fallback, which a usage-limit
|
|
143
|
+
* code or usage-limit prose would stretch to the ten-minute exhaustion fallback in
|
|
144
|
+
* `combos/failover.ts`.
|
|
145
|
+
*/
|
|
146
|
+
export function anthropicPoolCooldownResponse(
|
|
147
|
+
refusal: {
|
|
148
|
+
message: string;
|
|
149
|
+
retryableMessage: string;
|
|
150
|
+
retryAfterSeconds: number | null;
|
|
151
|
+
usageLimit: { readonly resetsAt: number | null } | null;
|
|
152
|
+
},
|
|
153
|
+
options: { comboAttempt?: boolean; logCtx?: { errorCode?: string } } = {},
|
|
154
|
+
): Response {
|
|
155
|
+
if (refusal.usageLimit !== null && !options.comboAttempt) {
|
|
156
|
+
if (options.logCtx) options.logCtx.errorCode = "usage_limit_reached";
|
|
157
|
+
return formatUsageLimitReachedResponse(refusal.message, refusal.usageLimit.resetsAt, refusal.retryAfterSeconds);
|
|
158
|
+
}
|
|
159
|
+
return formatErrorResponse(429, "rate_limit_error", refusal.retryableMessage,
|
|
160
|
+
refusal.retryAfterSeconds === null ? undefined : { retryAfter: String(refusal.retryAfterSeconds) });
|
|
161
|
+
}
|
|
162
|
+
|
|
135
163
|
|
|
136
164
|
export const UNREADABLE_ENCRYPTED_AGENT_TASK_MESSAGE =
|
|
137
165
|
"Routed V2 worker task is encrypted for the native ChatGPT backend and cannot be read by the selected provider. Use plaintext V2 agent-message delivery or select a native ChatGPT model.";
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { TranslatorBudget } from "../../lib/translator-budget";
|
|
2
2
|
import { finalizeTranslatorBudgetResponse } from "../../lib/translator-budget";
|
|
3
|
+
import { isNonReplayableResponse, markResponseNonReplayable } from "../../lib/upstream-retry";
|
|
3
4
|
import {
|
|
4
5
|
isNativePassthroughSseResponse,
|
|
5
6
|
markNativePassthroughSseResponse,
|
|
@@ -67,6 +68,7 @@ export function finalizeAccountLease(response: Response, release: () => void): R
|
|
|
67
68
|
async cancel(reason) { try { await reader.cancel(reason); } finally { finish(); } },
|
|
68
69
|
});
|
|
69
70
|
const wrapped = new Response(body, { status: response.status, statusText: response.statusText, headers: response.headers });
|
|
71
|
+
if (isNonReplayableResponse(response)) markResponseNonReplayable(wrapped);
|
|
70
72
|
if (isNativePassthroughSseResponse(response)) markNativePassthroughSseResponse(wrapped);
|
|
71
73
|
if (isEagerRelaySseResponse(response)) markEagerRelaySseResponse(wrapped);
|
|
72
74
|
if (isPreinspectedJsonResponse(response)) markPreinspectedJsonResponse(wrapped);
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { effectiveAzureOutputBudget } from "../../adapters/azure";
|
|
1
2
|
import { sanitizeLogMetadataString } from "../../lib/redact";
|
|
2
3
|
import type { RouteResult } from "../../router";
|
|
3
4
|
import type { InboundWire } from "../../providers/registry";
|
|
@@ -211,6 +212,16 @@ export async function applyFinalRouteRequestNormalization(args: {
|
|
|
211
212
|
logCtx.model = virtualModel?.selectedModelId ?? route.modelId;
|
|
212
213
|
logCtx.provider = route.providerName;
|
|
213
214
|
logCtx.spendPoolId = route.providerName;
|
|
215
|
+
// Admission precedes adapter serialization. Reserve the same explicit output cap
|
|
216
|
+
// the final destination will send, and recompute from the caller on every fallback.
|
|
217
|
+
const rawBudget = parsed._rawBody && typeof parsed._rawBody === "object"
|
|
218
|
+
? (parsed._rawBody as { max_output_tokens?: unknown }).max_output_tokens : undefined;
|
|
219
|
+
const outputBudget = effectiveAzureOutputBudget(route.provider,
|
|
220
|
+
route.provider.adapter === "azure" || route.provider.adapter === "azure-openai"
|
|
221
|
+
? rawBudget : parsed.options.maxOutputTokens);
|
|
222
|
+
if (typeof outputBudget === "number" && outputBudget > 0) {
|
|
223
|
+
logCtx.spendOutputCeilingTokens = Math.trunc(outputBudget);
|
|
224
|
+
}
|
|
214
225
|
logCtx.providerAdapter = route.provider.adapter;
|
|
215
226
|
logCtx.routeDecision = route.routeDecision;
|
|
216
227
|
logCtx.policyEligibility = route.policyEligibility;
|