@bitkyc08/opencodex 2.86.0 → 2.88.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-CHC89EXQ.js +58 -0
- package/gui/dist/assets/App-wT3mXqFz.css +1 -0
- package/gui/dist/assets/{Tray-DyR9yBee.js → Tray-B5bC7AWL.js} +1 -1
- package/gui/dist/assets/index-B5bsvxor.js +94 -0
- package/gui/dist/assets/index-DPeB9HpV.css +1 -0
- package/gui/dist/assets/quota-summary-D8y2wTsB.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-rejection-memo.ts +71 -0
- package/src/adapters/anthropic-replay.ts +135 -0
- package/src/adapters/anthropic-tool-catalog.ts +11 -0
- package/src/adapters/anthropic.ts +151 -33
- 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-progress-history.ts +53 -0
- package/src/adapters/google.ts +52 -9
- 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 +7 -1
- package/src/bridge/response-json.ts +18 -3
- package/src/bridge/sse.ts +59 -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 +77 -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 +46 -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 +33 -16
- 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/loopback-family.ts +31 -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 +14 -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/websocket-activity.ts +161 -0
- package/src/codex/websocket-registry.ts +25 -1
- 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 +53 -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 +657 -297
- package/src/grok/sync.ts +1 -1
- package/src/images/loop.ts +116 -27
- 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/lib/request-failure-model.ts +2 -0
- 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 +40 -11
- 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 +6 -5
- 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/opencode-zen-rate-limit.ts +7 -2
- 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 +74 -6
- 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/analytics.ts +7 -15
- 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 +56 -414
- package/src/server/effort-policy.ts +1 -1
- package/src/server/images.ts +15 -2
- package/src/server/index/live-sideband.ts +3 -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 +20 -6
- package/src/server/index/startup-warnings.ts +2 -0
- package/src/server/index/websocket-handler.ts +18 -4
- package/src/server/index.ts +8 -8
- package/src/server/inference/advisor-diagnostics.ts +31 -0
- package/src/server/lifecycle.ts +13 -1
- 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 +117 -24
- 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 +47 -19
- package/src/server/responses/adapter-delivery.ts +72 -12
- package/src/server/responses/adapter-dispatch.ts +46 -3
- package/src/server/responses/advisor-retention.ts +117 -0
- package/src/server/responses/advisor-slot.ts +73 -41
- package/src/server/responses/anthropic-thinking-recovery.ts +153 -0
- package/src/server/responses/codex-ws-wire.ts +15 -8
- package/src/server/responses/combo-stream-preflight.ts +3 -2
- package/src/server/responses/compact.ts +303 -125
- package/src/server/responses/compaction-recovery.ts +20 -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 +121 -36
- package/src/server/responses/core-errors.ts +3 -3
- package/src/server/responses/core-lifetime.ts +8 -4
- package/src/server/responses/core-opaque-recovery.ts +35 -0
- package/src/server/responses/core-options.ts +7 -0
- package/src/server/responses/core-pipeline.ts +141 -0
- package/src/server/responses/core-replay.ts +23 -3
- 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/passthrough-execution.ts +3 -2
- package/src/server/responses/request-prepare.ts +77 -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 +2 -0
- package/src/server/responses/response-tool-ownership.ts +21 -0
- package/src/server/responses/run-turn-execution.ts +96 -30
- package/src/server/responses/sidecar-execution.ts +63 -9
- package/src/server/responses/sidecar-send-budget.ts +2 -2
- package/src/server/responses/terminal-guard.ts +2 -16
- package/src/server/responses-undeclared-tool-guard.ts +1 -1
- package/src/server/ws-bridge.ts +2 -0
- 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 +35 -0
- package/src/types/provider.ts +32 -6
- package/src/types/request.ts +31 -2
- package/src/types/wire.ts +31 -37
- package/src/types.ts +2 -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 +30 -11
- package/src/usage/telemetry-contract.ts +1 -0
- 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/anthropic-history.ts +59 -0
- package/src/web-search/loop.ts +236 -73
- package/src/web-search/progress-stream.ts +15 -2
- 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
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
import { anthropicErrorBody } from "../claude/outbound";
|
|
2
|
+
import { redactSecretString } from "../lib/redact";
|
|
3
|
+
import { sseFieldValue } from "../lib/sse-decoder";
|
|
4
|
+
import { TRANSLATOR_MAX_SSE_EVENT_BYTES, type TranslatorBudget } from "../lib/translator-budget";
|
|
5
|
+
import { UNDECLARED_TOOL_CALL_ERROR_CODE, undeclaredToolCallMessage } from "./responses-undeclared-tool-guard";
|
|
6
|
+
import { createSseBlockBuffer, sseDataPayload } from "./sse-payload-rewrite";
|
|
7
|
+
|
|
8
|
+
type Rec = Record<string, unknown>;
|
|
9
|
+
const isRec = (value: unknown): value is Rec => value !== null && typeof value === "object" && !Array.isArray(value);
|
|
10
|
+
const NO_DECLARED_TOOLS: ReadonlySet<string> = new Set();
|
|
11
|
+
|
|
12
|
+
/** Inline system-message definitions extend an otherwise empty native catalog. */
|
|
13
|
+
export function nativeMessagesInlineToolNames(body: Rec): ReadonlySet<string> {
|
|
14
|
+
const definitions = new Set<string>();
|
|
15
|
+
const active = new Set<string>();
|
|
16
|
+
if (!Array.isArray(body.messages)) return active;
|
|
17
|
+
for (const message of body.messages) {
|
|
18
|
+
if (!isRec(message) || message.role !== "system" || !Array.isArray(message.content)) continue;
|
|
19
|
+
for (const block of message.content) {
|
|
20
|
+
if (!isRec(block) || !isRec(block.tool)) continue;
|
|
21
|
+
const tool = block.tool;
|
|
22
|
+
if (block.type === "tool_addition" && tool.type === "tool_definition" && isRec(tool.definition)
|
|
23
|
+
&& typeof tool.definition.name === "string" && tool.definition.name.length > 0) {
|
|
24
|
+
definitions.add(tool.definition.name);
|
|
25
|
+
active.add(tool.definition.name);
|
|
26
|
+
} else if (tool.type === "tool_reference" && typeof tool.name === "string") {
|
|
27
|
+
if (block.type === "tool_addition" && definitions.has(tool.name)) active.add(tool.name);
|
|
28
|
+
// Like native admission, only a permanent removal withdraws a declaration.
|
|
29
|
+
if (block.type === "tool_removal" && (message.clear_at === undefined || message.clear_at === "never")) active.delete(tool.name);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return active;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Inspect only native message content; opaque extension fields do not carry client calls. */
|
|
37
|
+
export function nativeMessagesClientToolName(value: unknown, declared: ReadonlySet<string> = NO_DECLARED_TOOLS): string | undefined {
|
|
38
|
+
if (!isRec(value)) return undefined;
|
|
39
|
+
const content = Array.isArray(value.content) ? value.content : [];
|
|
40
|
+
const block = content.find(item => isRec(item) && item.type === "tool_use"
|
|
41
|
+
&& (typeof item.name !== "string" || !declared.has(item.name)));
|
|
42
|
+
if (isRec(block)) return typeof block.name === "string" ? block.name : "<unnamed>";
|
|
43
|
+
return undefined;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Used only when the current native request explicitly declares no client tools. */
|
|
47
|
+
export function guardNativeMessagesEmptyToolCatalog(
|
|
48
|
+
source: ReadableStream<Uint8Array>,
|
|
49
|
+
budget: TranslatorBudget,
|
|
50
|
+
onRefusal: (message: string) => void,
|
|
51
|
+
declared: ReadonlySet<string> = NO_DECLARED_TOOLS,
|
|
52
|
+
): ReadableStream<Uint8Array> {
|
|
53
|
+
const reader = source.getReader();
|
|
54
|
+
const decoder = new TextDecoder();
|
|
55
|
+
const encoder = new TextEncoder();
|
|
56
|
+
const scope = { kind: "live_transient" as const };
|
|
57
|
+
const buffer = createSseBlockBuffer(budget, bytes => {
|
|
58
|
+
if (bytes > TRANSLATOR_MAX_SSE_EVENT_BYTES) throw new Error("upstream SSE event exceeded the safe limit");
|
|
59
|
+
});
|
|
60
|
+
// Hosted tools share input_json_delta with client tools, but cannot grant client authority.
|
|
61
|
+
const permittedInputBlocks = new Set<number>();
|
|
62
|
+
let queuedBytes = 0;
|
|
63
|
+
let settled = false;
|
|
64
|
+
const releaseQueued = () => {
|
|
65
|
+
budget.releaseRetained(queuedBytes, scope);
|
|
66
|
+
queuedBytes = 0;
|
|
67
|
+
};
|
|
68
|
+
const clear = () => {
|
|
69
|
+
buffer.clear();
|
|
70
|
+
budget.releaseRetained(permittedInputBlocks.size * 32, scope);
|
|
71
|
+
permittedInputBlocks.clear();
|
|
72
|
+
};
|
|
73
|
+
const enqueue = (controller: ReadableStreamDefaultController<Uint8Array>, text: string) => {
|
|
74
|
+
const bytes = Buffer.byteLength(text);
|
|
75
|
+
const reservation = budget.reserveTransient(bytes, scope);
|
|
76
|
+
try {
|
|
77
|
+
controller.enqueue(encoder.encode(text));
|
|
78
|
+
reservation.commitRetained();
|
|
79
|
+
queuedBytes += bytes;
|
|
80
|
+
} catch (error) {
|
|
81
|
+
reservation.release();
|
|
82
|
+
throw error;
|
|
83
|
+
}
|
|
84
|
+
};
|
|
85
|
+
const cancelReader = (reason?: unknown) => {
|
|
86
|
+
void reader.cancel(reason).catch(() => {});
|
|
87
|
+
};
|
|
88
|
+
const process = (controller: ReadableStreamDefaultController<Uint8Array>, block: string, delimiter: string) => {
|
|
89
|
+
const payload = sseDataPayload(block);
|
|
90
|
+
let data: unknown;
|
|
91
|
+
let terminal = false;
|
|
92
|
+
try { data = payload === null ? undefined : JSON.parse(payload); } catch {
|
|
93
|
+
// The buffered Messages collector concatenates data lines without separators. Inspect
|
|
94
|
+
// that representation too, so an invalid SSE split cannot become a valid folded tool call.
|
|
95
|
+
try { data = payload === null ? undefined : JSON.parse(payload.replace(/\n/g, "")); } catch { /* preserve opaque frames */ }
|
|
96
|
+
}
|
|
97
|
+
if (isRec(data)) {
|
|
98
|
+
const eventName = block.split(/\r\n|\r|\n/).map(line => sseFieldValue(line, "event"))
|
|
99
|
+
.filter(value => value !== null).at(-1)?.trim();
|
|
100
|
+
// Stream clients use the payload type; the buffered collector uses the event field.
|
|
101
|
+
const isEvent = (type: string) => data.type === type || eventName === type;
|
|
102
|
+
terminal = isEvent("message_stop") || isEvent("error");
|
|
103
|
+
let name = isEvent("message_start") ? nativeMessagesClientToolName(data.message, declared) : undefined;
|
|
104
|
+
if (isEvent("content_block_start") && isRec(data.content_block) && data.content_block.type === "tool_use"
|
|
105
|
+
&& (typeof data.content_block.name !== "string" || !declared.has(data.content_block.name))) {
|
|
106
|
+
name ??= typeof data.content_block.name === "string" ? data.content_block.name : "<unnamed>";
|
|
107
|
+
}
|
|
108
|
+
const index = typeof data.index === "number" && Number.isSafeInteger(data.index) ? data.index : undefined;
|
|
109
|
+
if (isEvent("content_block_delta") && isRec(data.delta) && data.delta.type === "input_json_delta"
|
|
110
|
+
&& (index === undefined || !permittedInputBlocks.has(index))) name ??= "<unnamed>";
|
|
111
|
+
if (name !== undefined) {
|
|
112
|
+
clear();
|
|
113
|
+
const message = redactSecretString(undeclaredToolCallMessage(name));
|
|
114
|
+
onRefusal(message);
|
|
115
|
+
enqueue(controller, `event: error\ndata: ${JSON.stringify(anthropicErrorBody(502, message, "api_error", UNDECLARED_TOOL_CALL_ERROR_CODE))}\n\n`);
|
|
116
|
+
settled = true;
|
|
117
|
+
cancelReader(new Error(message));
|
|
118
|
+
controller.close();
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
if (isEvent("content_block_start") && index !== undefined && isRec(data.content_block)) {
|
|
122
|
+
if (permittedInputBlocks.delete(index)) budget.releaseRetained(32, scope);
|
|
123
|
+
if (data.content_block.type === "server_tool_use" || data.content_block.type === "mcp_tool_use"
|
|
124
|
+
|| data.content_block.type === "tool_use" && typeof data.content_block.name === "string" && declared.has(data.content_block.name)) {
|
|
125
|
+
budget.chargeRetained(32, scope);
|
|
126
|
+
permittedInputBlocks.add(index);
|
|
127
|
+
}
|
|
128
|
+
} else if (isEvent("content_block_stop") && index !== undefined && permittedInputBlocks.delete(index)) {
|
|
129
|
+
budget.releaseRetained(32, scope);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
enqueue(controller, block + delimiter);
|
|
133
|
+
if (terminal) {
|
|
134
|
+
settled = true;
|
|
135
|
+
clear();
|
|
136
|
+
cancelReader();
|
|
137
|
+
controller.close();
|
|
138
|
+
}
|
|
139
|
+
};
|
|
140
|
+
return new ReadableStream<Uint8Array>({
|
|
141
|
+
async pull(controller) {
|
|
142
|
+
releaseQueued();
|
|
143
|
+
if (settled) return;
|
|
144
|
+
try {
|
|
145
|
+
for (;;) {
|
|
146
|
+
const block = buffer.next();
|
|
147
|
+
if (block) {
|
|
148
|
+
buffer.compact();
|
|
149
|
+
process(controller, block.block, block.delimiter);
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
const { done, value } = await reader.read();
|
|
153
|
+
if (settled) return;
|
|
154
|
+
if (!done) {
|
|
155
|
+
buffer.append(decoder.decode(value, { stream: true }));
|
|
156
|
+
continue;
|
|
157
|
+
}
|
|
158
|
+
buffer.append(decoder.decode());
|
|
159
|
+
const tail = buffer.tail();
|
|
160
|
+
if (tail) process(controller, tail, "");
|
|
161
|
+
if (!settled) {
|
|
162
|
+
settled = true;
|
|
163
|
+
clear();
|
|
164
|
+
controller.close();
|
|
165
|
+
}
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
} catch (error) {
|
|
169
|
+
if (settled) return;
|
|
170
|
+
settled = true;
|
|
171
|
+
clear();
|
|
172
|
+
cancelReader(error);
|
|
173
|
+
controller.error(error);
|
|
174
|
+
} finally {
|
|
175
|
+
if (settled) releaseQueued();
|
|
176
|
+
}
|
|
177
|
+
},
|
|
178
|
+
cancel(reason) {
|
|
179
|
+
settled = true;
|
|
180
|
+
clear();
|
|
181
|
+
releaseQueued();
|
|
182
|
+
return reader.cancel(reason);
|
|
183
|
+
},
|
|
184
|
+
});
|
|
185
|
+
}
|
|
@@ -44,6 +44,7 @@ import { classifyError } from "../lib/errors";
|
|
|
44
44
|
import { relaySseWithPayloadRewrite } from "./sse-payload-rewrite";
|
|
45
45
|
import { redactSecretString } from "../lib/redact";
|
|
46
46
|
import { resolveClientRetryAfter } from "../lib/retry-after";
|
|
47
|
+
import { enrichOpenCodeZenUpstreamMessage } from "../providers/opencode-zen-rate-limit";
|
|
47
48
|
import { isTranslatorBudgetExceededError, type TranslatorBudget } from "../lib/translator-budget";
|
|
48
49
|
import {
|
|
49
50
|
applyReplayRefusalClientHeaders,
|
|
@@ -61,7 +62,9 @@ import {
|
|
|
61
62
|
type UpstreamSendRecovery,
|
|
62
63
|
UPSTREAM_RESET_REPLAY_REFUSED_CODE,
|
|
63
64
|
} from "../lib/upstream-retry";
|
|
65
|
+
import { distributedKeyPhysicalFetch, distributedKeyResponseOwner, recordDistributedResponseFailure } from "../providers/api-key-response-ownership";
|
|
64
66
|
import { providerApiKeySelectionIsCurrent, resolveCurrentProviderApiKeyTransport } from "../providers/api-key-selection";
|
|
67
|
+
import { RequestPacingQueueOverloadError } from "../providers/request-pacing";
|
|
65
68
|
import {
|
|
66
69
|
hasKeyPoolFailover,
|
|
67
70
|
rateLimitRetryDelayMs,
|
|
@@ -91,12 +94,14 @@ import {
|
|
|
91
94
|
resolvePassthroughBodyGuard,
|
|
92
95
|
sanitizePassthroughToolCallIds,
|
|
93
96
|
tapAnthropicSseForLog,
|
|
94
|
-
} from "./
|
|
97
|
+
} from "./messages-shared";
|
|
95
98
|
import { beginInferenceAttempt } from "./inference/attempt";
|
|
96
99
|
import { createFinalRequestLog, type FinalRequestLogMeta } from "./inference/final-log";
|
|
97
100
|
import { registerTurn, unregisterTurn } from "./lifecycle";
|
|
98
101
|
import { nativeMessagesDeclineReason, type NativeMessagesSelector } from "./messages-native-eligibility";
|
|
99
102
|
import { messagesSecondaryInstanceUnavailable } from "./messages-native-selector";
|
|
103
|
+
import { guardNativeMessagesEmptyToolCatalog, nativeMessagesClientToolName, nativeMessagesInlineToolNames } from "./messages-native-tool-guard";
|
|
104
|
+
import { UNDECLARED_TOOL_CALL_ERROR_CODE, undeclaredToolCallMessage } from "./responses-undeclared-tool-guard";
|
|
100
105
|
import {
|
|
101
106
|
nativeOAuthBindingIsCurrent,
|
|
102
107
|
NativeOAuthSelectionChangedError,
|
|
@@ -315,6 +320,8 @@ export async function handleNativeMessages(options: HandleNativeMessagesOptions)
|
|
|
315
320
|
const { req, config, logCtx, logIds, route, body, requestedModel, translatorBudget } = options;
|
|
316
321
|
const nativeInstance = configuredAnthropicInstance(config, route.providerName);
|
|
317
322
|
const requestedStream = body.stream === true;
|
|
323
|
+
const clientDeclaredEmptyToolCatalog = Array.isArray(body.tools) && body.tools.length === 0;
|
|
324
|
+
const declaredInlineTools = clientDeclaredEmptyToolCatalog ? nativeMessagesInlineToolNames(body) : undefined;
|
|
318
325
|
logCtx.inboundProtocol = "messages";
|
|
319
326
|
logCtx.model = route.modelId;
|
|
320
327
|
logCtx.provider = route.providerName;
|
|
@@ -436,8 +443,16 @@ export async function handleNativeMessages(options: HandleNativeMessagesOptions)
|
|
|
436
443
|
}
|
|
437
444
|
} else {
|
|
438
445
|
// Same pre-dispatch key preference every direct send path applies (see chat-native.ts).
|
|
439
|
-
|
|
440
|
-
|
|
446
|
+
try {
|
|
447
|
+
const proactiveKeyProvider = selectProactiveApiKeyTransport(config, route.providerName, route.provider);
|
|
448
|
+
if (proactiveKeyProvider) route.provider = proactiveKeyProvider;
|
|
449
|
+
} catch (error) {
|
|
450
|
+
cleanupAbort(); upstream.abort();
|
|
451
|
+
if (!(error instanceof RequestPacingQueueOverloadError)) throw error;
|
|
452
|
+
const refusal = fail(429, error.message, "rate_limit_error");
|
|
453
|
+
refusal.headers.set("Retry-After", String(error.retryAfterSeconds));
|
|
454
|
+
return refusal;
|
|
455
|
+
}
|
|
441
456
|
}
|
|
442
457
|
let activeProvider: OcxProviderConfig = oauthBinding ? oauthProvider(oauthBinding) : route.provider;
|
|
443
458
|
stampApiKeyAccountLabel(logCtx, route.providerName, activeProvider);
|
|
@@ -521,6 +536,15 @@ export async function handleNativeMessages(options: HandleNativeMessagesOptions)
|
|
|
521
536
|
&& nativeMessagesDeclineReason({ ...route, provider: routed }, body, config, selector) === undefined;
|
|
522
537
|
};
|
|
523
538
|
|
|
539
|
+
// Keep the actual failed wire identity if a later recovery is refused before sending.
|
|
540
|
+
// A 429 remains eligible for explicit same-key retries until that ladder is finished.
|
|
541
|
+
let pendingDistributedFailure: (() => void) | undefined;
|
|
542
|
+
let distributedResponseOwnsKey = true;
|
|
543
|
+
const settleDistributedFailure = () => {
|
|
544
|
+
const record = pendingDistributedFailure;
|
|
545
|
+
pendingDistributedFailure = undefined;
|
|
546
|
+
record?.();
|
|
547
|
+
};
|
|
524
548
|
const send = async (recovery?: "rate-limit-429" | "oauth-401" | "oauth-account-403" | "key-429" | "key-401"): Promise<Response> => {
|
|
525
549
|
const remaining = remainingTransientSends();
|
|
526
550
|
if (requestTransientPolicy && remaining <= 0) {
|
|
@@ -538,7 +562,7 @@ export async function handleNativeMessages(options: HandleNativeMessagesOptions)
|
|
|
538
562
|
providerFetch(activeProvider, undefined, {
|
|
539
563
|
providerName: route.providerName,
|
|
540
564
|
modelId: route.modelId,
|
|
541
|
-
dispatchOverride: async (_input, init, execute) => {
|
|
565
|
+
dispatchOverride: async (_input, init, execute, reacquirePacing) => {
|
|
542
566
|
if (oauthBinding) {
|
|
543
567
|
if (!nativeOAuthRouteIsCurrent(oauthBinding)) throw new NativeOAuthSelectionChangedError();
|
|
544
568
|
// The OAuth twin of the key check below: re-resolve through the same selection
|
|
@@ -555,12 +579,25 @@ export async function handleNativeMessages(options: HandleNativeMessagesOptions)
|
|
|
555
579
|
rebuildFor(oauthProvider(oauthBinding));
|
|
556
580
|
logCtx.provider = formatAnthropicProviderForLog(route.providerName, oauthBinding.snapshot.accountId, config);
|
|
557
581
|
}
|
|
558
|
-
} else
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
582
|
+
} else {
|
|
583
|
+
for (let attempt = 0; !providerApiKeySelectionIsCurrent(config, route.providerName, activeProvider); attempt++) {
|
|
584
|
+
if (attempt >= 3) throw new Error("Provider key selection changed repeatedly before native Messages dispatch");
|
|
585
|
+
let current = resolveCurrentProviderApiKeyTransport(config, route.providerName, activeProvider);
|
|
586
|
+
if (!current && activeProvider.requestPacing?.enabled
|
|
587
|
+
&& (activeProvider.requestPacing.keys || activeProvider.requestPacing.keySelection === "round-robin")) {
|
|
588
|
+
const fresh = resolveCurrentProviderApiKeyTransport(config, route.providerName, { ...activeProvider, _apiKeyAttempt: undefined });
|
|
589
|
+
if (fresh) current = selectProactiveApiKeyTransport(config, route.providerName, fresh) ?? fresh;
|
|
590
|
+
}
|
|
591
|
+
if (!current || nativeMessagesDeclineReason({ ...route, provider: current }, body, config, selector) !== undefined) {
|
|
592
|
+
throw new Error("Provider key selection is no longer available for native Messages");
|
|
593
|
+
}
|
|
594
|
+
rebuildFor(current);
|
|
595
|
+
if (current.requestPacing?.enabled
|
|
596
|
+
&& (current.requestPacing.keys || current.requestPacing.keySelection === "round-robin")) {
|
|
597
|
+
if (!reacquirePacing) throw new Error("Refreshed native Messages dispatch requires pacing readmission");
|
|
598
|
+
await reacquirePacing(current, route.modelId);
|
|
599
|
+
}
|
|
562
600
|
}
|
|
563
|
-
rebuildFor(current);
|
|
564
601
|
}
|
|
565
602
|
// The retry closure may hold a pre-reselection request: send the current one whole.
|
|
566
603
|
const wire = activeRequest;
|
|
@@ -579,6 +616,7 @@ export async function handleNativeMessages(options: HandleNativeMessagesOptions)
|
|
|
579
616
|
}
|
|
580
617
|
const releaseFamily = snapshot && oauthBinding ? anthropicModelQuotaFor(oauthBinding.instance).claimAnthropicFamilyRevalidation(snapshot.accountId, route.modelId) : () => {};
|
|
581
618
|
if (!releaseFamily) throw new AnthropicAccountCooldownError(1);
|
|
619
|
+
const selection = activeProvider._apiKeyAttempt;
|
|
582
620
|
let dispatched: Response;
|
|
583
621
|
const spendReport = createPhysicalSendReporter(physicalBudget, () => ({
|
|
584
622
|
poolId: logCtx.spendPoolId ?? route.providerName, identityId: logCtx.accountLogLabel,
|
|
@@ -599,7 +637,9 @@ export async function handleNativeMessages(options: HandleNativeMessagesOptions)
|
|
|
599
637
|
if (snapshot && !sendOwner) throw new NativeOAuthSelectionChangedError();
|
|
600
638
|
physicalSendsUsed++;
|
|
601
639
|
dispatched = await sendWithConnectionPolicy(
|
|
602
|
-
|
|
640
|
+
distributedKeyPhysicalFetch(
|
|
641
|
+
anthropicBillingPhysicalFetch((activeProvider as OcxProviderTransport).fetch ?? execute, route.providerName, activeProvider),
|
|
642
|
+
route.providerName, activeProvider, wire.url, activeProvider.apiKeyTransport === "bearer" ? "authorization" : "x-api-key"),
|
|
603
643
|
wire.url,
|
|
604
644
|
applyUpstreamRecoveryInit({ ...init, method: "POST", headers, body: wire.body }, transportRecovery),
|
|
605
645
|
{ providerName: route.providerName, provider: activeProvider },
|
|
@@ -623,6 +663,14 @@ export async function handleNativeMessages(options: HandleNativeMessagesOptions)
|
|
|
623
663
|
}
|
|
624
664
|
} catch { /* Passive observation must not fail the response. */ }
|
|
625
665
|
}
|
|
666
|
+
const ownsDistributedKey = !selection?.distributed
|
|
667
|
+
|| distributedKeyResponseOwner(dispatched, config, route.providerName) !== null;
|
|
668
|
+
distributedResponseOwnsKey = ownsDistributedKey;
|
|
669
|
+
const failureStatus = dispatched.status;
|
|
670
|
+
pendingDistributedFailure = selection?.distributed && ownsDistributedKey && (failureStatus === 401 || failureStatus === 429)
|
|
671
|
+
&& !isNonReplayableResponse(dispatched)
|
|
672
|
+
? () => { recordDistributedResponseFailure(dispatched, config, route.providerName, req.signal); }
|
|
673
|
+
: undefined;
|
|
626
674
|
if (!dispatched.ok) await recordKeyAttemptFailure(logCtx, dispatched, init.signal ?? upstream.signal);
|
|
627
675
|
return dispatched;
|
|
628
676
|
},
|
|
@@ -647,25 +695,28 @@ export async function handleNativeMessages(options: HandleNativeMessagesOptions)
|
|
|
647
695
|
let response: Response;
|
|
648
696
|
try {
|
|
649
697
|
response = await send();
|
|
650
|
-
|
|
698
|
+
let retryPolicy: ReturnType<typeof rateLimitRetryPolicyFor> | undefined;
|
|
651
699
|
let retries = 0;
|
|
652
700
|
let billingRecoveryActive = false;
|
|
653
701
|
const keySendAvailable = () => transientSendAvailable()
|
|
654
702
|
&& (!billingRecoveryActive || Math.max(physicalSendsUsed, physicalBudget.used) < physicalBudget.policy.maxTotalModelSends);
|
|
655
|
-
|
|
703
|
+
const recoveryPasses = activeProvider._apiKeyAttempt?.distributed ? activeProvider.apiKeyPool?.length ?? 1 : 1;
|
|
704
|
+
for (let pass = 0; ; pass++) {
|
|
656
705
|
const priorResponse = response;
|
|
657
706
|
// A credential-scoped 401 on a static key pool says nothing about the sibling keys.
|
|
658
|
-
while (response.status === 401 && hasKeyPoolFailover(activeProvider) && keySendAvailable()) {
|
|
707
|
+
while (response.status === 401 && distributedResponseOwnsKey && hasKeyPoolFailover(activeProvider) && keySendAvailable()) {
|
|
659
708
|
const rotated = rotateProviderTransportOn401(config, route.providerName, activeProvider, {
|
|
660
709
|
now: Date.now(),
|
|
661
710
|
attemptedKey: activeProvider.apiKey,
|
|
711
|
+
attemptedSelection: activeProvider._apiKeyAttempt,
|
|
662
712
|
});
|
|
713
|
+
pendingDistributedFailure = undefined; // Rotation already records this physical failure.
|
|
663
714
|
if (!rotated) break;
|
|
664
715
|
discard(response);
|
|
665
716
|
rebuildFor(rotated);
|
|
666
717
|
response = await send("key-401");
|
|
667
718
|
}
|
|
668
|
-
while ((response.status === 400 || response.status === 402) && hasKeyPoolFailover(activeProvider)) {
|
|
719
|
+
while ((response.status === 400 || response.status === 402) && distributedResponseOwnsKey && hasKeyPoolFailover(activeProvider)) {
|
|
669
720
|
const rotated = await rotateAnthropicBillingKeyOnResponse(response, config, route.providerName, activeProvider, { signal: upstream.signal });
|
|
670
721
|
if (!rotated) break;
|
|
671
722
|
billingRecoveryActive = true;
|
|
@@ -674,9 +725,11 @@ export async function handleNativeMessages(options: HandleNativeMessagesOptions)
|
|
|
674
725
|
rebuildFor(rotated);
|
|
675
726
|
response = await send("key-401");
|
|
676
727
|
}
|
|
728
|
+
if (pass === 0) retryPolicy = oauthBinding ? undefined : rateLimitRetryPolicyFor(activeProvider);
|
|
677
729
|
// A refusal this proxy synthesized for a reset replay is not a provider rate limit.
|
|
678
730
|
while (
|
|
679
731
|
response.status === 429
|
|
732
|
+
&& distributedResponseOwnsKey
|
|
680
733
|
&& !isNonReplayableResponse(response)
|
|
681
734
|
&& retryPolicy
|
|
682
735
|
&& retries < retryPolicy.attempts
|
|
@@ -691,13 +744,14 @@ export async function handleNativeMessages(options: HandleNativeMessagesOptions)
|
|
|
691
744
|
if (upstream.signal.aborted) throw upstream.signal.reason;
|
|
692
745
|
response = await send("rate-limit-429");
|
|
693
746
|
}
|
|
694
|
-
while (response.status === 429 && !isNonReplayableResponse(response) && hasKeyPoolFailover(activeProvider)) {
|
|
747
|
+
while (response.status === 429 && distributedResponseOwnsKey && !isNonReplayableResponse(response) && hasKeyPoolFailover(activeProvider)) {
|
|
695
748
|
const rotated = rotateProviderTransportOn429(config, route.providerName, activeProvider, {
|
|
696
749
|
retryAfter: response.headers.get("retry-after"),
|
|
697
750
|
now: Date.now(),
|
|
698
751
|
attemptedKey: activeProvider.apiKey,
|
|
699
752
|
attemptedSelection: activeProvider._apiKeyAttempt,
|
|
700
753
|
});
|
|
754
|
+
pendingDistributedFailure = undefined; // Rotation already records this physical failure.
|
|
701
755
|
if (!rotated) break;
|
|
702
756
|
// Rotation already recorded the cooldown; keep the terminal 429 when no send remains.
|
|
703
757
|
if (!keySendAvailable()) break;
|
|
@@ -705,13 +759,17 @@ export async function handleNativeMessages(options: HandleNativeMessagesOptions)
|
|
|
705
759
|
rebuildFor(rotated);
|
|
706
760
|
response = await send("key-429");
|
|
707
761
|
}
|
|
708
|
-
|
|
709
|
-
|
|
762
|
+
if (response === priorResponse || isNonReplayableResponse(response) || !keySendAvailable()) break;
|
|
763
|
+
// Billing class transitions share the physical-send ceiling; distributed-only
|
|
764
|
+
// transitions additionally retain their original bounded pool-pass allowance.
|
|
765
|
+
if (distributedResponseOwnsKey && (
|
|
710
766
|
(billingRecoveryActive && [401, 429].includes(response.status))
|
|
711
767
|
|| (activeProvider.adapter === "anthropic" && hasKeyPoolFailover(activeProvider) && [400, 402].includes(response.status))
|
|
712
768
|
)) continue;
|
|
713
|
-
|
|
769
|
+
if (!activeProvider._apiKeyAttempt?.distributed || !distributedResponseOwnsKey
|
|
770
|
+
|| (response.status !== 401 && response.status !== 429) || pass + 1 >= recoveryPasses) break;
|
|
714
771
|
}
|
|
772
|
+
settleDistributedFailure();
|
|
715
773
|
const oauthRetryKey = {};
|
|
716
774
|
const triedAccountIds = new Set<string>();
|
|
717
775
|
let oauthFailovers = 0;
|
|
@@ -749,7 +807,13 @@ export async function handleNativeMessages(options: HandleNativeMessagesOptions)
|
|
|
749
807
|
cleanupAbort();
|
|
750
808
|
upstream.abort();
|
|
751
809
|
if (req.signal.aborted) return fail(499, "Client cancelled request", "api_error");
|
|
810
|
+
settleDistributedFailure();
|
|
752
811
|
const sendError = error instanceof UpstreamRetryEvidenceError ? error.cause : error;
|
|
812
|
+
if (sendError instanceof RequestPacingQueueOverloadError) {
|
|
813
|
+
const refusal = fail(429, sendError.message, "rate_limit_error");
|
|
814
|
+
refusal.headers.set("Retry-After", String(sendError.retryAfterSeconds));
|
|
815
|
+
return refusal;
|
|
816
|
+
}
|
|
753
817
|
if (sendError instanceof SendBudgetExhaustedError) return fail(429, sendError.message, "send_budget_exhausted", "send_budget_exhausted");
|
|
754
818
|
if (sendError instanceof NativeMessagesSpendRefusal) {
|
|
755
819
|
const refusal = workflowRefusalResponse("workflow-spend-exhausted", logCtx);
|
|
@@ -782,7 +846,7 @@ export async function handleNativeMessages(options: HandleNativeMessagesOptions)
|
|
|
782
846
|
upstream.abort();
|
|
783
847
|
return fail(499, "Client cancelled request", "api_error");
|
|
784
848
|
}
|
|
785
|
-
return retainUpstreamMessagesRequestId(nativeMessagesErrorResponse(response, bodyText, finishLog), response.headers);
|
|
849
|
+
return retainUpstreamMessagesRequestId(nativeMessagesErrorResponse(response, bodyText, finishLog, route.providerName, activeProvider), response.headers);
|
|
786
850
|
}
|
|
787
851
|
|
|
788
852
|
const contentType = response.headers.get("content-type")?.toLowerCase() ?? "";
|
|
@@ -794,15 +858,22 @@ export async function handleNativeMessages(options: HandleNativeMessagesOptions)
|
|
|
794
858
|
const renamed = activeRequest.oauthToolNames
|
|
795
859
|
? restoreOAuthToolNamesInSse(observed, activeRequest.oauthToolNames, translatorBudget)
|
|
796
860
|
: observed;
|
|
797
|
-
const
|
|
861
|
+
const echoed = echoRequestedModel(renamed, requestedModel);
|
|
862
|
+
let toolRefusal: string | undefined;
|
|
863
|
+
const source = clientDeclaredEmptyToolCatalog
|
|
864
|
+
? guardNativeMessagesEmptyToolCatalog(echoed, translatorBudget, message => {
|
|
865
|
+
toolRefusal = message;
|
|
866
|
+
logCtx.errorCode = UNDECLARED_TOOL_CALL_ERROR_CODE;
|
|
867
|
+
}, declaredInlineTools)
|
|
868
|
+
: echoed;
|
|
798
869
|
if (requestedStream) {
|
|
799
870
|
transferTurnToStream();
|
|
800
871
|
const relayed = tapAnthropicSseForLog(source, logCtx, (status, meta) => {
|
|
801
872
|
try {
|
|
802
873
|
cleanupAbort();
|
|
803
874
|
bindUsage(logCtx.usage);
|
|
804
|
-
finishLog(status,
|
|
805
|
-
if (meta.closeReason !== "terminal") upstream.abort();
|
|
875
|
+
finishLog(toolRefusal ? 502 : status, toolRefusal, toolRefusal ? { ...meta, terminalStatus: "failed" } : meta);
|
|
876
|
+
if (toolRefusal || meta.closeReason !== "terminal") upstream.abort();
|
|
806
877
|
} finally {
|
|
807
878
|
releaseStreamTurn();
|
|
808
879
|
}
|
|
@@ -833,6 +904,11 @@ export async function handleNativeMessages(options: HandleNativeMessagesOptions)
|
|
|
833
904
|
if (message.type === "error") {
|
|
834
905
|
const error = isRec(message.error) ? message.error : {};
|
|
835
906
|
const text = typeof error.message === "string" ? error.message : "upstream stream failed";
|
|
907
|
+
if (toolRefusal) {
|
|
908
|
+
upstream.abort();
|
|
909
|
+
finishLog(502, toolRefusal, { closeReason: "terminal", terminalStatus: "failed" });
|
|
910
|
+
return fail(502, toolRefusal, "api_error", UNDECLARED_TOOL_CALL_ERROR_CODE);
|
|
911
|
+
}
|
|
836
912
|
// The tap's own verdict (a reset, a stall, the byte cap) closes this row with the same meta
|
|
837
913
|
// as the streaming lane's row; a plain upstream error event keeps the non_stream row.
|
|
838
914
|
const tapMeta = tapState.meta;
|
|
@@ -884,6 +960,11 @@ export async function handleNativeMessages(options: HandleNativeMessagesOptions)
|
|
|
884
960
|
const message = activeRequest.oauthToolNames
|
|
885
961
|
? restoreOAuthToolNamesInMessage(parsedMessage, activeRequest.oauthToolNames)
|
|
886
962
|
: parsedMessage;
|
|
963
|
+
const undeclaredTool = clientDeclaredEmptyToolCatalog ? nativeMessagesClientToolName(message, declaredInlineTools) : undefined;
|
|
964
|
+
if (undeclaredTool !== undefined) {
|
|
965
|
+
logCtx.errorCode = UNDECLARED_TOOL_CALL_ERROR_CODE;
|
|
966
|
+
return fail(502, undeclaredToolCallMessage(undeclaredTool), "api_error", UNDECLARED_TOOL_CALL_ERROR_CODE);
|
|
967
|
+
}
|
|
887
968
|
bindUsage(anthropicUsageToOcx(isRec(message.usage) ? message.usage : undefined));
|
|
888
969
|
if (logIds) recordFirstOutput(logCtx, logIds.start);
|
|
889
970
|
try {
|
|
@@ -919,7 +1000,7 @@ function isNativeOverflowError(error: Rec, message: string): boolean {
|
|
|
919
1000
|
* Messages path, so a client sees the same contract on either lane: transient 5xx become 529
|
|
920
1001
|
* with a retry hint, and a proxy-side replay refusal keeps its no-retry marking.
|
|
921
1002
|
*/
|
|
922
|
-
function nativeMessagesErrorResponse(response: Response, bodyText: string, finishLog: FinishLog): Response {
|
|
1003
|
+
function nativeMessagesErrorResponse(response: Response, bodyText: string, finishLog: FinishLog, providerName: string, provider: OcxProviderConfig): Response {
|
|
923
1004
|
let upstreamType: string | undefined;
|
|
924
1005
|
let upstreamCode: string | undefined;
|
|
925
1006
|
let upstreamMessage: string | undefined;
|
|
@@ -933,8 +1014,20 @@ function nativeMessagesErrorResponse(response: Response, bodyText: string, finis
|
|
|
933
1014
|
}
|
|
934
1015
|
} catch { /* keep generic classification */ }
|
|
935
1016
|
const detail = formatAnthropicErrorBody(response.status, response.headers, bodyText);
|
|
936
|
-
const
|
|
1017
|
+
const rawMessage = upstreamMessage
|
|
937
1018
|
?? (detail ? `Provider error ${response.status}: ${detail}` : `Provider error ${response.status}`);
|
|
1019
|
+
const message = enrichOpenCodeZenUpstreamMessage(rawMessage, {
|
|
1020
|
+
status: response.status,
|
|
1021
|
+
providerName,
|
|
1022
|
+
baseUrl: provider.baseUrl,
|
|
1023
|
+
adapter: provider.adapter,
|
|
1024
|
+
authMode: provider.authMode,
|
|
1025
|
+
hasApiKey: Boolean(provider.apiKey?.trim()),
|
|
1026
|
+
upstreamRetryAfter: response.headers.get("retry-after"),
|
|
1027
|
+
upstreamErrorType: upstreamType,
|
|
1028
|
+
upstreamErrorCode: upstreamCode,
|
|
1029
|
+
supportsHttpSameKeyRetry: true,
|
|
1030
|
+
});
|
|
938
1031
|
const classified = classifyError(
|
|
939
1032
|
response.status,
|
|
940
1033
|
upstreamType ?? (response.status === 401 ? "authentication_error"
|