@bitkyc08/opencodex 2.86.0 → 2.87.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/gui/dist/assets/App-ZfYbLHkB.js +58 -0
- package/gui/dist/assets/App-wT3mXqFz.css +1 -0
- package/gui/dist/assets/{Tray-DyR9yBee.js → Tray-DCd-A1az.js} +1 -1
- package/gui/dist/assets/index-CfYDMj34.js +94 -0
- package/gui/dist/assets/index-DPeB9HpV.css +1 -0
- package/gui/dist/assets/quota-summary-ChvmKGOk.js +1 -0
- package/gui/dist/index.html +2 -2
- package/package.json +1 -1
- package/src/adapters/anthropic/beta-allowlist.ts +9 -8
- package/src/adapters/anthropic/beta-body-pairs.ts +93 -0
- package/src/adapters/anthropic/passthrough.ts +7 -23
- package/src/adapters/anthropic-progress.ts +84 -0
- package/src/adapters/anthropic.ts +108 -21
- package/src/adapters/base.ts +2 -0
- package/src/adapters/claude-cli/adapter.ts +111 -23
- package/src/adapters/claude-cli/profiles.ts +2 -1
- package/src/adapters/claude-cli/stable-replay.ts +108 -0
- package/src/adapters/claude-cli/usage-admission.ts +230 -0
- package/src/adapters/coding-agent/protocol.ts +1 -1
- package/src/adapters/coding-agent/turn.ts +18 -2
- package/src/adapters/dsh-account.ts +97 -0
- package/src/adapters/google.ts +35 -8
- package/src/adapters/openai-chat/messages.ts +4 -3
- package/src/adapters/openai-chat/passthrough.ts +8 -2
- package/src/adapters/openai-chat/reasoning-replay.ts +52 -0
- package/src/adapters/openai-responses/compaction-search-history.ts +14 -7
- package/src/adapters/openai-responses/passthrough.ts +57 -125
- package/src/adapters/openai-responses/request-strips.ts +25 -3
- package/src/adapters/openai-responses/transport.ts +81 -0
- package/src/adapters/qoder/adapter.ts +4 -2
- package/src/adapters/registry.ts +5 -0
- package/src/adapters/zed.ts +26 -1
- package/src/advisor/activity.ts +55 -0
- package/src/advisor/budget.ts +62 -0
- package/src/advisor/consult.ts +4 -1
- package/src/advisor/context.ts +16 -4
- package/src/advisor/evidence-tool.ts +21 -0
- package/src/advisor/evidence.ts +117 -0
- package/src/advisor/runtime.ts +79 -31
- package/src/advisor/settings.ts +11 -2
- package/src/bridge/errors.ts +19 -1
- package/src/bridge/internal.ts +3 -1
- package/src/bridge/response-json.ts +18 -3
- package/src/bridge/sse.ts +58 -8
- package/src/bridge/terminal-refusal.ts +59 -0
- package/src/claude/desktop-3p-library.ts +32 -7
- package/src/claude/desktop-3p.ts +55 -13
- package/src/claude/desktop-first-party.ts +1 -1
- package/src/claude/desktop-picker-platform.ts +4 -0
- package/src/claude/desktop-picker.ts +15 -8
- package/src/claude/desktop-system-proxy.ts +50 -4
- package/src/claude/inbound-content-options.ts +11 -0
- package/src/claude/inbound.ts +33 -0
- package/src/claude/intercept/picker-ca-startup.ts +2 -1
- package/src/claude/intercept/picker-runtime.ts +6 -5
- package/src/claude/intercept/picker-trust-windows.ts +338 -0
- package/src/claude/intercept/picker-trust.ts +19 -0
- package/src/claude/intercept/port-policy.ts +30 -0
- package/src/claude/intercept/runtime.ts +97 -94
- package/src/claude/outbound.ts +38 -13
- package/src/cli/account-api.ts +1 -0
- package/src/cli/account-policy-dto.ts +6 -0
- package/src/cli/account-policy.ts +34 -5
- package/src/cli/account-preservation.ts +44 -0
- package/src/cli/account.ts +8 -2
- package/src/cli/advisor.ts +17 -3
- package/src/cli/capabilities-accounts.ts +20 -1
- package/src/cli/capabilities-agents-routing.ts +5 -2
- package/src/cli/capabilities-base.ts +38 -10
- package/src/cli/capabilities-observe-system.ts +63 -3
- package/src/cli/capabilities-provider-models.ts +11 -0
- package/src/cli/codex-cli-update.ts +151 -41
- package/src/cli/codex-provider.ts +44 -0
- package/src/cli/combo-input.ts +10 -1
- package/src/cli/combo-result.ts +1 -1
- package/src/cli/combo.ts +2 -1
- package/src/cli/cross-home-owner.ts +17 -14
- package/src/cli/dispatch.ts +25 -4
- package/src/cli/doctor.ts +6 -1
- package/src/cli/ensure-desktop-cold-start.ts +60 -7
- package/src/cli/help.ts +3 -2
- package/src/cli/index.ts +1 -1
- package/src/cli/integration-plan-dto.ts +1 -0
- package/src/cli/log-history.ts +172 -0
- package/src/cli/models-runtime-subcommands.ts +1 -0
- package/src/cli/models-runtime.ts +72 -8
- package/src/cli/observe-stream.ts +2 -0
- package/src/cli/observe.ts +16 -4
- package/src/cli/provider.ts +27 -5
- package/src/cli/registry.ts +28 -8
- package/src/cli/runtime-api.ts +5 -3
- package/src/cli/system-command.ts +40 -8
- package/src/cli/uninstall-client-state.ts +32 -0
- package/src/cli/usage-compare.ts +309 -0
- package/src/cli/usage-model-search.ts +4 -4
- package/src/cli/usage-report.ts +2 -2
- package/src/cli/v2.ts +1 -1
- package/src/client/machine-listener.ts +1 -3
- package/src/client/runtime.ts +1 -0
- package/src/clients/client-path.ts +51 -0
- package/src/clients/config-export/contracts.ts +2 -1
- package/src/clients/config-export.ts +18 -44
- package/src/clients/gentle-shell-path.ts +87 -0
- package/src/codex/account-auto-switch.ts +2 -0
- package/src/codex/account-identity.ts +29 -0
- package/src/codex/account-label.ts +1 -1
- package/src/codex/account-lifecycle.ts +21 -5
- package/src/codex/account-preservation.ts +28 -0
- package/src/codex/account-use-remaining.ts +23 -0
- package/src/codex/app-server-processes.ts +97 -7
- package/src/codex/app-server-restart-service.ts +55 -4
- package/src/codex/auth-api/account-list.ts +4 -0
- package/src/codex/auth-api/account-pause-group.ts +6 -50
- package/src/codex/auth-api/account-preservation.ts +57 -0
- package/src/codex/auth-api/routes.ts +8 -0
- package/src/codex/auth-api/use-remaining-route.ts +36 -0
- package/src/codex/auth-api.ts +0 -1
- package/src/codex/auth-context.ts +2 -2
- package/src/codex/catalog/auto-review.ts +20 -3
- package/src/codex/catalog/build-entries.ts +9 -1
- package/src/codex/catalog/combo-member.ts +14 -5
- package/src/codex/catalog/configured-tiers.ts +1 -1
- package/src/codex/catalog/gather-capture.ts +2 -0
- package/src/codex/catalog/metadata.ts +11 -9
- package/src/codex/catalog/model-hints.ts +21 -16
- package/src/codex/catalog/model-visibility.ts +3 -1
- package/src/codex/catalog/parsing.ts +30 -86
- package/src/codex/catalog/provider-models.ts +36 -34
- package/src/codex/catalog/routed-gather.ts +12 -13
- package/src/codex/cli-install-provenance.ts +105 -17
- package/src/codex/cli-update-plan.ts +545 -0
- package/src/codex/config-write-lock.ts +23 -2
- package/src/codex/desktop-app/picker-freshness.ts +96 -0
- package/src/codex/desktop-app-restart.ts +21 -0
- package/src/codex/desktop-compatibility/installed-build.ts +24 -6
- package/src/codex/desktop-compatibility/routing-preflight.ts +1 -1
- package/src/codex/desktop-compatibility/runtime.ts +12 -3
- package/src/codex/desktop-compatibility/windows-key-protection.ts +115 -9
- package/src/codex/features.ts +85 -45
- package/src/codex/inject/history-failure.ts +27 -0
- package/src/codex/inject/restore.ts +24 -2
- package/src/codex/inject/routing-classify.ts +4 -3
- package/src/codex/inject.ts +18 -27
- package/src/codex/main-account-hard-lock.ts +30 -1
- package/src/codex/native-profile-startup.ts +1 -1
- package/src/codex/ocx-compaction-history.ts +12 -10
- package/src/codex/paths.ts +86 -1
- package/src/codex/provider-routing.ts +174 -0
- package/src/codex/refresh.ts +2 -2
- package/src/codex/routing/health-store.ts +1 -1
- package/src/codex/routing/selection.ts +4 -3
- package/src/codex/routing/thread-affinity.ts +1 -1
- package/src/codex/signed-in-identity.ts +43 -0
- package/src/codex/subagent-model-fallback.ts +1 -1
- package/src/codex/sync.ts +6 -1
- package/src/codex/user-identity.ts +16 -5
- package/src/codex/windows-local-appdata.ts +84 -0
- package/src/combos/failover.ts +6 -1
- package/src/combos/index.ts +1 -0
- package/src/combos/resolve.ts +46 -10
- package/src/combos/types.ts +15 -0
- package/src/config/diagnostics.ts +33 -6
- package/src/config/live-reconcile.ts +38 -2
- package/src/config/load-degrade.ts +17 -0
- package/src/config/opencode-wire-upgrade.ts +54 -0
- package/src/config/opencode-zen-upgrade.ts +52 -0
- package/src/config/owner-registry.ts +22 -17
- package/src/config/provider-validation.ts +45 -2
- package/src/config/schema/config-schema.ts +22 -0
- package/src/config/schema/leaf-validators.ts +38 -9
- package/src/config/schema/main-account-hard-lock-fallback.ts +26 -0
- package/src/config/schema/native-reasoning-retention.ts +26 -0
- package/src/config.ts +2 -0
- package/src/generated/compatibility-version.json +608 -284
- package/src/grok/sync.ts +1 -1
- package/src/images/loop.ts +46 -8
- package/src/integrations/catalog-refresh.ts +1 -1
- package/src/integrations/mutation-plan.ts +1 -0
- package/src/integrations/registry.ts +8 -0
- package/src/lib/codex-restart-contract.ts +15 -0
- package/src/lib/desktop-upstream-tunnel.ts +18 -53
- package/src/lib/empty-tool-catalog.ts +62 -0
- package/src/lib/errors.ts +19 -3
- package/src/lib/gcp-adc.ts +96 -9
- package/src/lib/gcp-credential-input.ts +12 -0
- package/src/lib/lab-live-route-production.ts +5 -0
- package/src/lib/opaque-proxy-tunnel.ts +217 -0
- package/src/lib/redact.ts +2 -2
- package/src/oauth/account-quota-rank.ts +69 -2
- package/src/oauth/anthropic-account-refusal.ts +20 -2
- package/src/oauth/anthropic-oauth-definitions.ts +3 -1
- package/src/oauth/anthropic-routing.ts +1 -21
- package/src/oauth/anthropic.ts +38 -7
- package/src/oauth/callback-server.ts +27 -1
- package/src/oauth/chatgpt-device.ts +1 -1
- package/src/oauth/chatgpt-shared.ts +83 -0
- package/src/oauth/chatgpt.ts +8 -72
- package/src/oauth/dsh.ts +203 -0
- package/src/oauth/generic-account-failover.ts +33 -7
- package/src/oauth/generic-session-affinity.ts +125 -0
- package/src/oauth/index.ts +184 -109
- package/src/oauth/local-token-detect.ts +21 -1
- package/src/oauth/login-cli-errors.ts +44 -0
- package/src/oauth/login-cli.ts +69 -7
- package/src/oauth/login-flow-state.ts +371 -23
- package/src/oauth/meta-muse-errors.ts +41 -0
- package/src/oauth/meta-muse-keychain.ts +52 -0
- package/src/oauth/meta-muse.ts +51 -84
- package/src/oauth/model-discovery-auth.ts +3 -0
- package/src/oauth/pool-settings-capability.ts +4 -2
- package/src/oauth/refresh-flight-retirement.ts +48 -0
- package/src/oauth/session-affinity-key.ts +23 -0
- package/src/oauth/store-anthropic-instance.ts +9 -2
- package/src/oauth/store.ts +12 -5
- package/src/protocols/encoders/adapter-events.ts +5 -4
- package/src/protocols/encoders/messages.ts +9 -6
- package/src/protocols/provider-summary.ts +2 -1
- package/src/providers/api-key-cooldowns.ts +49 -0
- package/src/providers/api-key-resolve.ts +11 -6
- package/src/providers/api-key-response-ownership.ts +57 -0
- package/src/providers/api-key-selection.ts +31 -6
- package/src/providers/api-keys.ts +214 -3
- package/src/providers/claude-cli-usage.ts +96 -0
- package/src/providers/dsh-account-destination.ts +31 -0
- package/src/providers/dsh-account-models.ts +41 -0
- package/src/providers/gcp-credential-cleanup.ts +59 -0
- package/src/providers/key-failover.ts +54 -50
- package/src/providers/key-store.ts +7 -2
- package/src/providers/model-rename-fields.ts +3 -0
- package/src/providers/model-rename-migration.ts +2 -2
- package/src/providers/openai-tiers-destination.ts +11 -2
- package/src/providers/openai-tiers.ts +5 -12
- package/src/providers/opencode-go-transport.ts +3 -1
- package/src/providers/quota/account-cache.ts +6 -5
- package/src/providers/quota/account-row.ts +34 -0
- package/src/providers/quota/antigravity.ts +28 -7
- package/src/providers/quota/dsh-account.ts +70 -0
- package/src/providers/quota/vendor-probes-oauth.ts +1 -93
- package/src/providers/quota-types.ts +2 -0
- package/src/providers/quota.ts +14 -20
- package/src/providers/registry/entries-core.ts +37 -8
- package/src/providers/registry/types.ts +1 -1
- package/src/providers/request-pacing.ts +77 -19
- package/src/providers/routed-config.ts +339 -0
- package/src/providers/service-tier.ts +2 -1
- package/src/quota/window-mapping.ts +5 -2
- package/src/responses/anthropic-progress.ts +34 -0
- package/src/responses/assistant-replay.ts +35 -0
- package/src/responses/compaction-history-framing.ts +65 -0
- package/src/responses/compaction.ts +6 -1
- package/src/responses/incomplete-reason.ts +31 -0
- package/src/responses/parser.ts +50 -5
- package/src/responses/reasoning-envelope.ts +4 -0
- package/src/responses/reasoning-replay-cache.ts +24 -2
- package/src/responses/reasoning-retention.ts +173 -0
- package/src/responses/retained-compaction.ts +46 -0
- package/src/responses/schema.ts +23 -0
- package/src/router.ts +8 -321
- package/src/routing/health.ts +37 -22
- package/src/routing/history/indexer.ts +6 -5
- package/src/routing/history/ingest.ts +6 -2
- package/src/routing/profile.ts +1 -1
- package/src/server/admission-model-scope.ts +17 -0
- package/src/server/auth-cors.ts +11 -0
- package/src/server/chat-completions.ts +7 -1
- package/src/server/chat-native-reasoning.ts +123 -0
- package/src/server/chat-native-sse.ts +110 -7
- package/src/server/chat-native.ts +76 -17
- package/src/server/claude-messages.ts +55 -414
- package/src/server/effort-policy.ts +1 -1
- package/src/server/images.ts +15 -2
- package/src/server/index/model-alias-id.ts +36 -0
- package/src/server/index/optional-listeners.ts +2 -0
- package/src/server/index/serve-options.ts +9 -1
- package/src/server/index/startup-warnings.ts +2 -0
- package/src/server/index.ts +1 -1
- package/src/server/inference/advisor-diagnostics.ts +31 -0
- package/src/server/management/advisor-routes.ts +15 -4
- package/src/server/management/claude-desktop-picker-routes.ts +1 -1
- package/src/server/management/combo-routes.ts +176 -50
- package/src/server/management/companion-routes.ts +1 -1
- package/src/server/management/config-routes.ts +75 -4
- package/src/server/management/cursor-integration-routes.ts +1 -1
- package/src/server/management/model-routes.ts +11 -6
- package/src/server/management/model-rows.ts +14 -0
- package/src/server/management/native-integration-routes.ts +2 -7
- package/src/server/management/native-reasoning-retention-routes.ts +41 -0
- package/src/server/management/oauth-account-routes.ts +87 -14
- package/src/server/management/provider-gcp-credential-routes.ts +29 -0
- package/src/server/management/provider-key-pacing-routes.ts +31 -0
- package/src/server/management/provider-overwrite-carry.ts +7 -0
- package/src/server/management/provider-routes.ts +25 -30
- package/src/server/management/route-registry.ts +10 -1
- package/src/server/management-api.ts +3 -1
- package/src/server/messages-native-tool-guard.ts +185 -0
- package/src/server/messages-native.ts +101 -21
- package/src/server/messages-shared.ts +431 -0
- package/src/server/relay.ts +1 -1
- package/src/server/request-log-terminal-status.ts +1 -1
- package/src/server/request-log.ts +6 -1
- package/src/server/responses/account-change-state.ts +8 -2
- package/src/server/responses/adapter-continuation.ts +13 -1
- package/src/server/responses/adapter-delivery.ts +19 -6
- package/src/server/responses/adapter-dispatch.ts +19 -2
- package/src/server/responses/advisor-retention.ts +117 -0
- package/src/server/responses/advisor-slot.ts +73 -41
- package/src/server/responses/codex-ws-wire.ts +15 -8
- package/src/server/responses/compact.ts +303 -125
- package/src/server/responses/compaction-recovery.ts +18 -10
- package/src/server/responses/compaction-retention.ts +61 -0
- package/src/server/responses/context-overflow.ts +11 -6
- package/src/server/responses/core-auth.ts +6 -0
- package/src/server/responses/core-codex-account.ts +11 -1
- package/src/server/responses/core-combo-failure.ts +2 -0
- package/src/server/responses/core-combo.ts +120 -36
- package/src/server/responses/core-errors.ts +3 -3
- package/src/server/responses/core-lifetime.ts +5 -2
- package/src/server/responses/core-opaque-recovery.ts +35 -0
- package/src/server/responses/core-options.ts +5 -0
- package/src/server/responses/core-pipeline.ts +141 -0
- package/src/server/responses/core-replay.ts +18 -2
- package/src/server/responses/core.ts +2 -115
- package/src/server/responses/fetch-helpers.ts +73 -19
- package/src/server/responses/main-hard-lock-routing.ts +109 -0
- package/src/server/responses/native-response-control.ts +4 -2
- package/src/server/responses/passthrough-delivery.ts +3 -1
- package/src/server/responses/passthrough-dispatch.ts +15 -13
- package/src/server/responses/request-prepare.ts +76 -12
- package/src/server/responses/request-sidecar-auth.ts +2 -2
- package/src/server/responses/request-transport.ts +104 -27
- package/src/server/responses/response-effects.ts +1 -0
- package/src/server/responses/run-turn-execution.ts +96 -30
- package/src/server/responses/sidecar-execution.ts +20 -2
- package/src/server/responses/terminal-guard.ts +2 -16
- package/src/service/auth-preflight.ts +71 -0
- package/src/service/cli.ts +4 -2
- package/src/service/definition.ts +97 -0
- package/src/service/guards.ts +1 -61
- package/src/service/health-sentinel-linux.ts +112 -0
- package/src/service/health-sentinel-main.ts +76 -57
- package/src/service/health.ts +5 -89
- package/src/service/launchd.ts +27 -7
- package/src/service/logging.ts +72 -9
- package/src/service/repair.ts +2 -1
- package/src/service/state.ts +7 -8
- package/src/service/systemd.ts +1 -1
- package/src/service/windows-guarded-manager.ts +2 -1
- package/src/service/windows-ops.ts +1 -1
- package/src/service/windows-scheduler.ts +2 -7
- package/src/service/windows-taskxml.ts +2 -2
- package/src/service/windows-wscript.ts +14 -0
- package/src/service.ts +4 -2
- package/src/sidecar/auth.ts +2 -0
- package/src/types/config.ts +64 -3
- package/src/types/opencode-go.ts +61 -0
- package/src/types/opencode-zen.ts +29 -0
- package/src/types/provider.ts +32 -6
- package/src/types/request.ts +23 -2
- package/src/types/wire.ts +29 -37
- package/src/types.ts +1 -0
- package/src/update/npm-invocation.mjs +12 -2
- package/src/usage/cost.ts +48 -12
- package/src/usage/expected-prices.ts +3 -0
- package/src/usage/ledger-retention.ts +2 -1
- package/src/usage/ledger-scanner.ts +6 -5
- package/src/usage/log.ts +17 -8
- package/src/usage/summary.ts +22 -9
- package/src/usage/user-cost-overlays.ts +14 -19
- package/src/usage/user-cost-validation.ts +38 -0
- package/src/vision/eligibility.ts +13 -0
- package/src/web-search/loop.ts +55 -5
- package/src/web-search/progress-stream.ts +4 -1
- package/src/web-search/run-turn-loop.ts +9 -1
- package/gui/dist/assets/App-BjlttVLy.js +0 -58
- package/gui/dist/assets/App-CSJvmjNs.css +0 -1
- package/gui/dist/assets/index-BB6uK6tU.css +0 -1
- package/gui/dist/assets/index-BMssP4k3.js +0 -94
- package/gui/dist/assets/quota-summary-D8M1bc_R.js +0 -1
|
@@ -27,10 +27,10 @@ import {
|
|
|
27
27
|
anthropicRoutingFor,
|
|
28
28
|
AnthropicAccountCooldownError,
|
|
29
29
|
formatAnthropicProviderForLog,
|
|
30
|
-
anthropicSessionKeyFromParts,
|
|
31
30
|
} from "../../oauth/anthropic-routing";
|
|
32
31
|
import {
|
|
33
32
|
OAuthAccountPausedError,
|
|
33
|
+
OAuthTokenRefreshBusyError,
|
|
34
34
|
getValidAccessSnapshotForAccount,
|
|
35
35
|
forceRefreshOAuthAccessSnapshot,
|
|
36
36
|
getValidAccessTokenSnapshot,
|
|
@@ -49,6 +49,9 @@ import {
|
|
|
49
49
|
refusalAwareInitialKiroAccount,
|
|
50
50
|
noteGenericPoolSelection,
|
|
51
51
|
} from "../../oauth/generic-account-failover";
|
|
52
|
+
import { admitGenericSessionAccount, bindGenericSessionAccount, genericSessionAccount, genericSessionAccountEligible, genericSessionAffinityEnabled, preferredGenericSessionAccount } from "../../oauth/generic-session-affinity";
|
|
53
|
+
import { oauthSessionKeyFromParts } from "../../oauth/session-affinity-key";
|
|
54
|
+
import { resolveContextPrincipal } from "../auth-cors";
|
|
52
55
|
import { tryKiroAlternateAfterTerminalRefresh } from "../../oauth/kiro-terminal-failover";
|
|
53
56
|
import { classifyModelFamilyForQuota } from "../../oauth/account-quota-rank";
|
|
54
57
|
import { expandInferenceOAuthSendBudget } from "../inference/context";
|
|
@@ -78,6 +81,7 @@ import { bindRouteReasoningReplayScope } from "./core-replay";
|
|
|
78
81
|
import { sessionIdHeaderFromRequest, normalizeLogConversationId } from "../request-log-conversation";
|
|
79
82
|
import { redactSecretString } from "../../lib/redact";
|
|
80
83
|
import { selectProactiveApiKeyTransport } from "../../providers/key-failover";
|
|
84
|
+
import { distributedKeyPhysicalFetch } from "../../providers/api-key-response-ownership";
|
|
81
85
|
import { isCanonicalOpenAiForwardProvider } from "../../providers/openai-tiers";
|
|
82
86
|
import { providerConsumesCallerAuthorization } from "../../providers/caller-authorization";
|
|
83
87
|
import { releaseCodexAuthContextProbeLease, stripCodexRuntimeProviderFields } from "../../codex/auth-context";
|
|
@@ -120,6 +124,7 @@ export async function prepareResponsesTransport(
|
|
|
120
124
|
) {
|
|
121
125
|
const { config, logCtx, options, req } = requestContext;
|
|
122
126
|
const { route, parsed, inboundWire, translatorBudget } = requestState;
|
|
127
|
+
const genericAffinityMode = genericSessionAffinityEnabled(config, route.providerName);
|
|
123
128
|
const anthropicInstance = route.provider.authMode === "oauth"
|
|
124
129
|
? configuredAnthropicInstance(config, route.providerName) : undefined;
|
|
125
130
|
// Refuse custom/orphan B OAuth before any generic resolver can observe its auth row.
|
|
@@ -176,6 +181,7 @@ export async function prepareResponsesTransport(
|
|
|
176
181
|
|| route.providerName === "orcarouter-oauth"
|
|
177
182
|
// runTurn transport: the replay runs on the first-event preflight in run-turn-execution.
|
|
178
183
|
|| route.providerName === "devin"
|
|
184
|
+
|| route.providerName === "dsh-account"
|
|
179
185
|
) && route.provider.authMode === "oauth";
|
|
180
186
|
let sentOAuthSnapshot: OAuthAccessSnapshot | undefined;
|
|
181
187
|
// One inference-401 replay belongs to this request, even when Auto changes exchange wire.
|
|
@@ -217,6 +223,8 @@ export async function prepareResponsesTransport(
|
|
|
217
223
|
for (let attempt = 0; attempt < maxSelectionAttempts; attempt++) {
|
|
218
224
|
requireAnthropicAdmission();
|
|
219
225
|
if (candidate.provider !== route.providerName) return null;
|
|
226
|
+
if (genericAffinityMode && route.provider.googleMode === "cloud-code-assist" && !candidate.projectId)
|
|
227
|
+
throw new OAuthLoginRequiredError(route.providerName);
|
|
220
228
|
if (anthropicInstance) anthropicRouteDecision = currentAnthropicRouteDecision();
|
|
221
229
|
if (!oauthSelection) return null;
|
|
222
230
|
const proactiveEnabled = anthropicInstance !== undefined
|
|
@@ -234,7 +242,9 @@ export async function prepareResponsesTransport(
|
|
|
234
242
|
if (candidate.provider !== route.providerName) return null;
|
|
235
243
|
if (anthropicInstance) anthropicRouteDecision = currentAnthropicRouteDecision();
|
|
236
244
|
if (anthropicRouteDecision && !routeCandidates(anthropicRouting!.getEligibleAnthropicAccounts(Date.now(), route.modelId), anthropicRouteDecision).includes(candidate.accountId)) return null;
|
|
237
|
-
const committed =
|
|
245
|
+
const committed = genericAffinityMode
|
|
246
|
+
? admitGenericSessionAccount(config, route.providerName, oauthSelection, candidate, route.modelId, genericAffinityMode)
|
|
247
|
+
: await commitOAuthAccountSelection(route.providerName, candidate.accountId, {
|
|
238
248
|
expectedSelection: oauthSelection,
|
|
239
249
|
expectedCredentialGeneration: candidate.generation,
|
|
240
250
|
requireUsableAccount: true,
|
|
@@ -248,14 +258,18 @@ export async function prepareResponsesTransport(
|
|
|
248
258
|
)) return null;
|
|
249
259
|
oauthSelection = committed;
|
|
250
260
|
servingOAuthSnapshot = candidate;
|
|
261
|
+
bindGenericSessionAccount(config, route.providerName, genericSessionKey, candidate.accountId, route.modelId);
|
|
251
262
|
if (anthropicInstance) replayOAuthCredentialSnapshot = { accountId: candidate.accountId, generation: candidate.generation };
|
|
252
263
|
forgetGenericFailoverRoster(route.providerName);
|
|
253
264
|
return candidate;
|
|
254
265
|
}
|
|
255
266
|
// A newer manual choice wins over this request's old proposal, including A→B→A.
|
|
256
267
|
// Resolve that choice, not the rejected candidate, before trying admission again.
|
|
268
|
+
const previousSelection = oauthSelection;
|
|
257
269
|
oauthSelection = captureOAuthAccountSelection(route.providerName);
|
|
258
270
|
if (!oauthSelection) return null;
|
|
271
|
+
const selectionChanged = oauthSelection.accountId !== previousSelection.accountId
|
|
272
|
+
|| oauthSelection.revision !== previousSelection.revision;
|
|
259
273
|
// A revision also changes on per-account policy edits. Re-evaluate the selector
|
|
260
274
|
// after credential waits even without a model route, rather than reusing stale active.
|
|
261
275
|
const revisedAnthropic = anthropicInstance !== undefined
|
|
@@ -263,9 +277,15 @@ export async function prepareResponsesTransport(
|
|
|
263
277
|
const revisedAnthropicId = revisedAnthropic?.accountId;
|
|
264
278
|
if (anthropicInstance !== undefined && !revisedAnthropicId) return null;
|
|
265
279
|
if (revisedAnthropic) anthropicReason = revisedAnthropic.reason;
|
|
280
|
+
const genericAccountId = genericAffinityMode && !selectionChanged
|
|
281
|
+
? (genericSessionAccountEligible(config, route.providerName, candidate.accountId, route.modelId, Date.now(), true)
|
|
282
|
+
? candidate.accountId : (preferredGenericSessionAccount(config, route.providerName, genericSessionKey, route.modelId)
|
|
283
|
+
?? oauthSelection.accountId))
|
|
284
|
+
: oauthSelection.accountId;
|
|
285
|
+
if (!genericAccountId && !anthropicInstance) return null;
|
|
266
286
|
candidate = anthropicInstance !== undefined
|
|
267
287
|
? await anthropicRouting!.getAnthropicPoolAccessSnapshot(revisedAnthropicId ?? oauthSelection.accountId)
|
|
268
|
-
: await getValidAccessSnapshotForAccount(route.providerName,
|
|
288
|
+
: await getValidAccessSnapshotForAccount(route.providerName, genericAccountId!, { requireUsableAccount: true });
|
|
269
289
|
if (route.provider.googleMode === "cloud-code-assist" && !candidate.projectId) return null;
|
|
270
290
|
}
|
|
271
291
|
return null;
|
|
@@ -275,7 +295,10 @@ export async function prepareResponsesTransport(
|
|
|
275
295
|
// Keyed on the account, not the selection revision: a selection that moved away and back
|
|
276
296
|
// (A -> B -> A) before the 401 landed still serves the rejected credential, and skipping
|
|
277
297
|
// the refresh would replay it and spend the one recovery attempt.
|
|
278
|
-
const candidate = current?.accountId === sent.accountId
|
|
298
|
+
const candidate = (current?.accountId === sent.accountId
|
|
299
|
+
|| (genericAffinityMode && current?.revision === oauthSelection?.revision
|
|
300
|
+
&& current?.accountId === oauthSelection?.accountId && servingOAuthSnapshot?.accountId === sent.accountId))
|
|
301
|
+
? await forceRefreshOAuthAccessSnapshot(sent) : sent;
|
|
279
302
|
const admitted = await commitResolvedOAuthSelection(candidate);
|
|
280
303
|
if (!admitted) throw new Error("OAuth selection changed during credential recovery");
|
|
281
304
|
if (kiroLoadEnabled && options.accountLoad?.lease?.accountId !== admitted.accountId) {
|
|
@@ -519,6 +542,8 @@ export async function prepareResponsesTransport(
|
|
|
519
542
|
return selected?.accountId === binding.selection.accountId && selected?.revision === binding.selection.revision
|
|
520
543
|
&& !!row && !row.paused && !row.needsReauth && row.credential.expires > Date.now()
|
|
521
544
|
&& (anthropicInstance === undefined || !anthropicRouting!.getAnthropicAccountHealthSnapshot(binding.snapshot.accountId) && !anthropicRatePolicy!.anthropicRatePauseUntil(binding.snapshot.accountId) && !anthropicQuota!.anthropicFamilyRejected(binding.snapshot.accountId, route.modelId))
|
|
545
|
+
&& (!genericAffinityMode
|
|
546
|
+
|| genericSessionAccountEligible(config, route.providerName, binding.snapshot.accountId, route.modelId, Date.now(), true))
|
|
522
547
|
&& credentialGeneration(row.credential) === binding.snapshot.generation;
|
|
523
548
|
};
|
|
524
549
|
/** Bind the negotiated wire adapter and its built requests to the serving credential's authority. */
|
|
@@ -592,12 +617,23 @@ export async function prepareResponsesTransport(
|
|
|
592
617
|
const accountId = await anthropicRouting!.resolveAnthropicDispatchAccountId(config, anthropicSessionKey, anthropicRouteDecision, route.modelId);
|
|
593
618
|
candidate = await anthropicRouting!.getAnthropicPoolAccessSnapshot(accountId);
|
|
594
619
|
}
|
|
620
|
+
if (genericAffinityMode
|
|
621
|
+
&& (!candidate || !genericSessionAccountEligible(config, route.providerName, candidate.accountId, route.modelId, Date.now(), true))) {
|
|
622
|
+
const accountId = preferredGenericSessionAccount(config, route.providerName, genericSessionKey, route.modelId)
|
|
623
|
+
?? captureOAuthAccountSelection(route.providerName)?.accountId;
|
|
624
|
+
candidate = accountId ? await getValidAccessSnapshotForAccount(route.providerName, accountId, { requireUsableAccount: true }) : undefined;
|
|
625
|
+
}
|
|
595
626
|
if (!candidate || !await applyFailoverSnapshot(candidate, requestParsed, true)) {
|
|
596
627
|
if (anthropicInstance !== undefined) await anthropicRouting!.resolveAnthropicDispatchAccountId(config, anthropicSessionKey, anthropicRouteDecision, route.modelId);
|
|
597
628
|
throw new Error("OAuth account selection changed before dispatch");
|
|
598
629
|
}
|
|
599
630
|
} else {
|
|
600
|
-
|
|
631
|
+
let current = resolveCurrentProviderApiKeyTransport(config, route.providerName, route.provider);
|
|
632
|
+
if (!current && (route.provider._apiKeyAttempt?.distributed || (route.provider.requestPacing?.enabled
|
|
633
|
+
&& (route.provider.requestPacing.keys || route.provider.requestPacing.keySelection === "round-robin")))) {
|
|
634
|
+
const fresh = resolveCurrentProviderApiKeyTransport(config, route.providerName, { ...route.provider, _apiKeyAttempt: undefined });
|
|
635
|
+
if (fresh) current = selectProactiveApiKeyTransport(config, route.providerName, fresh) ?? fresh;
|
|
636
|
+
}
|
|
601
637
|
if (!current) throw new Error("API key selection is unavailable before dispatch");
|
|
602
638
|
route.provider = current;
|
|
603
639
|
await resolveCopilotSelection(requestParsed, true);
|
|
@@ -625,7 +661,10 @@ export async function prepareResponsesTransport(
|
|
|
625
661
|
? (options.sendBudget as import("../../lib/request-execution-budget").RequestExecutionBudget).beginSpendProducer?.() : undefined;
|
|
626
662
|
try {
|
|
627
663
|
for (let attempt = 0; attempt < 3; attempt++) {
|
|
628
|
-
|
|
664
|
+
incoming.abortSignal?.throwIfAborted();
|
|
665
|
+
const selectionRefreshed = !selectionIsCurrent(adapterBindings.get(selectedAdapter));
|
|
666
|
+
if (attempt === 0 && selectionRefreshed) releaseProviderRequestSlot(incoming.pacingSlot);
|
|
667
|
+
if (selectionRefreshed) selectedAdapter = await refreshRunTurnAdapter(requestParsed);
|
|
629
668
|
const binding = adapterBindings.get(selectedAdapter);
|
|
630
669
|
const run = rawRunTurns.get(selectedAdapter);
|
|
631
670
|
if (!run) throw new Error("Selected provider no longer supports this turn transport");
|
|
@@ -635,24 +674,39 @@ export async function prepareResponsesTransport(
|
|
|
635
674
|
// Both main and image-loop callers already acquired the initial pacing slot.
|
|
636
675
|
// Subsequent physical messages retain this adapter/credential and are paced normally.
|
|
637
676
|
try {
|
|
638
|
-
|
|
639
|
-
|
|
677
|
+
// A pre-acquired lease belongs to the original selection, never its replacement.
|
|
678
|
+
attemptSlot = attempt === 0 && !selectionRefreshed && incoming.pacingSlot
|
|
679
|
+
? incoming.pacingSlot
|
|
640
680
|
: await waitForProviderRequestSlot(route.providerName, route.provider, route.modelId, incoming.abortSignal);
|
|
681
|
+
incoming.abortSignal?.throwIfAborted();
|
|
682
|
+
// Every admission can queue, including a replacement's. Never enter an
|
|
683
|
+
// adapter on a lease whose credential changed while it waited.
|
|
684
|
+
if (!selectionIsCurrent(binding)) {
|
|
685
|
+
refused = true;
|
|
686
|
+
throw new Error("Account selection changed before turn admission");
|
|
687
|
+
}
|
|
688
|
+
const beforePhysicalSend = (): void => {
|
|
689
|
+
incoming.abortSignal?.throwIfAborted();
|
|
690
|
+
if (sent) {
|
|
691
|
+
if (binding?.kind === "api-key" && binding.provider._apiKeyAttempt?.distributed
|
|
692
|
+
&& !selectionIsCurrent(binding)) throw new Error("API key became unavailable during the turn");
|
|
693
|
+
return;
|
|
694
|
+
}
|
|
695
|
+
if (!selectionIsCurrent(binding)) {
|
|
696
|
+
refused = true;
|
|
697
|
+
throw new Error("Account selection changed before the first turn dispatch");
|
|
698
|
+
}
|
|
699
|
+
incoming.beforePhysicalSend?.();
|
|
700
|
+
commitKeyAttemptSend();
|
|
701
|
+
sent = true;
|
|
702
|
+
};
|
|
641
703
|
const fetch = providerFetch(route.provider, options.codexWsRuntimeIdentity, {
|
|
642
704
|
providerName: route.providerName, modelId: route.modelId, pacingSlotAcquired: true,
|
|
643
705
|
pacingSlot: attemptSlot,
|
|
644
706
|
turnScopedPacing: true,
|
|
645
|
-
beforeDispatch:
|
|
646
|
-
if (sent) return;
|
|
647
|
-
if (!selectionIsCurrent(binding)) {
|
|
648
|
-
refused = true;
|
|
649
|
-
throw new Error("Account selection changed before the first turn dispatch");
|
|
650
|
-
}
|
|
651
|
-
commitKeyAttemptSend();
|
|
652
|
-
sent = true;
|
|
653
|
-
},
|
|
707
|
+
beforeDispatch: beforePhysicalSend,
|
|
654
708
|
});
|
|
655
|
-
await run(requestParsed, { ...incoming, providerFetch: fetch }, event => { if (!refused) emit(event); });
|
|
709
|
+
await run(requestParsed, { ...incoming, pacingSlot: attemptSlot, providerFetch: fetch, beforePhysicalSend }, event => { if (!refused) emit(event); });
|
|
656
710
|
} catch (error) {
|
|
657
711
|
if (!refused) throw error;
|
|
658
712
|
} finally {
|
|
@@ -661,7 +715,7 @@ export async function prepareResponsesTransport(
|
|
|
661
715
|
if (!refused) return;
|
|
662
716
|
// The adapter may map the guard's exception to an error event. Neither that
|
|
663
717
|
// event nor a refused send may escape before retrying the newly selected account.
|
|
664
|
-
selectedAdapter = await refreshRunTurnAdapter(requestParsed);
|
|
718
|
+
if (attempt < 2) selectedAdapter = await refreshRunTurnAdapter(requestParsed);
|
|
665
719
|
}
|
|
666
720
|
throw new Error("Account selection changed repeatedly before turn dispatch");
|
|
667
721
|
} finally { producer?.close(); }
|
|
@@ -669,10 +723,16 @@ export async function prepareResponsesTransport(
|
|
|
669
723
|
/** Fence the physical send, with bounded credential/session rebuilds and safe negotiated refusals. */
|
|
670
724
|
const oauthDispatch = (wireRequest: AdapterRequest, requestParsed = parsed, owner: "adapter" | "passthrough" = "adapter"): ProviderFetchOptions["dispatchOverride"] => {
|
|
671
725
|
if (route.provider.authMode === "forward") return undefined;
|
|
672
|
-
return /** Dispatch with the current binding and session, rebuilding at most three times before returning a safe refusal. */ async (input, init, execute) => {
|
|
726
|
+
return /** Dispatch with the current binding and session, rebuilding at most three times before returning a safe refusal. */ async (input, init, execute, reacquirePacing) => {
|
|
673
727
|
let destination = input;
|
|
674
728
|
let dispatchInit = init;
|
|
729
|
+
let pacingChanged = false;
|
|
675
730
|
for (let attempt = 0; attempt < 3; attempt++) {
|
|
731
|
+
if (pacingChanged) {
|
|
732
|
+
if (!reacquirePacing) throw new Error("Refreshed dispatch requires pacing readmission");
|
|
733
|
+
await reacquirePacing(route.provider, route.modelId);
|
|
734
|
+
pacingChanged = false;
|
|
735
|
+
}
|
|
676
736
|
if (selectionIsCurrent(requestBindings.get(wireRequest)) && copilotSessionExpiresAt > Date.now() + 1000) {
|
|
677
737
|
const fetchImpl = (route.provider as OcxProviderConfig & { fetch?: typeof globalThis.fetch }).fetch ?? execute;
|
|
678
738
|
const binding = requestBindings.get(wireRequest);
|
|
@@ -713,7 +773,7 @@ export async function prepareResponsesTransport(
|
|
|
713
773
|
}
|
|
714
774
|
commitKeyAttemptSend();
|
|
715
775
|
response = await sendWithConnectionPolicy(
|
|
716
|
-
anthropicBillingPhysicalFetch(fetchImpl, route.providerName, route.provider),
|
|
776
|
+
distributedKeyPhysicalFetch(anthropicBillingPhysicalFetch(fetchImpl, route.providerName, route.provider), route.providerName, route.provider, target),
|
|
717
777
|
destination,
|
|
718
778
|
{ ...dispatchInit, redirect: "manual" },
|
|
719
779
|
{ providerName: route.providerName, provider: route.provider },
|
|
@@ -775,13 +835,15 @@ export async function prepareResponsesTransport(
|
|
|
775
835
|
sameTargetToken = transportToken;
|
|
776
836
|
destination = rebuilt.url;
|
|
777
837
|
dispatchInit = { ...dispatchInit, method: rebuilt.method, headers, body: rebuilt.body };
|
|
778
|
-
|
|
838
|
+
pacingChanged = route.provider.requestPacing?.enabled === true
|
|
839
|
+
&& (!!route.provider.requestPacing.keys || route.provider.requestPacing.keySelection === "round-robin");
|
|
840
|
+
// A refreshed key must reacquire admission before the next synchronous send guard.
|
|
779
841
|
}
|
|
780
842
|
throw new Error("OAuth account selection changed repeatedly before dispatch");
|
|
781
843
|
};
|
|
782
844
|
};
|
|
783
|
-
const
|
|
784
|
-
?
|
|
845
|
+
const oauthSessionKey = route.provider.authMode === "oauth"
|
|
846
|
+
? oauthSessionKeyFromParts({
|
|
785
847
|
sessionIdHeader: sessionIdHeaderFromRequest(req.headers),
|
|
786
848
|
threadIdHeader: req.headers.get("thread-id"),
|
|
787
849
|
promptCacheKey: typeof parsed.options.promptCacheKey === "string" ? parsed.options.promptCacheKey : null,
|
|
@@ -789,6 +851,9 @@ export async function prepareResponsesTransport(
|
|
|
789
851
|
promptCacheKeyIsSharedCohort: options.promptCacheKeyIsSharedCohort === true,
|
|
790
852
|
})
|
|
791
853
|
: null;
|
|
854
|
+
const anthropicSessionKey = anthropicInstance ? oauthSessionKey : null;
|
|
855
|
+
const genericSessionKey = genericAffinityMode && oauthSessionKey
|
|
856
|
+
? JSON.stringify([resolveContextPrincipal(req, config, options.admission) ?? "loopback", oauthSessionKey]) : null;
|
|
792
857
|
if (route.provider.authMode === "oauth") {
|
|
793
858
|
try {
|
|
794
859
|
if (anthropicInstance !== undefined && anthropicRouting!.isAnthropicAccountPoolEnabled(config)) {
|
|
@@ -821,10 +886,17 @@ export async function prepareResponsesTransport(
|
|
|
821
886
|
// without quota evidence keeps the resolution it has today.
|
|
822
887
|
const refusalAwareId = route.providerName === "kiro" && oauthSelection?.accountId
|
|
823
888
|
? refusalAwareInitialKiroAccount(config, oauthSelection.accountId, Date.now(), route.modelId) : null;
|
|
824
|
-
const
|
|
889
|
+
const affinityAccountId = genericSessionAccount(config, route.providerName, genericSessionKey, route.modelId);
|
|
890
|
+
const proposedAccountId = affinityAccountId
|
|
891
|
+
?? preferredGenericSessionAccount(config, route.providerName, genericSessionKey, route.modelId)
|
|
892
|
+
?? refusalAwareId ?? (isGenericFailoverProvider(route.providerName, route.provider)
|
|
825
893
|
? preferredInitialAccount(config, route.providerName, Date.now(), route.modelId)
|
|
826
894
|
: null);
|
|
827
|
-
|
|
895
|
+
// An unchanged active choice must retain the ordinary resolver's terminal-refresh
|
|
896
|
+
// recovery (not the speculative-preference fallback that resolves the same dead grant).
|
|
897
|
+
const preferredAccountId = genericAffinityMode && proposedAccountId === oauthSelection?.accountId
|
|
898
|
+
? null : proposedAccountId;
|
|
899
|
+
let safetyAlternateId: string | null = !affinityAccountId && preferredAccountId === refusalAwareId ? refusalAwareId : null;
|
|
828
900
|
// Resolved account-scoped, NOT through failoverAccountSnapshot: that helper marks a
|
|
829
901
|
// rotation site, and rotation sites must apply their credential through
|
|
830
902
|
// applyFailoverSnapshot's pairing rules. This is initial resolution — the code below
|
|
@@ -926,7 +998,8 @@ export async function prepareResponsesTransport(
|
|
|
926
998
|
// Advance the pool cursor only now that this account is actually admitted. The
|
|
927
999
|
// helper returns immediately unless the kernel is on AND the strategy is
|
|
928
1000
|
// round-robin, so quota and fill-first pools reach it without being touched.
|
|
929
|
-
|
|
1001
|
+
if (!affinityAccountId || affinityAccountId !== resolved.accountId)
|
|
1002
|
+
noteGenericPoolSelection(config, route.providerName, resolved.accountId, route.modelId);
|
|
930
1003
|
}
|
|
931
1004
|
// Anthropic is excluded from isGenericFailoverProvider -- its own pool owns affinity and
|
|
932
1005
|
// a fail-closed local-cli credential rule -- so without this stamp its identity is
|
|
@@ -964,6 +1037,10 @@ export async function prepareResponsesTransport(
|
|
|
964
1037
|
`${redactSecretString(err.message)}. Remove or reconfigure provider '${safeProviderName}' in the OpenCodex configuration.`,
|
|
965
1038
|
);
|
|
966
1039
|
}
|
|
1040
|
+
if (err instanceof OAuthTokenRefreshBusyError) {
|
|
1041
|
+
return options.abortSignal?.aborted || req.signal.aborted ? clientCancelledResponse()
|
|
1042
|
+
: formatErrorResponse(503, "server_error", err.message, { retryAfter: "1" });
|
|
1043
|
+
}
|
|
967
1044
|
if (err instanceof OAuthAccountPausedError) {
|
|
968
1045
|
return formatErrorResponse(403, "permission_error", publicOAuthAuthenticationErrorMessage(err));
|
|
969
1046
|
}
|
|
@@ -89,6 +89,7 @@ export function createResponsesEffects(
|
|
|
89
89
|
if (routedCompaction) {
|
|
90
90
|
delete parsed.context.tools;
|
|
91
91
|
delete parsed._webSearch;
|
|
92
|
+
delete parsed._anthropicServerTools;
|
|
92
93
|
delete parsed.options.toolChoice;
|
|
93
94
|
delete parsed.options.parallelToolCalls;
|
|
94
95
|
// The compaction turn is a plain prose summary; a surviving structured-output format
|
|
@@ -30,7 +30,7 @@ import {
|
|
|
30
30
|
rotateGenericOAuthAccountOn429,
|
|
31
31
|
failoverAccountSnapshot,
|
|
32
32
|
} from "../../oauth/generic-account-failover";
|
|
33
|
-
import { OAuthAccountPausedError, publicOAuthAuthenticationErrorMessage, type OAuthAccessSnapshot } from "../../oauth/index";
|
|
33
|
+
import { OAuthAccountPausedError, OAuthTokenRefreshBusyError, publicOAuthAuthenticationErrorMessage, type OAuthAccessSnapshot } from "../../oauth/index";
|
|
34
34
|
import { tryAlternateAfterTerminalRefresh } from "../../oauth/kiro-terminal-failover";
|
|
35
35
|
import { resolveWireProtocolOverride } from "../adapter-resolve";
|
|
36
36
|
import { formatErrorResponse, bridgeToResponsesSSE, buildResponseJSON } from "../../bridge";
|
|
@@ -49,12 +49,14 @@ import { rememberResponseState } from "../../responses/state";
|
|
|
49
49
|
import { trackStreamLifetime } from "../lifecycle";
|
|
50
50
|
import { awaitThoughtSignatureDurability } from "../../responses/thought-signature-replay";
|
|
51
51
|
import { undeclaredToolCallMessage } from "../responses-undeclared-tool-guard";
|
|
52
|
+
import { emptyToolCatalogRefusal, guardEmptyToolCatalogEvents, guardEmptyToolCatalogStream } from "../../lib/empty-tool-catalog";
|
|
52
53
|
// LOCAL PATCH (runturn-websearch)
|
|
53
54
|
import { planWebSearch } from "../../web-search";
|
|
54
55
|
import { runTurnWebSearchInitialParsed, runTurnWebSearchLoop } from "../../web-search/run-turn-loop";
|
|
55
56
|
import { WEB_SEARCH_TOOL_NAME } from "../../web-search/synthetic-tool";
|
|
56
57
|
import { orderDevinMessagesOutput } from "../../claude/devin-output-order";
|
|
57
58
|
import { unboundPoolSpendRefusalMessage } from "../workflow-refusal";
|
|
59
|
+
import { claudeUsagePreflight } from "../../adapters/claude-cli/usage-admission";
|
|
58
60
|
|
|
59
61
|
// LOCAL PATCH (runturn-websearch): top-level fields route binding or the
|
|
60
62
|
// adapter itself may write during a turn. Iteration-local `turnParsed` objects
|
|
@@ -71,13 +73,16 @@ const RUNTURN_WS_ROUTE_STATE_KEYS = [
|
|
|
71
73
|
"_providerContinuationOwner",
|
|
72
74
|
"_providerContinuationCandidate",
|
|
73
75
|
"_stripReasoningEncryptedContent",
|
|
76
|
+
"_stripNativeCompactionEncryptedContent",
|
|
77
|
+
"_nativeReasoningRetention",
|
|
78
|
+
"_nativeReasoningRetainedAcrossRoute",
|
|
74
79
|
"_dropForeignReasoningItemIds",
|
|
75
80
|
"_reasoningReplayScope",
|
|
76
81
|
] as const;
|
|
77
82
|
|
|
78
83
|
/** One responsibility of the Responses request pipeline; state owners are explicit. */
|
|
79
84
|
export async function executeResponsesRunTurn(
|
|
80
|
-
requestContext: Pick<ResponsesRequestContext, "options" | "logCtx" | "config">,
|
|
85
|
+
requestContext: Pick<ResponsesRequestContext, "req" | "options" | "logCtx" | "config">,
|
|
81
86
|
admissionState: ResponsesAdmissionState,
|
|
82
87
|
requestState: Pick<
|
|
83
88
|
PreparedResponsesRequest,
|
|
@@ -87,6 +92,7 @@ export async function executeResponsesRunTurn(
|
|
|
87
92
|
| "translatorBudget"
|
|
88
93
|
| "inboundWire"
|
|
89
94
|
| "toolBridgeMaps"
|
|
95
|
+
| "clientToolCatalogRestriction"
|
|
90
96
|
| "rememberKiroDeliveredFinalAnswer"
|
|
91
97
|
| "responseStateOptions"
|
|
92
98
|
>,
|
|
@@ -127,7 +133,7 @@ export async function executeResponsesRunTurn(
|
|
|
127
133
|
>,
|
|
128
134
|
completionPolicy: Pick<ResponsesCompletionPolicy, "emptyCompletionGuardEnabled">,
|
|
129
135
|
): Promise<Response> {
|
|
130
|
-
const { options, logCtx, config } = requestContext;
|
|
136
|
+
const { req, options, logCtx, config } = requestContext;
|
|
131
137
|
const {
|
|
132
138
|
selectionIsCurrent,
|
|
133
139
|
adapterBindings,
|
|
@@ -169,6 +175,33 @@ export async function executeResponsesRunTurn(
|
|
|
169
175
|
const releaseSearchProbeLease = (): void => {
|
|
170
176
|
sidecarState.openAiSidecar?.releaseProbeLease?.();
|
|
171
177
|
};
|
|
178
|
+
// Publish the known subscription pause before headers or a CLI turn, preserving its reset
|
|
179
|
+
// across both Responses and translated Chat instead of presenting a retryable generic 502.
|
|
180
|
+
if (transportState.runTurnAdapter.name === "claude-cli") {
|
|
181
|
+
let usage: Awaited<ReturnType<typeof claudeUsagePreflight>>;
|
|
182
|
+
try {
|
|
183
|
+
usage = await claudeUsagePreflight(parsed.modelId);
|
|
184
|
+
} catch (error) {
|
|
185
|
+
// The outer cleanup does not own the sidecar's probe lease: hand it back before rethrowing.
|
|
186
|
+
releaseSearchProbeLease();
|
|
187
|
+
throw error;
|
|
188
|
+
}
|
|
189
|
+
if (usage.state === "exhausted") {
|
|
190
|
+
cancelResponseCompletion();
|
|
191
|
+
releaseSearchProbeLease();
|
|
192
|
+
const refusal = new Response(JSON.stringify({ error: {
|
|
193
|
+
type: "rate_limit_error", code: "claude_subscription_cooldown", param: null,
|
|
194
|
+
message: usage.message ?? "Claude subscription usage is exhausted.",
|
|
195
|
+
} }), {
|
|
196
|
+
status: 429,
|
|
197
|
+
headers: { "Content-Type": "application/json",
|
|
198
|
+
...(usage.resetAt ? { "Retry-After": String(Math.max(1, Math.ceil((usage.resetAt - Date.now()) / 1000))) } : {}),
|
|
199
|
+
},
|
|
200
|
+
});
|
|
201
|
+
markResponseNonReplayable(refusal);
|
|
202
|
+
return refusal;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
172
205
|
// When Codex declared hosted web_search and a sidecar plan resolves, drive
|
|
173
206
|
// the routed model through the same search interception the fetch-path loop
|
|
174
207
|
// runs — injected as a function tool, calls intercepted, results appended to
|
|
@@ -186,7 +219,9 @@ export async function executeResponsesRunTurn(
|
|
|
186
219
|
}
|
|
187
220
|
|
|
188
221
|
const runTurnAbort = new AbortController();
|
|
189
|
-
const
|
|
222
|
+
const cleanupRequestAbort = linkAbortSignal(runTurnAbort, req.signal);
|
|
223
|
+
const cleanupOptionAbort = linkAbortSignal(runTurnAbort, options.abortSignal);
|
|
224
|
+
const cleanupRunTurnAbort = (): void => { cleanupRequestAbort(); cleanupOptionAbort(); };
|
|
190
225
|
const devinProducers = new Set<AbortController>();
|
|
191
226
|
const orderMessagesOutput = (source: AsyncIterable<AdapterEvent>): AsyncIterable<AdapterEvent> =>
|
|
192
227
|
inboundWire === "anthropic" && transportState.runTurnAdapter.name === "devin" && !routedCompaction
|
|
@@ -197,12 +232,13 @@ export async function executeResponsesRunTurn(
|
|
|
197
232
|
const queue = createAdapterEventQueue({
|
|
198
233
|
onBacklogExceeded: () => runTurnAbort.abort(),
|
|
199
234
|
});
|
|
200
|
-
const refreshRunTurnSelection = async (): Promise<
|
|
201
|
-
if (selectionIsCurrent(adapterBindings.get(transportState.runTurnAdapter))) return;
|
|
235
|
+
const refreshRunTurnSelection = async (): Promise<boolean> => {
|
|
236
|
+
if (selectionIsCurrent(adapterBindings.get(transportState.runTurnAdapter))) return false;
|
|
202
237
|
await refreshRunTurnAdapter(parsed);
|
|
203
238
|
bindRouteReasoningReplayScope({ parsed, providerName: route.providerName, provider: route.provider,
|
|
204
239
|
adapterName: transportState.runTurnAdapter.name, oauthCredentialSnapshot: transportState.replayOAuthCredentialSnapshot });
|
|
205
240
|
sealRequestAttemptIdentity(logCtx.activeAttempt, logCtx.provider, transportState.runTurnAdapter.name, logCtx.accountLogLabel);
|
|
241
|
+
return true;
|
|
206
242
|
};
|
|
207
243
|
// Initial admission must settle before the streaming Response commits HTTP 200.
|
|
208
244
|
// Let the outer Responses facade preserve the local retryable-429 contract.
|
|
@@ -256,13 +292,18 @@ export async function executeResponsesRunTurn(
|
|
|
256
292
|
if (!pacingSlotAcquired) {
|
|
257
293
|
pacingSlot = await waitForProviderRequestSlot(route.providerName, route.provider, route.modelId, runTurnAbort.signal);
|
|
258
294
|
}
|
|
259
|
-
await refreshRunTurnSelection();
|
|
295
|
+
const selectionChanged = await refreshRunTurnSelection();
|
|
260
296
|
const selectedBinding = adapterBindings.get(transportState.runTurnAdapter);
|
|
261
297
|
if (lastKeyFailure && selectedBinding?.kind === "api-key"
|
|
262
298
|
&& failedKeys.has(selectedBinding.provider.apiKey ?? "")) {
|
|
263
299
|
emit(lastKeyFailure);
|
|
264
300
|
return;
|
|
265
301
|
}
|
|
302
|
+
if (selectionChanged) {
|
|
303
|
+
releaseProviderRequestSlot(pacingSlot);
|
|
304
|
+
pacingSlot = undefined;
|
|
305
|
+
pacingSlot = await waitForProviderRequestSlot(route.providerName, route.provider, route.modelId, runTurnAbort.signal);
|
|
306
|
+
}
|
|
266
307
|
// LOCAL PATCH (runturn-websearch): refreshRunTurnSelection binds route
|
|
267
308
|
// state onto `parsed`; mirror it onto the iteration-local copy the
|
|
268
309
|
// adapter actually receives.
|
|
@@ -436,7 +477,19 @@ export async function executeResponsesRunTurn(
|
|
|
436
477
|
// selected; the turn then moves to a surviving stored account, or, with none, the client
|
|
437
478
|
// gets the login instruction instead of an opaque upstream 401.
|
|
438
479
|
let oauth401ReplayAttempted = false;
|
|
439
|
-
let
|
|
480
|
+
let authRecoveryRefusal: Response | undefined;
|
|
481
|
+
const refuseLocalAuthRecovery = (error: Extract<AdapterEvent, { type: "error" }>, cause: unknown): boolean => {
|
|
482
|
+
const cancelled = runTurnAbort.signal.aborted;
|
|
483
|
+
if (!cancelled && !(cause instanceof OAuthTokenRefreshBusyError) && !(cause instanceof OAuthAccountPausedError)) return false;
|
|
484
|
+
const status = cancelled ? 499 : cause instanceof OAuthTokenRefreshBusyError ? 503 : 403;
|
|
485
|
+
const errorType = cancelled ? "client_cancelled" : status === 503 ? "server_error" : "permission_error";
|
|
486
|
+
const message = cancelled ? "Client cancelled request" : publicOAuthAuthenticationErrorMessage(cause);
|
|
487
|
+
// If a readiness heartbeat already committed SSE, the mapped event remains retryable
|
|
488
|
+
// capacity rather than retaining the rejected credential's authentication error/code.
|
|
489
|
+
Object.assign(error, { status, errorType, message, code: undefined, retryable: status === 503 });
|
|
490
|
+
authRecoveryRefusal = formatErrorResponse(status, errorType, message, status === 503 ? { retryAfter: "1" } : undefined);
|
|
491
|
+
return true;
|
|
492
|
+
};
|
|
440
493
|
const recoverRunTurnAdapterOnPreflight401 = async (
|
|
441
494
|
error: Extract<AdapterEvent, { type: "error" }>,
|
|
442
495
|
): Promise<boolean> => {
|
|
@@ -450,9 +503,7 @@ export async function executeResponsesRunTurn(
|
|
|
450
503
|
try {
|
|
451
504
|
admitted = await applyFailoverSnapshot(await refreshResolvedOAuthSelection(sent));
|
|
452
505
|
} catch (err) {
|
|
453
|
-
if (err
|
|
454
|
-
pausedAuthRecovery = true;
|
|
455
|
-
Object.assign(error, { status: 403, errorType: "permission_error", message: publicOAuthAuthenticationErrorMessage(err) });
|
|
506
|
+
if (refuseLocalAuthRecovery(error, err)) {
|
|
456
507
|
hop.permit?.release();
|
|
457
508
|
return false;
|
|
458
509
|
}
|
|
@@ -475,7 +526,9 @@ export async function executeResponsesRunTurn(
|
|
|
475
526
|
} catch (err) {
|
|
476
527
|
// Applying the alternate can still fail, e.g. when a newer manual selection points back
|
|
477
528
|
// at the flagged account; the client then needs the login instruction, not the raw 401.
|
|
478
|
-
|
|
529
|
+
if (!refuseLocalAuthRecovery(error, err)) {
|
|
530
|
+
Object.assign(error, { errorType: "authentication_error", message: publicOAuthAuthenticationErrorMessage(err) });
|
|
531
|
+
}
|
|
479
532
|
hop.permit?.release();
|
|
480
533
|
return false;
|
|
481
534
|
}
|
|
@@ -699,14 +752,19 @@ export async function executeResponsesRunTurn(
|
|
|
699
752
|
return orderMessagesOutput(retryQueue.stream());
|
|
700
753
|
};
|
|
701
754
|
|
|
702
|
-
const
|
|
755
|
+
const clientToolCatalog = requestState.clientToolCatalogRestriction && route.provider.authMode !== "forward"
|
|
756
|
+
? requestState.clientToolCatalogRestriction : undefined;
|
|
757
|
+
const { toolNsMap, declaredToolNames, toolParameterSchemas, freeformToolNames, bareCustomToolNames, toolSearchToolNames } = clientToolCatalog
|
|
758
|
+
? { ...toolBridgeMaps, ...clientToolCatalog } : toolBridgeMaps;
|
|
703
759
|
const enforceDeclaredToolNames = inboundWire !== "chat" && inboundWire !== "anthropic";
|
|
704
760
|
const classifyUndeclaredFirstTool = (
|
|
705
761
|
event: AdapterEvent,
|
|
706
762
|
): Extract<AdapterEvent, { type: "error" }> | undefined => {
|
|
707
|
-
if (!enforceDeclaredToolNames || event.type !== "tool_call_start") return undefined;
|
|
708
763
|
// This tool is declared to the adapter by the private search loop.
|
|
709
|
-
if (wsPlan && event.name === WEB_SEARCH_TOOL_NAME) return undefined;
|
|
764
|
+
if (wsPlan && event.type === "tool_call_start" && event.name === WEB_SEARCH_TOOL_NAME) return undefined;
|
|
765
|
+
const emptyCatalogRefusal = emptyToolCatalogRefusal(event, clientToolCatalog);
|
|
766
|
+
if (emptyCatalogRefusal) return emptyCatalogRefusal;
|
|
767
|
+
if (!enforceDeclaredToolNames || event.type !== "tool_call_start") return undefined;
|
|
710
768
|
const effectiveName = normalizeDeclaredToolName(event.name, declaredToolNames, undefined, bareCustomToolNames);
|
|
711
769
|
if (declaredToolNames.has(effectiveName)) return undefined;
|
|
712
770
|
return {
|
|
@@ -752,13 +810,13 @@ export async function executeResponsesRunTurn(
|
|
|
752
810
|
// Preflight holds only heartbeats and the first meaningful event. A first-event 429 can be
|
|
753
811
|
// replayed transparently; after any output reaches the bridge, a later error stays terminal.
|
|
754
812
|
eventSource = await preflightRunTurnFailover(eventSource, wsFirstParsed, preflightDeadlineAt);
|
|
755
|
-
if (
|
|
813
|
+
if (authRecoveryRefusal) {
|
|
756
814
|
cancelResponseCompletion();
|
|
757
815
|
runTurnAbort.abort();
|
|
758
816
|
queue.close();
|
|
759
817
|
cleanupRunTurnAbort();
|
|
760
818
|
releaseSearchProbeLease();
|
|
761
|
-
return
|
|
819
|
+
return authRecoveryRefusal;
|
|
762
820
|
}
|
|
763
821
|
}
|
|
764
822
|
if (grokDevinPreflight) {
|
|
@@ -776,9 +834,13 @@ export async function executeResponsesRunTurn(
|
|
|
776
834
|
if (preflight.error || preflight.empty) {
|
|
777
835
|
runTurnAbort.abort();
|
|
778
836
|
queue.close();
|
|
779
|
-
const
|
|
780
|
-
|
|
781
|
-
|
|
837
|
+
const classified = adapterFailureFromEvent(preflight.error ?? {
|
|
838
|
+
type: "error", status: 502, message: "Adapter ended before producing a response",
|
|
839
|
+
});
|
|
840
|
+
const storageRefusal = preflight.error?.code === SPEND_LEDGER_STORAGE_UNAVAILABLE_CODE;
|
|
841
|
+
const failure = formatErrorResponse(storageRefusal ? 503 : classified.httpStatus,
|
|
842
|
+
storageRefusal ? SPEND_LEDGER_STORAGE_UNAVAILABLE_CODE : classified.error.type,
|
|
843
|
+
redactSecretString(classified.error.message), { code: classified.error.code, classified: true });
|
|
782
844
|
// A replay-unsafe heartbeat means the adapter already ran a local side effect, so the
|
|
783
845
|
// combo must not send this turn to another target: the failure stays with this child.
|
|
784
846
|
if (preflight.replayUnsafe) markResponseNonReplayable(failure);
|
|
@@ -824,7 +886,7 @@ export async function executeResponsesRunTurn(
|
|
|
824
886
|
console.warn(emptyCompletionNotice(route.providerName, route.modelId));
|
|
825
887
|
});
|
|
826
888
|
const sseStream = bridgeToResponsesSSE(
|
|
827
|
-
guardedSource, parsed._responseModelId ?? parsed.modelId, toolNsMap, freeformToolNames, toolSearchToolNames,
|
|
889
|
+
guardEmptyToolCatalogStream(guardedSource, clientToolCatalog, translatorBudget), parsed._responseModelId ?? parsed.modelId, toolNsMap, freeformToolNames, toolSearchToolNames,
|
|
828
890
|
() => {
|
|
829
891
|
cancelResponseCompletion();
|
|
830
892
|
runTurnAbort.abort();
|
|
@@ -840,9 +902,11 @@ export async function executeResponsesRunTurn(
|
|
|
840
902
|
declaredToolNames,
|
|
841
903
|
bareCustomToolNames,
|
|
842
904
|
enforceDeclaredToolNames,
|
|
905
|
+
// Same wire split: only a Responses client (Codex) needs the refusal as a failure.
|
|
906
|
+
terminalRefusalAsFailed: enforceDeclaredToolNames,
|
|
843
907
|
toolParameterSchemas,
|
|
844
908
|
...(options.onFirstOutput ? { onFirstOutput: options.onFirstOutput } : {}),
|
|
845
|
-
...(routedCompaction ? { compaction: true } : {}),
|
|
909
|
+
...(routedCompaction ? { compaction: true, compactionRetention: parsed._compactionRetention } : {}),
|
|
846
910
|
// grok-build's strict decoder dies on the typed response.heartbeat frame; its
|
|
847
911
|
// eventsource layer tolerates comment keep-alives. Codex needs the opposite.
|
|
848
912
|
...(logCtx.surface === "grok" ? { heartbeatStyle: "comment" as const } : {}),
|
|
@@ -897,13 +961,13 @@ export async function executeResponsesRunTurn(
|
|
|
897
961
|
for await (const event of await preflightRunTurnFailover(
|
|
898
962
|
(async function* () { yield* firstAttemptEvents; })(),
|
|
899
963
|
)) runTurnEvents.push(event);
|
|
900
|
-
if (
|
|
964
|
+
if (authRecoveryRefusal) {
|
|
901
965
|
cancelResponseCompletion();
|
|
902
966
|
runTurnAbort.abort();
|
|
903
967
|
queue.close();
|
|
904
968
|
cleanupRunTurnAbort();
|
|
905
969
|
releaseSearchProbeLease();
|
|
906
|
-
return
|
|
970
|
+
return authRecoveryRefusal;
|
|
907
971
|
}
|
|
908
972
|
}
|
|
909
973
|
if (grokDevinPreflight) {
|
|
@@ -961,6 +1025,7 @@ export async function executeResponsesRunTurn(
|
|
|
961
1025
|
}
|
|
962
1026
|
events = searched;
|
|
963
1027
|
}
|
|
1028
|
+
events = guardEmptyToolCatalogEvents(events, clientToolCatalog, translatorBudget);
|
|
964
1029
|
if (options.comboAttempt) {
|
|
965
1030
|
const firstMeaningfulIndex = events.findIndex(event => event.type !== "heartbeat");
|
|
966
1031
|
const firstMeaningful = firstMeaningfulIndex === -1 ? undefined : events[firstMeaningfulIndex];
|
|
@@ -974,12 +1039,13 @@ export async function executeResponsesRunTurn(
|
|
|
974
1039
|
? classifyUndeclaredFirstTool(firstMeaningful)
|
|
975
1040
|
: undefined;
|
|
976
1041
|
if (!firstMeaningful || firstMeaningful.type === "error" || classifiedError) {
|
|
977
|
-
const
|
|
978
|
-
? firstMeaningful
|
|
979
|
-
: "Adapter ended before producing a response");
|
|
1042
|
+
const classified = adapterFailureFromEvent(classifiedError ?? (firstMeaningful?.type === "error"
|
|
1043
|
+
? firstMeaningful
|
|
1044
|
+
: { type: "error", status: 502, message: "Adapter ended before producing a response" }));
|
|
980
1045
|
const storageRefusal = firstMeaningful?.type === "error" && firstMeaningful.code === SPEND_LEDGER_STORAGE_UNAVAILABLE_CODE;
|
|
981
|
-
const failure = formatErrorResponse(storageRefusal ? 503 :
|
|
982
|
-
storageRefusal ? SPEND_LEDGER_STORAGE_UNAVAILABLE_CODE :
|
|
1046
|
+
const failure = formatErrorResponse(storageRefusal ? 503 : classified.httpStatus,
|
|
1047
|
+
storageRefusal ? SPEND_LEDGER_STORAGE_UNAVAILABLE_CODE : classified.error.type,
|
|
1048
|
+
redactSecretString(classified.error.message), { code: classified.error.code, classified: true });
|
|
983
1049
|
if (replayUnsafe) markResponseNonReplayable(failure);
|
|
984
1050
|
return failure;
|
|
985
1051
|
}
|
|
@@ -997,7 +1063,7 @@ export async function executeResponsesRunTurn(
|
|
|
997
1063
|
freeformToolNames,
|
|
998
1064
|
bareCustomToolNames,
|
|
999
1065
|
toolSearchToolNames,
|
|
1000
|
-
...(routedCompaction ? { compaction: true } : {}),
|
|
1066
|
+
...(routedCompaction ? { compaction: true, compactionRetention: parsed._compactionRetention } : {}),
|
|
1001
1067
|
onProviderState: state => { providerState = state; },
|
|
1002
1068
|
onUsage: usage => {
|
|
1003
1069
|
transportState.bindKeyUsageFromBridge(usage);
|