@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
|
@@ -72,10 +72,25 @@ export type { AnthropicAccountPoolConfig } from "../types/anthropic-account-pool
|
|
|
72
72
|
|
|
73
73
|
/** Local admission refusal across async dispatch; never treat it as provider reachability evidence. */
|
|
74
74
|
export class AnthropicAccountCooldownError extends Error {
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
75
|
+
/** The retryable wording, kept unchanged for combo children: combo failover classifies by prose too. */
|
|
76
|
+
readonly retryableMessage: string;
|
|
77
|
+
constructor(
|
|
78
|
+
readonly retryAfterSeconds: number | null,
|
|
79
|
+
readonly routePosition?: number,
|
|
80
|
+
/**
|
|
81
|
+
* Set only when no account the route can use is admissible and every held one is out on
|
|
82
|
+
* spent quota (#6991). `resetsAt` is the earliest upstream window reset among them (epoch
|
|
83
|
+
* ms), or null when any held account's reset is unknown. Null overall means at least one hold
|
|
84
|
+
* is a transient throttle or an entitlement refusal, which a client retry may outlast.
|
|
85
|
+
*/
|
|
86
|
+
readonly usageLimit: { readonly resetsAt: number | null } | null = null,
|
|
87
|
+
) {
|
|
88
|
+
const scope = routePosition === undefined ? "All Anthropic OAuth accounts" : "Anthropic OAuth accounts for this model route";
|
|
89
|
+
const retryable = `${scope} are temporarily rate-limited`;
|
|
90
|
+
super(usageLimit === null ? retryable
|
|
91
|
+
: usageLimit.resetsAt === null ? `${scope} have reached their usage limit`
|
|
92
|
+
: `${scope} have reached their usage limit; the first one resets at ${new Date(usageLimit.resetsAt).toISOString()}`);
|
|
93
|
+
this.retryableMessage = retryable;
|
|
79
94
|
this.name = "AnthropicAccountCooldownError";
|
|
80
95
|
}
|
|
81
96
|
}
|
|
@@ -89,7 +104,7 @@ export class AnthropicAccountCooldownError extends Error {
|
|
|
89
104
|
* `retry-after` would report a drained five-hour window as request-rate throttling.
|
|
90
105
|
*/
|
|
91
106
|
type AnthropicCooldownSource = "retry-after" | "reset-derived" | "default";
|
|
92
|
-
type AnthropicQuotaWindow = "five-hour" | "weekly";
|
|
107
|
+
export type AnthropicQuotaWindow = "five-hour" | "weekly";
|
|
93
108
|
|
|
94
109
|
interface AccountHealth {
|
|
95
110
|
cooldownUntil: number;
|
|
@@ -98,6 +113,24 @@ interface AccountHealth {
|
|
|
98
113
|
rejectedQuotaWindows?: AnthropicQuotaWindow[];
|
|
99
114
|
/** Monotonic fence so an older quota probe cannot erase a newer refusal. */
|
|
100
115
|
cooldownGeneration: number;
|
|
116
|
+
/** When the refusal was recorded; a reset settled earlier must not lift it. */
|
|
117
|
+
recordedAt: number;
|
|
118
|
+
/** 429 (shared quota) or 403 (entitlement); only a quota refusal can be lifted by a reset grant. */
|
|
119
|
+
refusalStatus: 429 | 403;
|
|
120
|
+
/** Windows a 429 reported as rejected, whatever set the deadline; empty when it named none. */
|
|
121
|
+
refusedQuotaWindows: AnthropicQuotaWindow[];
|
|
122
|
+
/**
|
|
123
|
+
* When the rejected windows reopen according to upstream's reset headers, kept even when
|
|
124
|
+
* Retry-After set the deadline. A usage-limit answer reports this, never an admission deadline.
|
|
125
|
+
*/
|
|
126
|
+
quotaResetAt?: number;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** See `anthropicAccountHold`: the latest hold on one account and its known window reset. */
|
|
130
|
+
interface AnthropicAccountHold {
|
|
131
|
+
until: number;
|
|
132
|
+
quota: boolean;
|
|
133
|
+
resetAt?: number;
|
|
101
134
|
}
|
|
102
135
|
|
|
103
136
|
interface AffinityEntry {
|
|
@@ -127,6 +160,14 @@ function parseRetryAfterMs(value: string | null | undefined, now: number): numbe
|
|
|
127
160
|
return delayUntil(Date.parse(text), now);
|
|
128
161
|
}
|
|
129
162
|
|
|
163
|
+
/** Windows whose unified status a refusal reported as rejected, independent of any reset header. */
|
|
164
|
+
function rejectedQuotaWindowsIn(headers: AnthropicRateLimitHeaders | null | undefined): AnthropicQuotaWindow[] {
|
|
165
|
+
if (!headers) return [];
|
|
166
|
+
return ([["5h", "five-hour"], ["7d", "weekly"]] as const)
|
|
167
|
+
.filter(([window]) => headers.get(`anthropic-ratelimit-unified-${window}-status`)?.trim() === "rejected")
|
|
168
|
+
.map(([, quotaWindow]) => quotaWindow);
|
|
169
|
+
}
|
|
170
|
+
|
|
130
171
|
function parseRateLimitReset(
|
|
131
172
|
headers: AnthropicRateLimitHeaders | null | undefined,
|
|
132
173
|
now: number,
|
|
@@ -152,6 +193,32 @@ function parseRateLimitReset(
|
|
|
152
193
|
return { delayMs: latest - now, rejectedQuotaWindows };
|
|
153
194
|
}
|
|
154
195
|
|
|
196
|
+
/**
|
|
197
|
+
* When every window a refusal reported as rejected reopens, per upstream's reset headers; undefined
|
|
198
|
+
* unless each rejected window carries a usable reset. Unlike `parseRateLimitReset`, which picks the
|
|
199
|
+
* best available admission deadline, this is reported to the client as the usage-limit reset, so a
|
|
200
|
+
* weekly window with no reset cannot hide behind a five-hour one that has one.
|
|
201
|
+
*/
|
|
202
|
+
function rejectedWindowsResetAt(headers: AnthropicRateLimitHeaders | null | undefined, now: number): number | undefined {
|
|
203
|
+
if (!headers) return undefined;
|
|
204
|
+
const usableReset = (header: string): number | undefined => {
|
|
205
|
+
const resetAt = Number(headers.get(header)?.trim()) * 1000;
|
|
206
|
+
return Number.isFinite(resetAt) && delayUntil(resetAt, now) !== undefined ? resetAt : undefined;
|
|
207
|
+
};
|
|
208
|
+
const rejected = rejectedQuotaWindowsIn(headers);
|
|
209
|
+
if (!rejected.length) {
|
|
210
|
+
return headers.get("anthropic-ratelimit-unified-status")?.trim() === "rejected"
|
|
211
|
+
? usableReset("anthropic-ratelimit-unified-reset") : undefined;
|
|
212
|
+
}
|
|
213
|
+
let latest = 0;
|
|
214
|
+
for (const window of rejected) {
|
|
215
|
+
const resetAt = usableReset(`anthropic-ratelimit-unified-${window === "five-hour" ? "5h" : "7d"}-reset`);
|
|
216
|
+
if (resetAt === undefined) return undefined;
|
|
217
|
+
latest = Math.max(latest, resetAt);
|
|
218
|
+
}
|
|
219
|
+
return latest;
|
|
220
|
+
}
|
|
221
|
+
|
|
155
222
|
interface ScoredAccount {
|
|
156
223
|
accountId: string;
|
|
157
224
|
hasKnownUsage: boolean;
|
|
@@ -286,6 +353,7 @@ type RoutingFacadeSymbol =
|
|
|
286
353
|
| "settleAnthropicCooldownRecovery"
|
|
287
354
|
| "getAnthropicAccountHealthSnapshot"
|
|
288
355
|
| "clearAnthropicAccountCooldown"
|
|
356
|
+
| "clearAnthropicAccountCooldownForReset"
|
|
289
357
|
| "sweepExpiredAnthropicRoutingHealth"
|
|
290
358
|
| "clearAnthropicAccountPoolState"
|
|
291
359
|
| "anthropicSessionAffinitySizeForTests"
|
|
@@ -293,6 +361,8 @@ type RoutingFacadeSymbol =
|
|
|
293
361
|
| "hasAnthropicFailoverQuorum"
|
|
294
362
|
| "forgetAnthropicFailoverQuorum"
|
|
295
363
|
| "getAnthropicPoolRetryAfterSeconds"
|
|
364
|
+
| "anthropicPoolCooldownError"
|
|
365
|
+
| "anthropicPoolUsageLimitAfterRefusal"
|
|
296
366
|
| "pickAlternateAnthropicAccount"
|
|
297
367
|
| "resolveAnthropicAccountForSession"
|
|
298
368
|
| "resolveAnthropicDispatchAccountId"
|
|
@@ -315,7 +385,7 @@ const handlers = new Map<AnthropicInstanceId, RoutingHandlers>();
|
|
|
315
385
|
function createAnthropicRouting(instance: AnthropicInstanceId): AnthropicRouting {
|
|
316
386
|
const PROVIDER = instance;
|
|
317
387
|
const poolKey = anthropicPoolKey(instance);
|
|
318
|
-
const { anthropicFamilyRejected, anthropicFamilyRetryAt, anthropicModelExhausted, clearAnthropicFamilyQuota } = anthropicModelQuotaFor(instance);
|
|
388
|
+
const { anthropicFamilyRejected, anthropicFamilyRetryAt, anthropicFamilyHoldState, anthropicModelExhausted, clearAnthropicFamilyQuota } = anthropicModelQuotaFor(instance);
|
|
319
389
|
const { anthropicRatePauseUntil, clearAnthropicRatePauses, pauseAnthropicRateAdmission } = anthropicRatePolicyFor(instance);
|
|
320
390
|
const { anthropicCooldownGeneration, clearAnthropicCooldownGenerations, noteAnthropicCooldownMutation } = anthropicCooldownRecoveryFor(instance);
|
|
321
391
|
// Request-owned config authorizes selection/commit. Namespace-only credential and roster
|
|
@@ -356,7 +426,26 @@ function createAnthropicRouting(instance: AnthropicInstanceId): AnthropicRouting
|
|
|
356
426
|
|
|
357
427
|
|
|
358
428
|
/**
|
|
359
|
-
*
|
|
429
|
+
* The windows a fresh usage read must show open before it may lift this cooldown, or null when
|
|
430
|
+
* no usage read can lift it.
|
|
431
|
+
*
|
|
432
|
+
* Only a 429 is quota evidence: `recordAnthropicAccountRefusal` keeps a 429 only after
|
|
433
|
+
* classifying it as shared quota, and a 403 is an entitlement refusal that usage cannot answer.
|
|
434
|
+
* The deadline source does not matter (#6989). Anthropic sends Retry-After on a spent window as
|
|
435
|
+
* well, set to that window's reset, so a `retry-after` cooldown that named rejected windows
|
|
436
|
+
* carries exactly the evidence a `reset-derived` one does; the read decides, not the header.
|
|
437
|
+
* A refusal that named no window can only be the aggregate unified status. It stays claimable
|
|
438
|
+
* when reset-derived, as before, but no usage reading covers it, so it is always retained.
|
|
439
|
+
*/
|
|
440
|
+
function recoverableQuotaWindows(entry: AccountHealth): readonly AnthropicQuotaWindow[] | null {
|
|
441
|
+
if (entry.refusalStatus !== 429) return null;
|
|
442
|
+
if (entry.rejectedQuotaWindows?.length) return entry.rejectedQuotaWindows;
|
|
443
|
+
if (entry.refusedQuotaWindows.length) return entry.refusedQuotaWindows;
|
|
444
|
+
return entry.cooldownSource === "reset-derived" ? [] : null;
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
/**
|
|
448
|
+
* Capture the exact quota refusal a fresh usage probe is allowed to recover.
|
|
360
449
|
*
|
|
361
450
|
* The generation fence matters because the usage request is asynchronous: a newer 429 may
|
|
362
451
|
* arrive while it is in flight, and an older answer must never erase that newer refusal.
|
|
@@ -366,13 +455,14 @@ function createAnthropicRouting(instance: AnthropicInstanceId): AnthropicRouting
|
|
|
366
455
|
now = Date.now(),
|
|
367
456
|
): AnthropicCooldownRecoveryClaim | null {
|
|
368
457
|
const entry = upstreamHealth.get(accountId);
|
|
369
|
-
if (!entry || entry.cooldownUntil <= now || entry
|
|
458
|
+
if (!entry || entry.cooldownUntil <= now || recoverableQuotaWindows(entry) === null) return null;
|
|
370
459
|
return { instance, accountId, cooldownGeneration: entry.cooldownGeneration, claimedAt: now };
|
|
371
460
|
}
|
|
372
461
|
|
|
373
462
|
/**
|
|
374
|
-
* Clear only the claimed
|
|
375
|
-
*
|
|
463
|
+
* Clear only the claimed quota cooldown when a fresh, complete usage result proves headroom in
|
|
464
|
+
* every window that upstream previously reported as rejected. A result read before the claim
|
|
465
|
+
* (`updatedAt < claimedAt`) is stale and settles nothing.
|
|
376
466
|
*/
|
|
377
467
|
function settleAnthropicCooldownRecovery(
|
|
378
468
|
claim: AnthropicCooldownRecoveryClaim,
|
|
@@ -380,12 +470,12 @@ function createAnthropicRouting(instance: AnthropicInstanceId): AnthropicRouting
|
|
|
380
470
|
): AnthropicCooldownRecoverySettlement {
|
|
381
471
|
if (claim.instance !== instance) return "superseded";
|
|
382
472
|
const entry = upstreamHealth.get(claim.accountId);
|
|
383
|
-
|
|
473
|
+
const windows = entry ? recoverableQuotaWindows(entry) : null;
|
|
474
|
+
if (!entry || windows === null
|
|
384
475
|
|| entry.cooldownGeneration !== claim.cooldownGeneration
|
|
385
476
|
|| anthropicCooldownGeneration(claim.accountId) !== claim.cooldownGeneration
|
|
386
477
|
|| !Number.isFinite(quota.updatedAt) || quota.updatedAt < claim.claimedAt) return "superseded";
|
|
387
|
-
|
|
388
|
-
if (!windows?.length) return "retained";
|
|
478
|
+
if (!windows.length) return "retained";
|
|
389
479
|
const recovered = windows.every(window => {
|
|
390
480
|
const percent = window === "five-hour" ? quota.fiveHourPercent : quota.weeklyPercent;
|
|
391
481
|
return typeof percent === "number" && Number.isFinite(percent) && percent >= 0 && percent < 100;
|
|
@@ -416,6 +506,34 @@ function createAnthropicRouting(instance: AnthropicInstanceId): AnthropicRouting
|
|
|
416
506
|
return cleared;
|
|
417
507
|
}
|
|
418
508
|
|
|
509
|
+
/**
|
|
510
|
+
* Lift a quota cooldown for a reset grant. `claimedAt` is when the claim was first journaled,
|
|
511
|
+
* before it was sent: a refusal recorded at or after it may already describe the post-reset
|
|
512
|
+
* account (a tie within one clock tick cannot be ordered, so it stays too). The windows the
|
|
513
|
+
* grant confirmed cleared must cover every window the refusal named; a refusal that named none
|
|
514
|
+
* needs both windows cleared, and an entitlement (403) refusal is never lifted by a reset.
|
|
515
|
+
*/
|
|
516
|
+
function clearAnthropicAccountCooldownForReset(
|
|
517
|
+
accountId: string,
|
|
518
|
+
claimedAt: number,
|
|
519
|
+
clearedWindows: ReadonlySet<AnthropicQuotaWindow>,
|
|
520
|
+
): "cleared" | "absent" | "newer" | "uncovered" {
|
|
521
|
+
const entry = upstreamHealth.get(accountId);
|
|
522
|
+
if (!entry) return "absent";
|
|
523
|
+
if (entry.recordedAt >= claimedAt) return "newer";
|
|
524
|
+
const named = entry.rejectedQuotaWindows?.length ? entry.rejectedQuotaWindows : entry.refusedQuotaWindows;
|
|
525
|
+
const required: readonly AnthropicQuotaWindow[] = named.length ? named : ["five-hour", "weekly"];
|
|
526
|
+
if (entry.refusalStatus !== 429 || !required.every(window => clearedWindows.has(window))) {
|
|
527
|
+
// Kept, but reads that started before the grant are now stale. Move the entry to the same
|
|
528
|
+
// new generation so a fresh usage read can still recover a 429 quota cooldown.
|
|
529
|
+
entry.cooldownGeneration = noteAnthropicCooldownMutation(accountId);
|
|
530
|
+
return "uncovered";
|
|
531
|
+
}
|
|
532
|
+
upstreamHealth.delete(accountId);
|
|
533
|
+
noteAnthropicCooldownMutation(accountId);
|
|
534
|
+
return "cleared";
|
|
535
|
+
}
|
|
536
|
+
|
|
419
537
|
function sweepExpiredAnthropicRoutingHealth(now = Date.now()): number {
|
|
420
538
|
let removed = 0;
|
|
421
539
|
for (const [accountId, health] of upstreamHealth) {
|
|
@@ -603,23 +721,90 @@ function createAnthropicRouting(instance: AnthropicInstanceId): AnthropicRouting
|
|
|
603
721
|
|
|
604
722
|
/** Earliest remaining cooldown among cooled Anthropic accounts, for client Retry-After. */
|
|
605
723
|
function getAnthropicPoolRetryAfterSeconds(now = Date.now(), decision: AnthropicRouteDecision | null = null, model?: string): number | null {
|
|
724
|
+
return anthropicPoolCooldownError(now, decision, model).retryAfterSeconds;
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
/**
|
|
728
|
+
* What keeps one account out of admission, and whether that is spent quota (#6991).
|
|
729
|
+
*
|
|
730
|
+
* Every hold must lift before the account can serve, so the latest one decides. A 429 health
|
|
731
|
+
* entry is quota by construction (only a shared-quota classification records one), whatever its
|
|
732
|
+
* deadline source; a 403 entry is an entitlement refusal; an admission pause is a transient
|
|
733
|
+
* throttle; a family exclusion is a spent family window unless a revalidation send is already
|
|
734
|
+
* in flight. A tie goes to the transient hold, because then a retry at that instant may succeed.
|
|
735
|
+
*
|
|
736
|
+
* The reported reset is computed apart from that admission order: the account's usage reopens
|
|
737
|
+
* when the last of its quota holds' upstream windows resets, and it is unknown when any of them
|
|
738
|
+
* lacks a known reset. Admission deadlines are not resets: they may be Retry-After, the
|
|
739
|
+
* sixty-second default or the thirty-minute family evidence lifetime.
|
|
740
|
+
*/
|
|
741
|
+
function anthropicAccountHold(accountId: string, now: number, model?: string): AnthropicAccountHold | null {
|
|
742
|
+
const latest = { until: 0, quota: false };
|
|
743
|
+
let quotaResetAt = 0;
|
|
744
|
+
let quotaResetKnown = true;
|
|
745
|
+
const consider = (until: number | undefined, quota: boolean, reset?: number) => {
|
|
746
|
+
if (until === undefined || until <= now) return;
|
|
747
|
+
if (quota) {
|
|
748
|
+
if (reset === undefined || reset <= now) quotaResetKnown = false;
|
|
749
|
+
else quotaResetAt = Math.max(quotaResetAt, reset);
|
|
750
|
+
}
|
|
751
|
+
if (until < latest.until || (until === latest.until && quota)) return;
|
|
752
|
+
latest.until = until;
|
|
753
|
+
latest.quota = quota;
|
|
754
|
+
};
|
|
755
|
+
const entry = getAnthropicAccountHealthSnapshot(accountId, now) ? upstreamHealth.get(accountId) : undefined;
|
|
756
|
+
consider(entry?.cooldownUntil, entry?.refusalStatus === 429, entry?.quotaResetAt);
|
|
757
|
+
consider(anthropicRatePauseUntil(accountId, now), false);
|
|
758
|
+
const family = anthropicFamilyHoldState(accountId, model);
|
|
759
|
+
consider(anthropicFamilyRetryAt(accountId, model, now), family?.revalidating === false, family?.resetAt);
|
|
760
|
+
if (latest.until === 0) return null;
|
|
761
|
+
return { until: latest.until, quota: latest.quota,
|
|
762
|
+
...(latest.quota && quotaResetKnown && quotaResetAt > 0 ? { resetAt: quotaResetAt } : {}) };
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
/**
|
|
766
|
+
* The local refusal for a pool (or model route) with no admissible account. Retry-After is the
|
|
767
|
+
* earliest hold among accounts the route can use once they recover. The refusal is a usage limit
|
|
768
|
+
* only when no account the route can use is admissible and every held one is out on spent quota;
|
|
769
|
+
* then a client retry before that instant cannot succeed, and Codex should stop and show the
|
|
770
|
+
* reset instead of retrying. The reset is the earliest known window reset, and is omitted when
|
|
771
|
+
* any held account's reset is unknown, since that account could reopen first.
|
|
772
|
+
*/
|
|
773
|
+
function anthropicPoolCooldownError(now = Date.now(), decision: AnthropicRouteDecision | null = null, model?: string): AnthropicAccountCooldownError {
|
|
606
774
|
const set = getAccountSet(PROVIDER);
|
|
607
|
-
if (!set) return null;
|
|
775
|
+
if (!set) return new AnthropicAccountCooldownError(null, decision?.position);
|
|
608
776
|
// Once an explicit fallback route has no eligible declared account, selection may use
|
|
609
777
|
// the ordinary pool. Its earliest usable cooldown must determine the advertised wait.
|
|
610
778
|
const fallbackExpanded = decision?.fallback === true
|
|
611
779
|
&& !getEligibleAnthropicAccounts(now, model).some(id => decision.accounts.includes(id));
|
|
612
780
|
let earliest: number | null = null;
|
|
781
|
+
let allQuota = true;
|
|
782
|
+
let earliestReset: number | null = null;
|
|
783
|
+
let resetsKnown = true;
|
|
613
784
|
for (const account of set.accounts) {
|
|
614
785
|
if (account.paused === true || account.needsReauth === true || !isPoolCredentialUsable(account.id, now)) continue;
|
|
615
786
|
if (decision && !fallbackExpanded && !decision.accounts.includes(account.id)) continue;
|
|
616
|
-
const
|
|
617
|
-
|
|
618
|
-
if (
|
|
619
|
-
if (
|
|
787
|
+
const hold = anthropicAccountHold(account.id, now, model);
|
|
788
|
+
if (!hold) continue;
|
|
789
|
+
if (!hold.quota) allQuota = false;
|
|
790
|
+
else if (hold.resetAt === undefined) resetsKnown = false;
|
|
791
|
+
else if (earliestReset === null || hold.resetAt < earliestReset) earliestReset = hold.resetAt;
|
|
792
|
+
if (earliest === null || hold.until < earliest) earliest = hold.until;
|
|
620
793
|
}
|
|
621
|
-
if (earliest === null
|
|
622
|
-
|
|
794
|
+
if (earliest === null) return new AnthropicAccountCooldownError(null, decision?.position);
|
|
795
|
+
const exhausted = allQuota && routeCandidates(getEligibleAnthropicAccounts(now, model), decision).length === 0;
|
|
796
|
+
return new AnthropicAccountCooldownError(Math.max(1, Math.ceil((earliest - now) / 1000)), decision?.position,
|
|
797
|
+
exhausted ? { resetsAt: resetsKnown ? earliestReset : null } : null);
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
/**
|
|
801
|
+
* After an upstream refusal found no replacement: the usage-limit refusal when the pool is
|
|
802
|
+
* exhausted as defined above, else null. A refusal that left an admissible sibling (a send
|
|
803
|
+
* budget ran out, a request-scoped 429) or a sibling on a transient hold is not exhaustion.
|
|
804
|
+
*/
|
|
805
|
+
function anthropicPoolUsageLimitAfterRefusal(now = Date.now(), decision: AnthropicRouteDecision | null = null, model?: string): AnthropicAccountCooldownError | null {
|
|
806
|
+
const refusal = anthropicPoolCooldownError(now, decision, model);
|
|
807
|
+
return refusal.usageLimit === null ? null : refusal;
|
|
623
808
|
}
|
|
624
809
|
|
|
625
810
|
function pickLowestUsage(config: OcxConfig, excludeId: string | undefined, now: number, decision: AnthropicRouteDecision | null = null, model?: string, excludedAccountIds?: ReadonlySet<string>): string | null {
|
|
@@ -950,7 +1135,7 @@ function createAnthropicRouting(instance: AnthropicInstanceId): AnthropicRouting
|
|
|
950
1135
|
const now = Date.now();
|
|
951
1136
|
const selection = resolveAnthropicAccountForSession(sessionKey, config, now, decision, model);
|
|
952
1137
|
if (selection.reason === "all-cooled") {
|
|
953
|
-
throw
|
|
1138
|
+
throw anthropicPoolCooldownError(now, decision, model);
|
|
954
1139
|
}
|
|
955
1140
|
if (selection.reason === "paused" || !selection.accountId || !getEligibleAnthropicAccounts(now, model).includes(selection.accountId)) {
|
|
956
1141
|
const { OAuthAccountPausedError, OAuthLoginRequiredError } = await import("./index");
|
|
@@ -1084,6 +1269,7 @@ function createAnthropicRouting(instance: AnthropicInstanceId): AnthropicRouting
|
|
|
1084
1269
|
// account is back in the rotation a minute later.
|
|
1085
1270
|
const parsedRetry = parseRetryAfterMs(retryAfterHeader, now);
|
|
1086
1271
|
const resetDerived = status === 429 && parsedRetry === undefined ? parseRateLimitReset(rateLimitHeaders, now) : undefined;
|
|
1272
|
+
const quotaResetAt = status === 429 ? rejectedWindowsResetAt(rateLimitHeaders, now) : undefined;
|
|
1087
1273
|
const cooldownMs = parsedRetry ?? resetDerived?.delayMs ?? (status === 403 ? ACCOUNT_REFUSAL_COOLDOWN_MS : DEFAULT_COOLDOWN_MS);
|
|
1088
1274
|
const cooldownGeneration = noteAnthropicCooldownMutation(failedAccountId);
|
|
1089
1275
|
upstreamHealth.set(failedAccountId, {
|
|
@@ -1093,6 +1279,10 @@ function createAnthropicRouting(instance: AnthropicInstanceId): AnthropicRouting
|
|
|
1093
1279
|
: resetDerived !== undefined ? "reset-derived" : "default",
|
|
1094
1280
|
...(resetDerived ? { rejectedQuotaWindows: resetDerived.rejectedQuotaWindows } : {}),
|
|
1095
1281
|
cooldownGeneration,
|
|
1282
|
+
recordedAt: now,
|
|
1283
|
+
refusalStatus: status,
|
|
1284
|
+
refusedQuotaWindows: status === 429 ? rejectedQuotaWindowsIn(rateLimitHeaders) : [],
|
|
1285
|
+
...(quotaResetAt !== undefined ? { quotaResetAt } : {}),
|
|
1096
1286
|
});
|
|
1097
1287
|
sweepExpiredOnWrite(now);
|
|
1098
1288
|
clearAnthropicSessionAffinityForAccount(failedAccountId);
|
|
@@ -1272,9 +1462,10 @@ function createAnthropicRouting(instance: AnthropicInstanceId): AnthropicRouting
|
|
|
1272
1462
|
instance, captureAnthropicManualSelectionGeneration, anthropicAccountPoolConfig,
|
|
1273
1463
|
isAnthropicAccountPoolEnabled, anthropicAutoSwitchThreshold, anthropicAccountAutoSwitchThreshold,
|
|
1274
1464
|
captureAnthropicCooldownRecovery, settleAnthropicCooldownRecovery, getAnthropicAccountHealthSnapshot,
|
|
1275
|
-
clearAnthropicAccountCooldown, sweepExpiredAnthropicRoutingHealth, clearAnthropicAccountPoolState,
|
|
1465
|
+
clearAnthropicAccountCooldown, clearAnthropicAccountCooldownForReset, sweepExpiredAnthropicRoutingHealth, clearAnthropicAccountPoolState,
|
|
1276
1466
|
anthropicSessionAffinitySizeForTests, getEligibleAnthropicAccounts, hasAnthropicFailoverQuorum,
|
|
1277
|
-
forgetAnthropicFailoverQuorum, getAnthropicPoolRetryAfterSeconds,
|
|
1467
|
+
forgetAnthropicFailoverQuorum, getAnthropicPoolRetryAfterSeconds, anthropicPoolCooldownError,
|
|
1468
|
+
anthropicPoolUsageLimitAfterRefusal, pickAlternateAnthropicAccount,
|
|
1278
1469
|
resolveAnthropicAccountForSession, resolveAnthropicDispatchAccountId, bindAnthropicSessionAffinity,
|
|
1279
1470
|
clearAnthropicSessionAffinityForAccount, rotateAnthropicAccountOn429, rotateAnthropicAccountOnRefusal,
|
|
1280
1471
|
recordAnthropicAccount429, recordAnthropicAccountRefusal, promoteAnthropicActiveAccount,
|
|
@@ -1352,6 +1543,14 @@ export function clearAnthropicAccountCooldown(accountId: string): boolean {
|
|
|
1352
1543
|
return anthropicRoutingFor("anthropic").clearAnthropicAccountCooldown(accountId);
|
|
1353
1544
|
}
|
|
1354
1545
|
|
|
1546
|
+
export function clearAnthropicAccountCooldownForReset(
|
|
1547
|
+
accountId: string,
|
|
1548
|
+
claimedAt: number,
|
|
1549
|
+
clearedWindows: ReadonlySet<AnthropicQuotaWindow>,
|
|
1550
|
+
): "cleared" | "absent" | "newer" | "uncovered" {
|
|
1551
|
+
return anthropicRoutingFor("anthropic").clearAnthropicAccountCooldownForReset(accountId, claimedAt, clearedWindows);
|
|
1552
|
+
}
|
|
1553
|
+
|
|
1355
1554
|
export function sweepExpiredAnthropicRoutingHealth(now = Date.now()): number {
|
|
1356
1555
|
return anthropicRoutingFor("anthropic").sweepExpiredAnthropicRoutingHealth(now);
|
|
1357
1556
|
}
|
|
@@ -1380,6 +1579,14 @@ export function getAnthropicPoolRetryAfterSeconds(now = Date.now(), decision: An
|
|
|
1380
1579
|
return anthropicRoutingFor("anthropic").getAnthropicPoolRetryAfterSeconds(now, decision, model);
|
|
1381
1580
|
}
|
|
1382
1581
|
|
|
1582
|
+
export function anthropicPoolCooldownError(now = Date.now(), decision: AnthropicRouteDecision | null = null, model?: string): AnthropicAccountCooldownError {
|
|
1583
|
+
return anthropicRoutingFor("anthropic").anthropicPoolCooldownError(now, decision, model);
|
|
1584
|
+
}
|
|
1585
|
+
|
|
1586
|
+
export function anthropicPoolUsageLimitAfterRefusal(now = Date.now(), decision: AnthropicRouteDecision | null = null, model?: string): AnthropicAccountCooldownError | null {
|
|
1587
|
+
return anthropicRoutingFor("anthropic").anthropicPoolUsageLimitAfterRefusal(now, decision, model);
|
|
1588
|
+
}
|
|
1589
|
+
|
|
1383
1590
|
export function pickAlternateAnthropicAccount(
|
|
1384
1591
|
config: OcxConfig,
|
|
1385
1592
|
excludeId: string,
|
package/src/oauth/anthropic.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/** Anthropic OAuth flow (Claude Pro/Max). Ported from jawcode oauth/anthropic.ts. */
|
|
2
2
|
import { OAuthCallbackFlow } from "./callback-server";
|
|
3
|
-
import { bindAnthropicIdentity, resolveAnthropicAccountIdentity } from "./anthropic-identity";
|
|
3
|
+
import { bindAnthropicIdentity, normalizeAnthropicIdentity, resolveAnthropicAccountIdentity } from "./anthropic-identity";
|
|
4
4
|
import { generatePKCE } from "./pkce";
|
|
5
5
|
import type { LocalTokenImportMode, OAuthController, OAuthCredentials } from "./types";
|
|
6
6
|
import { outboundProxyConfigured, startupOutboundProxyConfigured } from "../lib/proxy-env";
|
|
@@ -12,6 +12,8 @@ const AUTHORIZE_URL = "https://claude.ai/oauth/authorize";
|
|
|
12
12
|
const TOKEN_URL = "https://api.anthropic.com/v1/oauth/token";
|
|
13
13
|
const CALLBACK_PORT = 54545;
|
|
14
14
|
const CALLBACK_PATH = "/callback";
|
|
15
|
+
/** Profile enrichment budget on token refresh; login paths keep the resolver's own deadline. */
|
|
16
|
+
const REFRESH_PROFILE_TIMEOUT_MS = 2_000;
|
|
15
17
|
const SCOPES = "org:create_api_key user:profile user:inference";
|
|
16
18
|
|
|
17
19
|
// ── OAuth-request requirements applied by the anthropic adapter when authMode==="oauth" ──
|
|
@@ -130,6 +132,15 @@ function credsFrom(data: AnthropicTokenResponse, refreshFallback?: string): OAut
|
|
|
130
132
|
};
|
|
131
133
|
}
|
|
132
134
|
|
|
135
|
+
async function withAccountProfile(credential: OAuthCredentials, signal?: AbortSignal): Promise<OAuthCredentials> {
|
|
136
|
+
const profile = await resolveAnthropicAccountIdentity(credential.access, signal);
|
|
137
|
+
if (!profile) return credential;
|
|
138
|
+
// Token-response identity remains authoritative here; enrichment changes display metadata only.
|
|
139
|
+
const identity = normalizeAnthropicIdentity(credential.anthropicIdentity, credential.access);
|
|
140
|
+
if (identity && identity.accountUuid !== profile.accountUuid) return credential;
|
|
141
|
+
return { ...credential, plan: profile.plan ?? null };
|
|
142
|
+
}
|
|
143
|
+
|
|
133
144
|
export class AnthropicOAuthFlow extends OAuthCallbackFlow {
|
|
134
145
|
#verifier = "";
|
|
135
146
|
|
|
@@ -174,7 +185,7 @@ export class AnthropicOAuthFlow extends OAuthCallbackFlow {
|
|
|
174
185
|
redirect_uri: redirectUri,
|
|
175
186
|
code_verifier: this.#verifier,
|
|
176
187
|
});
|
|
177
|
-
return credsFrom(parseTokenResponse(responseBody));
|
|
188
|
+
return withAccountProfile(credsFrom(parseTokenResponse(responseBody)), this.ctrl.signal);
|
|
178
189
|
}
|
|
179
190
|
}
|
|
180
191
|
|
|
@@ -192,7 +203,8 @@ export async function loginAnthropic(
|
|
|
192
203
|
if (local.expires >= Date.now() + 60_000) {
|
|
193
204
|
const identity = await resolveAnthropicAccountIdentity(local.access, ctrl.signal);
|
|
194
205
|
ctrl.signal?.throwIfAborted();
|
|
195
|
-
return { ...local, ...(identity ? { accountId: identity.accountUuid,
|
|
206
|
+
return { ...local, ...(identity ? { accountId: identity.accountUuid,
|
|
207
|
+
anthropicIdentity: normalizeAnthropicIdentity(identity, local.access), plan: identity.plan ?? null } : {}) };
|
|
196
208
|
}
|
|
197
209
|
try {
|
|
198
210
|
return { ...(await refreshAnthropicToken(local.refresh)), source: "local-cli" };
|
|
@@ -218,5 +230,7 @@ export async function refreshAnthropicToken(refreshToken: string): Promise<OAuth
|
|
|
218
230
|
client_id: CLIENT_ID,
|
|
219
231
|
refresh_token: refreshToken,
|
|
220
232
|
});
|
|
221
|
-
|
|
233
|
+
// Refresh can sit on a request path, so the profile enrichment gets a short deadline. A
|
|
234
|
+
// missed read leaves plan unobserved and the stored label survives through mergeOAuthPlan.
|
|
235
|
+
return withAccountProfile(credsFrom(parseTokenResponse(responseBody), refreshToken), AbortSignal.timeout(REFRESH_PROFILE_TIMEOUT_MS));
|
|
222
236
|
}
|
|
@@ -41,6 +41,7 @@ import type { KiroRefusalKind } from "../adapters/kiro-refusal";
|
|
|
41
41
|
import { kiroAccountEvidence } from "../providers/kiro-usage";
|
|
42
42
|
import { kiroEvidenceIdentity } from "../providers/kiro-account-state-disk";
|
|
43
43
|
import { kiroAccountSupportsModel } from "../providers/kiro-model-catalog";
|
|
44
|
+
import { antigravityAccountSupportsModel } from "../providers/antigravity-account-roster";
|
|
44
45
|
import { ACCOUNT_QUOTA_TTL_MS } from "../providers/quota-wire";
|
|
45
46
|
import { sweepExpiredOnWrite } from "../lib/state-store-sweeper";
|
|
46
47
|
import { subscribeOAuthAccountPauseChanges } from "../lib/account-selection-events";
|
|
@@ -248,11 +249,31 @@ export function eligibleFailoverAccounts(providerName: string, now = Date.now(),
|
|
|
248
249
|
return eligibleIdsIn(getAccountSet(providerName), providerName, now, family);
|
|
249
250
|
}
|
|
250
251
|
|
|
251
|
-
/**
|
|
252
|
+
/** Whether an account's own model listing includes `model`; undefined when unknown. */
|
|
253
|
+
function accountListsModel(providerName: string, accountId: string, model: string): boolean | undefined {
|
|
254
|
+
if (providerName === "kiro") return kiroAccountSupportsModel(accountId, model);
|
|
255
|
+
if (providerName === "google-antigravity") return antigravityAccountSupportsModel(providerName, accountId, model);
|
|
256
|
+
return undefined;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Model evidence only ranks accounts already eligible and below any configured cap.
|
|
261
|
+
*
|
|
262
|
+
* Kiro and Antigravity both gate models per account. For Antigravity (Claude 4.6 vs 5.5, Argon)
|
|
263
|
+
* an account whose discovered roster lists the model wins; an account whose roster omits it is
|
|
264
|
+
* skipped while any other remains; an account with no roster yet is unknown and stays usable.
|
|
265
|
+
*/
|
|
252
266
|
export function preferKiroModelSupport(
|
|
253
267
|
providerName: string, ids: string[], model?: string | null, config?: OcxConfig,
|
|
254
268
|
): string[] {
|
|
255
|
-
if (
|
|
269
|
+
if (!model) return ids;
|
|
270
|
+
if (providerName === "google-antigravity") {
|
|
271
|
+
const positive = ids.filter(id => accountListsModel(providerName, id, model) === true);
|
|
272
|
+
if (positive.length) return positive;
|
|
273
|
+
const notListedAgainst = ids.filter(id => accountListsModel(providerName, id, model) !== false);
|
|
274
|
+
return notListedAgainst.length ? notListedAgainst : ids;
|
|
275
|
+
}
|
|
276
|
+
if (providerName !== "kiro") return ids;
|
|
256
277
|
const cap = config?.providers?.kiro?.oauthAccountFailover?.maxConcurrentPerAccount;
|
|
257
278
|
const withRoom = typeof cap === "number" && Number.isInteger(cap) && cap >= 1 && cap <= 100
|
|
258
279
|
? ids.filter(id => accountInFlight("kiro", id) < cap) : ids;
|
|
@@ -631,10 +652,13 @@ export function preferredInitialAccount(
|
|
|
631
652
|
const modelEligible = preferKiroModelSupport(providerName,
|
|
632
653
|
eligibleFailoverAccounts(providerName, now, classifyModelFamilyForQuota(providerName, requestedModelId)),
|
|
633
654
|
requestedModelId, config);
|
|
634
|
-
const activeLacks = providerName === "kiro"
|
|
635
|
-
&&
|
|
655
|
+
const activeLacks = (providerName === "kiro" || providerName === "google-antigravity")
|
|
656
|
+
&& !!active && !!requestedModelId
|
|
657
|
+
&& accountListsModel(providerName, active, requestedModelId) === false;
|
|
636
658
|
const listingSibling = activeLacks
|
|
637
|
-
? modelEligible.find(id => id !== active &&
|
|
659
|
+
? modelEligible.find(id => id !== active && (providerName === "kiro"
|
|
660
|
+
? kiroModelListingHasRoom(config, id, requestedModelId!)
|
|
661
|
+
: accountListsModel(providerName, id, requestedModelId!) === true))
|
|
638
662
|
: undefined;
|
|
639
663
|
if (listingSibling) return listingSibling;
|
|
640
664
|
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
*/
|
|
12
12
|
import { OAuthCallbackFlow, type OAuthCallbackFlowOptions } from "./callback-server";
|
|
13
13
|
import { generatePKCE } from "./pkce";
|
|
14
|
+
import { normalizeOAuthPlan } from "./plan";
|
|
14
15
|
import type { OAuthController, OAuthCredentials } from "./types";
|
|
15
16
|
import { antigravityUserAgent, ANTIGRAVITY_IDE_VERSION } from "../adapters/client-fingerprint";
|
|
16
17
|
|
|
@@ -94,7 +95,7 @@ function extractProjectId(data: Record<string, unknown> | undefined): string | u
|
|
|
94
95
|
return undefined;
|
|
95
96
|
}
|
|
96
97
|
|
|
97
|
-
async function loadCodeAssistProject(accessToken: string, signal?: AbortSignal): Promise<string | undefined> {
|
|
98
|
+
async function loadCodeAssistProject(accessToken: string, signal?: AbortSignal): Promise<{ projectId?: string; plan: string | null } | undefined> {
|
|
98
99
|
const response = await fetch(`${PROD_API}/${API_VERSION}:loadCodeAssist`, {
|
|
99
100
|
method: "POST",
|
|
100
101
|
headers: { Authorization: `Bearer ${accessToken}`, Accept: "*/*", "Content-Type": "application/json", "User-Agent": antigravityUserAgent() },
|
|
@@ -102,7 +103,12 @@ async function loadCodeAssistProject(accessToken: string, signal?: AbortSignal):
|
|
|
102
103
|
signal: requestSignal(signal),
|
|
103
104
|
});
|
|
104
105
|
if (!response.ok) return undefined;
|
|
105
|
-
|
|
106
|
+
const data: unknown = await response.json().catch(() => undefined);
|
|
107
|
+
if (!data || typeof data !== "object" || Array.isArray(data)) return undefined;
|
|
108
|
+
const payload = data as Record<string, unknown>;
|
|
109
|
+
const tier = (value: unknown) => value && typeof value === "object" && "id" in value
|
|
110
|
+
? normalizeOAuthPlan(value.id) : undefined;
|
|
111
|
+
return { projectId: extractProjectId(payload), plan: tier(payload.paidTier) ?? tier(payload.currentTier) ?? null };
|
|
106
112
|
}
|
|
107
113
|
|
|
108
114
|
async function onboardProject(accessToken: string, signal?: AbortSignal): Promise<string | undefined> {
|
|
@@ -138,7 +144,18 @@ async function onboardProject(accessToken: string, signal?: AbortSignal): Promis
|
|
|
138
144
|
|
|
139
145
|
/** Discover the CCA project for an access token (loadCodeAssist → onboardUser fallback). */
|
|
140
146
|
export async function discoverAntigravityProject(accessToken: string, signal?: AbortSignal): Promise<string | undefined> {
|
|
141
|
-
return (await
|
|
147
|
+
return (await discoverAntigravityAccount(accessToken, signal)).projectId;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
async function discoverAntigravityAccount(accessToken: string, signal?: AbortSignal): Promise<{ projectId?: string; plan?: string | null }> {
|
|
151
|
+
const account = await loadCodeAssistProject(accessToken, signal);
|
|
152
|
+
// Once loadCodeAssist answered, an onboarding failure must not discard its tier observation;
|
|
153
|
+
// a missing project still fails login, and cancellation still propagates.
|
|
154
|
+
const projectId = account?.projectId ?? await (account
|
|
155
|
+
? onboardProject(accessToken, signal).catch(error => { if (signal?.aborted) throw error; return undefined; })
|
|
156
|
+
: onboardProject(accessToken, signal));
|
|
157
|
+
// An unanswered loadCodeAssist observed nothing; only a parsed body may clear a stored label.
|
|
158
|
+
return { projectId, ...(account ? { plan: account.plan } : {}) };
|
|
142
159
|
}
|
|
143
160
|
|
|
144
161
|
function credentialsFromPayload(payload: GoogleTokenPayload, refreshFallback = ""): OAuthCredentials {
|
|
@@ -207,13 +224,13 @@ class AntigravityOAuthFlow extends OAuthCallbackFlow {
|
|
|
207
224
|
}, this.ctrl.signal);
|
|
208
225
|
const creds = credentialsFromPayload(payload);
|
|
209
226
|
this.ctrl.onProgress?.("Discovering Cloud Code Assist project");
|
|
210
|
-
const projectId = await
|
|
227
|
+
const { projectId, plan } = await discoverAntigravityAccount(creds.access, this.ctrl.signal);
|
|
211
228
|
if (!projectId) {
|
|
212
229
|
// Fail the login rather than persisting a credential that every request would reject for a
|
|
213
230
|
// missing CCA project — otherwise status shows "logged in" while all calls fail closed.
|
|
214
231
|
throw new Error("Antigravity login could not discover a Cloud Code Assist project for this account. Ensure the account has Antigravity/Cloud Code Assist access and try again.");
|
|
215
232
|
}
|
|
216
|
-
return { ...creds, projectId };
|
|
233
|
+
return { ...creds, projectId, plan };
|
|
217
234
|
}
|
|
218
235
|
}
|
|
219
236
|
|
|
@@ -231,8 +248,8 @@ export async function refreshAntigravityToken(refreshToken: string, signal?: Abo
|
|
|
231
248
|
}, signal);
|
|
232
249
|
const creds = credentialsFromPayload(payload, refreshToken);
|
|
233
250
|
// Re-discover the project on refresh so a newly-onboarded account fills in projectId.
|
|
234
|
-
const
|
|
235
|
-
return
|
|
251
|
+
const account = await discoverAntigravityAccount(creds.access, signal).catch(() => undefined);
|
|
252
|
+
return account ? { ...creds, ...account } : creds;
|
|
236
253
|
}
|
|
237
254
|
|
|
238
255
|
/**
|