@bitkyc08/opencodex 2.84.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/README.md +0 -4
- package/bin/ocx.mjs +61 -3
- package/gui/dist/assets/App-ZfYbLHkB.js +58 -0
- package/gui/dist/assets/App-wT3mXqFz.css +1 -0
- package/gui/dist/assets/{Tray-C9TGtwad.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 -2
- package/src/adapters/anthropic/beta-allowlist.ts +91 -11
- package/src/adapters/anthropic/beta-body-pairs.ts +93 -0
- package/src/adapters/anthropic/client-identity.ts +1 -1
- package/src/adapters/anthropic/passthrough.ts +16 -3
- package/src/adapters/anthropic-progress.ts +84 -0
- package/src/adapters/anthropic.ts +108 -21
- package/src/adapters/azure.ts +19 -1
- 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/client-fingerprint.ts +24 -0
- package/src/adapters/coding-agent/protocol.ts +1 -1
- package/src/adapters/coding-agent/turn.ts +40 -11
- package/src/adapters/cursor/arg-codec.ts +17 -5
- package/src/adapters/cursor/catalog.ts +8 -0
- package/src/adapters/cursor/effort-map.ts +3 -0
- package/src/adapters/cursor/freeform-args.ts +75 -0
- package/src/adapters/cursor/protobuf-events.ts +36 -23
- package/src/adapters/dsh-account.ts +97 -0
- package/src/adapters/google-antigravity-wire.ts +22 -1
- package/src/adapters/google.ts +37 -10
- 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-chat/response-events.ts +59 -1
- package/src/adapters/openai-chat.ts +8 -5
- 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 +44 -3
- 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 +223 -0
- package/src/advisor/context.ts +238 -0
- package/src/advisor/disclosure.ts +20 -0
- package/src/advisor/evidence-tool.ts +21 -0
- package/src/advisor/evidence.ts +117 -0
- package/src/advisor/runtime.ts +353 -0
- package/src/advisor/settings.ts +144 -0
- package/src/advisor/state.ts +369 -0
- package/src/advisor/synthetic-tool.ts +33 -0
- package/src/bridge/errors.ts +24 -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/chat/inbound.ts +30 -6
- package/src/chat/outbound.ts +5 -0
- package/src/claude/desktop-3p-library.ts +72 -7
- package/src/claude/desktop-3p.ts +75 -19
- package/src/claude/desktop-first-party.ts +1 -1
- package/src/claude/desktop-picker-platform.ts +4 -0
- package/src/claude/desktop-picker-profile.ts +11 -4
- package/src/claude/desktop-picker.ts +15 -8
- package/src/claude/desktop-profile.ts +10 -1
- package/src/claude/desktop-remote-store-io.ts +5 -3
- package/src/claude/desktop-system-proxy.ts +50 -4
- package/src/claude/inbound-content-options.ts +11 -0
- package/src/claude/inbound.ts +154 -50
- package/src/claude/intercept/local-ca.ts +8 -1
- 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/model-default-settings.ts +154 -0
- package/src/claude/model-info.ts +7 -7
- package/src/claude/outbound.ts +51 -19
- 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 +165 -0
- package/src/cli/auxiliary-listener-preflight.ts +37 -0
- package/src/cli/capabilities-accounts.ts +20 -1
- package/src/cli/capabilities-agents-routing.ts +23 -2
- package/src/cli/capabilities-base.ts +39 -11
- package/src/cli/capabilities-observe-system.ts +63 -3
- package/src/cli/capabilities-provider-models.ts +11 -0
- package/src/cli/chatgpt-command.ts +35 -14
- package/src/cli/claude.ts +46 -6
- 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 +30 -5
- package/src/cli/doctor.ts +15 -1
- package/src/cli/ensure-desktop-cold-start.ts +382 -0
- package/src/cli/export-command.ts +1 -1
- package/src/cli/help.ts +4 -2
- package/src/cli/index.ts +14 -5
- package/src/cli/integration-plan-dto.ts +3 -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 +41 -9
- package/src/cli/restart-scope.ts +3 -1
- 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 +3 -1
- package/src/clients/config-export/model-metadata.ts +3 -2
- package/src/clients/config-export/qoder.ts +93 -0
- package/src/clients/config-export.ts +35 -48
- 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/pool-mode-gate.ts +5 -2
- 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 +81 -7
- package/src/codex/autostart-health.ts +4 -0
- package/src/codex/catalog/auto-review.ts +20 -3
- package/src/codex/catalog/build-entries.ts +19 -2
- package/src/codex/catalog/combo-member.ts +14 -5
- package/src/codex/catalog/configured-tiers.ts +50 -0
- package/src/codex/catalog/effort.ts +4 -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 +42 -19
- package/src/codex/catalog/model-visibility.ts +3 -1
- package/src/codex/catalog/native-ultrafast.ts +106 -0
- package/src/codex/catalog/parsing.ts +35 -86
- package/src/codex/catalog/provider-models.ts +40 -34
- package/src/codex/catalog/retained-sync.ts +2 -0
- package/src/codex/catalog/routed-gather.ts +33 -16
- package/src/codex/catalog/vendor-context.ts +82 -0
- 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 +63 -4
- package/src/codex/convergence.ts +2 -0
- package/src/codex/desktop-app/picker-freshness.ts +96 -0
- package/src/codex/desktop-app/types.ts +2 -0
- package/src/codex/desktop-app/windows.ts +29 -16
- package/src/codex/desktop-app-restart.ts +25 -2
- package/src/codex/desktop-compatibility/certificate-service.ts +99 -0
- package/src/codex/desktop-compatibility/certificate-store.ts +193 -0
- package/src/codex/desktop-compatibility/connection-store.ts +87 -0
- package/src/codex/desktop-compatibility/installed-build.ts +73 -0
- package/src/codex/desktop-compatibility/json-body.ts +29 -0
- package/src/codex/desktop-compatibility/native-identity.ts +62 -0
- package/src/codex/desktop-compatibility/relay-listener.ts +93 -0
- package/src/codex/desktop-compatibility/routing-binding.ts +10 -0
- package/src/codex/desktop-compatibility/routing-preflight.ts +69 -0
- package/src/codex/desktop-compatibility/runtime-ownership.ts +28 -0
- package/src/codex/desktop-compatibility/runtime.ts +182 -0
- package/src/codex/desktop-compatibility/service.ts +30 -0
- package/src/codex/desktop-compatibility/startup-settings.ts +50 -0
- package/src/codex/desktop-compatibility/usage-activation.ts +86 -0
- package/src/codex/desktop-compatibility/usage-controlled-fetch.ts +59 -0
- package/src/codex/desktop-compatibility/usage-controller.ts +107 -0
- package/src/codex/desktop-compatibility/usage-policy.ts +59 -0
- package/src/codex/desktop-compatibility/usage-refresh.ts +33 -0
- package/src/codex/desktop-compatibility/usage-sse-controller.ts +86 -0
- package/src/codex/desktop-compatibility/windows-activation-source.ts +44 -0
- package/src/codex/desktop-compatibility/windows-certificate-trust.ts +81 -0
- package/src/codex/desktop-compatibility/windows-key-protection.ts +165 -0
- package/src/codex/desktop-compatibility/windows-package-command.ts +86 -0
- package/src/codex/desktop-compatibility/windows-package-launch.ts +28 -0
- package/src/codex/features.ts +85 -45
- package/src/codex/history-provider.ts +1 -1
- package/src/codex/inject/config-write-section.ts +4 -1
- package/src/codex/inject/history-failure.ts +27 -0
- package/src/codex/inject/remove.ts +18 -6
- 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/journal.ts +16 -1
- package/src/codex/main-account-hard-lock.ts +30 -1
- package/src/codex/main-account.ts +99 -15
- package/src/codex/native-main-refresh-mode.ts +217 -0
- package/src/codex/native-profile-startup.ts +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 +2 -2
- package/src/codex/routing/pool-recovery.ts +79 -0
- package/src/codex/routing/selection.ts +4 -3
- package/src/codex/routing/thread-affinity.ts +1 -1
- package/src/codex/shim-diagnostics.ts +6 -0
- package/src/codex/shim-overlay.ts +7 -2
- 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 +35 -7
- package/src/config/live-reconcile.ts +51 -2
- package/src/config/load-degrade.ts +23 -1
- 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/paths.ts +10 -3
- package/src/config/provider-validation.ts +45 -2
- package/src/config/schema/config-schema.ts +39 -0
- package/src/config/schema/desktop-compatibility.ts +9 -0
- package/src/config/schema/leaf-validators.ts +39 -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 +962 -350
- package/src/generated/model-metadata.ts +1 -1
- 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/config-io.ts +20 -2
- package/src/integrations/journal.ts +15 -1
- package/src/integrations/mutation-plan.ts +2 -0
- package/src/integrations/registry.ts +15 -0
- package/src/integrations/store.ts +3 -0
- package/src/integrations/writer.ts +10 -1
- package/src/lib/advisor-activation.ts +16 -0
- package/src/lib/background-work.ts +65 -0
- package/src/lib/bounded-file-read.ts +58 -0
- package/src/lib/bun-path-runtime.d.mts +10 -1
- package/src/lib/bun-path-runtime.mjs +62 -3
- package/src/lib/codex-restart-contract.ts +15 -0
- package/src/lib/desktop-proxy-route.ts +30 -0
- package/src/lib/desktop-upstream-tunnel.ts +72 -0
- package/src/lib/empty-tool-catalog.ts +62 -0
- package/src/lib/errors.ts +26 -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/local-internal-call-capability.ts +55 -0
- package/src/lib/opaque-proxy-tunnel.ts +217 -0
- package/src/lib/package-tree-integrity.ts +2 -2
- package/src/lib/redact.ts +2 -2
- package/src/lib/socks5-fetch.ts +48 -71
- package/src/lib/socks5-handshake.ts +112 -0
- package/src/lib/translator-budget.ts +7 -1
- package/src/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 +62 -12
- package/src/oauth/generic-session-affinity.ts +125 -0
- package/src/oauth/index.ts +188 -111
- package/src/oauth/kiro-device-login.ts +2 -1
- package/src/oauth/kiro.ts +7 -1
- 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/codecs/chat-ir.ts +467 -0
- package/src/protocols/codecs/chat.ts +10 -1
- package/src/protocols/codecs/messages-ir.ts +500 -0
- package/src/protocols/encoders/adapter-events.ts +5 -4
- package/src/protocols/encoders/messages.ts +9 -6
- package/src/protocols/features.ts +10 -2
- package/src/protocols/path.ts +11 -1
- package/src/protocols/plan-snapshot.ts +39 -16
- package/src/protocols/plan.ts +9 -1
- package/src/protocols/provider-summary.ts +2 -1
- package/src/protocols/settings.ts +8 -0
- package/src/providers/antigravity-account-roster.ts +102 -0
- package/src/providers/antigravity-models.ts +85 -7
- 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 +5 -0
- package/src/providers/model-rename-migration.ts +7 -5
- 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 +44 -14
- package/src/providers/registry/entries-extended.ts +19 -0
- 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 +78 -22
- 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/responses/state.ts +32 -29
- package/src/router.ts +8 -321
- package/src/routing/health.ts +37 -22
- package/src/routing/history/build-file.ts +92 -0
- package/src/routing/history/indexer.ts +254 -221
- package/src/routing/history/ingest.ts +242 -0
- package/src/routing/history/rebuild-job.ts +111 -0
- package/src/routing/history/rebuild-protocol.ts +15 -0
- package/src/routing/history/rebuild-worker.ts +34 -0
- package/src/routing/profile.ts +1 -1
- package/src/server/admission-model-scope.ts +21 -1
- package/src/server/antigravity-direct-eligibility.ts +208 -0
- package/src/server/auth-cors.ts +15 -0
- package/src/server/chat-completions.ts +82 -10
- 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 +164 -433
- package/src/server/effort-policy.ts +1 -1
- package/src/server/images.ts +15 -2
- package/src/server/index/desktop-compatibility-startup.ts +56 -0
- 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 +11 -3
- package/src/server/index/startup-warnings.ts +11 -0
- package/src/server/index/websocket-handler.ts +9 -0
- package/src/server/index.ts +14 -11
- package/src/server/inference/advisor-diagnostics.ts +31 -0
- package/src/server/inference/client-encoder-delivery.ts +6 -5
- package/src/server/login-shell-path.ts +146 -0
- package/src/server/management/advisor-routes.ts +202 -0
- package/src/server/management/agent-settings-routes.ts +22 -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 +4 -0
- package/src/server/management/config-routes.ts +77 -6
- package/src/server/management/context.ts +4 -0
- package/src/server/management/cursor-integration-routes.ts +1 -1
- package/src/server/management/desktop-compatibility-routes.ts +46 -0
- package/src/server/management/desktop-compatibility-runtime-routes.ts +46 -0
- package/src/server/management/desktop-compatibility-settings-routes.ts +27 -0
- package/src/server/management/model-routes.ts +13 -8
- 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/protocol-settings-patch.ts +1 -0
- 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 +21 -1
- package/src/server/management/sibling-guard.ts +1 -0
- package/src/server/management-api.ts +9 -1
- package/src/server/messages-antigravity-direct.ts +195 -0
- package/src/server/messages-native-tool-guard.ts +185 -0
- package/src/server/messages-native.ts +106 -22
- 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 +13 -1
- package/src/server/responses/account-change-state.ts +8 -2
- package/src/server/responses/adapter-continuation.ts +48 -19
- package/src/server/responses/adapter-delivery.ts +43 -8
- package/src/server/responses/adapter-dispatch.ts +48 -4
- package/src/server/responses/advisor-plan-slot.ts +27 -0
- package/src/server/responses/advisor-retention.ts +117 -0
- package/src/server/responses/advisor-slot.ts +368 -0
- package/src/server/responses/code-mode-collab-surface.ts +116 -8
- 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 +12 -1
- package/src/server/responses/core-combo.ts +141 -36
- package/src/server/responses/core-errors.ts +3 -3
- package/src/server/responses/core-lifetime.ts +7 -2
- package/src/server/responses/core-normalize.ts +11 -0
- package/src/server/responses/core-opaque-recovery.ts +35 -0
- package/src/server/responses/core-options.ts +51 -0
- package/src/server/responses/core-pipeline.ts +141 -0
- package/src/server/responses/core-replay.ts +25 -6
- package/src/server/responses/core.ts +3 -116
- 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 +19 -11
- package/src/server/responses/passthrough-dispatch.ts +29 -16
- package/src/server/responses/request-prepare.ts +144 -17
- package/src/server/responses/request-sidecar-auth.ts +2 -2
- package/src/server/responses/request-transport.ts +120 -27
- package/src/server/responses/response-effects.ts +1 -0
- package/src/server/responses/run-turn-execution.ts +166 -38
- package/src/server/responses/sidecar-execution.ts +98 -5
- package/src/server/responses/terminal-guard.ts +4 -17
- package/src/server/responses-undeclared-tool-guard.ts +30 -9
- package/src/server/startup-action-control.ts +16 -2
- package/src/server/startup-health-cache.ts +1 -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/desktop-startup.ts +14 -0
- package/src/service/guards.ts +1 -61
- package/src/service/health-sentinel-linux.ts +112 -0
- package/src/service/health-sentinel-main.ts +99 -61
- package/src/service/health-sentinel-probe.ts +24 -7
- package/src/service/health.ts +5 -89
- package/src/service/launchd.ts +27 -7
- package/src/service/logging.ts +135 -0
- 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/tray/windows-tray.ps1 +43 -24
- package/src/types/config.ts +124 -6
- package/src/types/opencode-go.ts +61 -0
- package/src/types/opencode-zen.ts +29 -0
- package/src/types/provider.ts +39 -6
- package/src/types/request.ts +38 -2
- package/src/types/tools.ts +2 -0
- package/src/types/wire.ts +29 -37
- package/src/types.ts +1 -0
- package/src/update/async-check.ts +1 -1
- package/src/update/badge.ts +2 -2
- package/src/update/index.ts +88 -15
- package/src/update/job-supervised-restart.ts +107 -0
- package/src/update/job.ts +32 -32
- package/src/update/notify.ts +1 -1
- package/src/update/npm-invocation.mjs +12 -2
- package/src/update/refresh-scheduler.ts +2 -2
- package/src/update/supervised-recovery.d.mts +27 -0
- package/src/update/supervised-recovery.mjs +157 -0
- package/src/usage/cache-diagnostic.ts +2 -0
- package/src/usage/cost.ts +48 -12
- package/src/usage/expected-prices.ts +15 -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/gemini-executor.ts +2 -2
- 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/assets/sponsors/packycode.png +0 -0
- package/gui/dist/assets/App-CSJvmjNs.css +0 -1
- package/gui/dist/assets/App-CZtqiGcw.js +0 -56
- package/gui/dist/assets/index-BB6uK6tU.css +0 -1
- package/gui/dist/assets/index-CTm4l-HQ.js +0 -94
- package/gui/dist/assets/quota-summary-BbEeRfPY.js +0 -1
|
@@ -15,19 +15,25 @@ import {
|
|
|
15
15
|
} from "./admission-model-scope";
|
|
16
16
|
import { jsonUtf8Bytes } from "../lib/json-byte-size";
|
|
17
17
|
import { redactSecretString } from "../lib/redact";
|
|
18
|
-
import {
|
|
19
|
-
import { enforceAnthropicImageLimits, sniffImageDimensions } from "../adapters/anthropic-image-guard";
|
|
18
|
+
import { enforceAnthropicImageLimits } from "../adapters/anthropic-image-guard";
|
|
20
19
|
import { normalizeAnthropicImages } from "../adapters/anthropic-image-normalize";
|
|
21
|
-
import { createToolCallIdAllocator } from "../adapters/tool-call-id";
|
|
22
20
|
import { openAIChatSerializesThinking } from "../adapters/openai-chat/messages";
|
|
23
21
|
import { messagesToResponsesTranslation } from "../protocols/codecs/messages";
|
|
24
|
-
import {
|
|
22
|
+
import {
|
|
23
|
+
messagesDirectPlanHolds,
|
|
24
|
+
messagesDirectRequestOption,
|
|
25
|
+
messagesDirectTraceDecline,
|
|
26
|
+
planMessagesAntigravityDirect,
|
|
27
|
+
type MessagesDirectPlan,
|
|
28
|
+
} from "./messages-antigravity-direct";
|
|
29
|
+
import { AnthropicRequestError, callerCacheRetention, DesktopModelMappingUnavailableError, extractOcxEffortDirective, extractOcxRouteDirective, nativeAnthropicProjection, resolveInboundModel, type ClaudeCacheKeySource } from "../claude/inbound";
|
|
25
30
|
import { nativeReasoningTag, type NativeReasoningOwner } from "../responses/reasoning-replay-cache";
|
|
26
31
|
import { isKnownDesktop3pModelId, resolveDesktop3pAlias } from "../claude/desktop-3p";
|
|
27
32
|
import { resolveAlias, claudeCodeNativeAlias, legacyAliasForNative } from "../claude/alias";
|
|
28
33
|
import { recordDesktopRequest } from "../claude/desktop-health";
|
|
29
34
|
import { stripOneMillionMarker } from "../claude/context-windows";
|
|
30
35
|
import { captureClaudeInbound } from "../claude/inbound-debug";
|
|
36
|
+
import { declaredAnthropicServerTools } from "../claude/inbound-content-options";
|
|
31
37
|
import { claudeCodeForIngress } from "../claude/intercept/model-bindings";
|
|
32
38
|
import { classifyInterceptClient } from "../claude/intercept/client-class";
|
|
33
39
|
import { analyzeClaudeCompatibility, isClaudeCompatibilityMode } from "../claude/compatibility";
|
|
@@ -40,6 +46,7 @@ import {
|
|
|
40
46
|
REPLAY_REFUSED_STATUS,
|
|
41
47
|
UPSTREAM_RESET_REPLAY_REFUSED_CODE,
|
|
42
48
|
} from "../lib/upstream-retry";
|
|
49
|
+
import { isClientClosedMessage, recognizedUpstreamError, UNDECLARED_TOOL_CALL_ERROR_CODE } from "../lib/errors";
|
|
43
50
|
import { resolveClientRetryAfter } from "../lib/retry-after";
|
|
44
51
|
import { anthropicRateLimitHeaders } from "./anthropic-rate-limit-headers";
|
|
45
52
|
import { upstreamMessagesRequestIdHeaders } from "./messages-response-headers";
|
|
@@ -51,10 +58,8 @@ import {
|
|
|
51
58
|
responsesSseToAnthropicSse,
|
|
52
59
|
} from "../claude/outbound";
|
|
53
60
|
import { clearableDeadline, idleDeadline } from "../lib/abort";
|
|
54
|
-
import { estimateTokens } from "../lib/token-estimate";
|
|
55
61
|
import {
|
|
56
62
|
CLAUDE_NATIVE_THINKING,
|
|
57
|
-
projectClaudeRequest,
|
|
58
63
|
type ClaudeThinkingProjection,
|
|
59
64
|
} from "../lib/claude-request-projection";
|
|
60
65
|
import { captureRouteStaticPolicy, NoEligiblePolicyCandidateError, previewRouteModel, routedProviderConfig, UnknownRoutingPolicyError, routeModel, type RouteResult } from "../router";
|
|
@@ -63,7 +68,6 @@ import { resolveWireProtocolOverride } from "./adapter-resolve";
|
|
|
63
68
|
import type { OcxConfig } from "../types";
|
|
64
69
|
import { readJsonRequestBody, resolveInboundBodyLimitBytes } from "./request-decompress";
|
|
65
70
|
import { addFinalRequestLog, httpStatusForRequestLogTerminal, recordFirstOutput, type RequestLogContext } from "./request-log";
|
|
66
|
-
import { recordGenerationEvent } from "./request-log-generation-window";
|
|
67
71
|
import { createFinalRequestLog } from "./inference/final-log";
|
|
68
72
|
import {
|
|
69
73
|
conversationIdFromClaudeMetadata,
|
|
@@ -276,309 +280,39 @@ function uuidFromHex(hex32: string): string {
|
|
|
276
280
|
return `${h.slice(0, 8)}-${h.slice(8, 12)}-4${h.slice(13, 16)}-8${h.slice(17, 20)}-${h.slice(20, 32)}`;
|
|
277
281
|
}
|
|
278
282
|
|
|
279
|
-
export function anthropicUsageToOcx(usage: Rec | undefined): { inputTokens: number; outputTokens: number; cachedInputTokens?: number; cacheReadInputTokens?: number; cacheCreationInputTokens?: number } | undefined {
|
|
280
|
-
if (!usage) return undefined;
|
|
281
|
-
const num = (v: unknown) => typeof v === "number" ? v : 0;
|
|
282
|
-
const hasCache = usage.cache_read_input_tokens !== undefined || usage.cache_creation_input_tokens !== undefined;
|
|
283
|
-
const read = num(usage.cache_read_input_tokens);
|
|
284
|
-
const write = num(usage.cache_creation_input_tokens);
|
|
285
|
-
// Anthropic input_tokens excludes cache read/write; normalize to the canonical
|
|
286
|
-
// inclusive convention (types.ts OcxUsage / devlog 070). cached = READS only.
|
|
287
|
-
return {
|
|
288
|
-
inputTokens: num(usage.input_tokens) + read + write,
|
|
289
|
-
outputTokens: num(usage.output_tokens),
|
|
290
|
-
...(hasCache ? {
|
|
291
|
-
cachedInputTokens: read,
|
|
292
|
-
cacheReadInputTokens: read,
|
|
293
|
-
cacheCreationInputTokens: write,
|
|
294
|
-
} : {}),
|
|
295
|
-
};
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
/** Body-occupancy guard for the native passthrough (devlog 260716_passthrough_followups/010). */
|
|
299
|
-
export interface PassthroughBodyGuard {
|
|
300
|
-
/** Idle window in ms — raw upstream-byte inactivity while a read is pending. 0 disables. */
|
|
301
|
-
stallMs: number;
|
|
302
|
-
/** Cumulative body byte cap. 0 disables. */
|
|
303
|
-
maxBytes: number;
|
|
304
|
-
/** Client request signal for deterministic cancel classification. */
|
|
305
|
-
reqSignal?: AbortSignal;
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
type PassthroughCloseReason = "terminal" | "client_cancel" | "body_stall" | "body_overflow";
|
|
309
|
-
type PassthroughFinalizeMeta = { closeReason: PassthroughCloseReason; terminalStatus?: "failed" | "incomplete" };
|
|
310
|
-
|
|
311
283
|
/**
|
|
312
|
-
*
|
|
313
|
-
*
|
|
314
|
-
*
|
|
315
|
-
*
|
|
316
|
-
* headers went out, it appends a protocol-compatible Anthropic `event: error` terminal
|
|
317
|
-
* frame after a blank-line boundary, closes, and cancels the upstream reader — never a
|
|
318
|
-
* total-wall-clock bound (slow-but-alive streams live).
|
|
319
|
-
* Exported for deterministic unit tests.
|
|
284
|
+
* The passthrough accounting layer, shared with the managed native lane. It lives in
|
|
285
|
+
* messages-shared.ts so that lane can take it without importing this module, and this
|
|
286
|
+
* module without importing that lane. Re-exported here because this file was its only
|
|
287
|
+
* home and callers still resolve it from this module.
|
|
320
288
|
*/
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
let usageAcc: Rec = {};
|
|
331
|
-
// message_stop or an upstream error event: the turn's own terminal already went through.
|
|
332
|
-
let terminalSeen = false;
|
|
333
|
-
const inspectFrame = (frame: string) => {
|
|
334
|
-
const dataLine = frame
|
|
335
|
-
.split("\n")
|
|
336
|
-
.map(l => sseFieldValue(l, "data"))
|
|
337
|
-
.filter((v): v is string => v !== null)
|
|
338
|
-
.join("");
|
|
339
|
-
if (!dataLine) return;
|
|
340
|
-
let data: unknown;
|
|
341
|
-
try { data = JSON.parse(dataLine); } catch { return; }
|
|
342
|
-
if (!isRec(data)) return;
|
|
343
|
-
recordGenerationEvent(logCtx, data.type);
|
|
344
|
-
if (data.type === "message_start" && isRec(data.message) && isRec(data.message.usage)) {
|
|
345
|
-
usageAcc = { ...usageAcc, ...data.message.usage };
|
|
346
|
-
} else if (data.type === "message_delta" && isRec(data.usage)) {
|
|
347
|
-
usageAcc = { ...usageAcc, ...data.usage };
|
|
348
|
-
} else if (data.type === "message_stop" || data.type === "error") {
|
|
349
|
-
terminalSeen = true;
|
|
350
|
-
}
|
|
351
|
-
};
|
|
352
|
-
const inspect = (chunk: Uint8Array) => {
|
|
353
|
-
buffer += decoder.decode(chunk, { stream: true });
|
|
354
|
-
// SSE lines may end in CRLF, LF or CR. Normalize the inspection copy to LF (the forwarded
|
|
355
|
-
// bytes are untouched), holding a trailing CR until the next chunk shows whether an LF
|
|
356
|
-
// follows it, so a CRLF split across chunks stays one line ending.
|
|
357
|
-
const heldCr = buffer.endsWith("\r");
|
|
358
|
-
buffer = (heldCr ? buffer.slice(0, -1) : buffer).replace(/\r\n?/g, "\n") + (heldCr ? "\r" : "");
|
|
359
|
-
let sep: number;
|
|
360
|
-
while ((sep = buffer.indexOf("\n\n")) !== -1) {
|
|
361
|
-
const frame = buffer.slice(0, sep);
|
|
362
|
-
buffer = buffer.slice(sep + 2);
|
|
363
|
-
inspectFrame(frame);
|
|
364
|
-
}
|
|
365
|
-
};
|
|
366
|
-
// The last block can sit in the buffer without its blank line: an upstream that stopped after
|
|
367
|
-
// it, or a held trailing CR. Count it before deciding how the turn ended (the Responses relay
|
|
368
|
-
// flushes the same candidate). Returns true when this flush found the terminal in a block the
|
|
369
|
-
// client has not seen a blank line after, so the caller restores one.
|
|
370
|
-
const flushTail = (): boolean => {
|
|
371
|
-
const terminalBefore = terminalSeen;
|
|
372
|
-
const tail = (buffer + decoder.decode()).replace(/\r\n?/g, "\n");
|
|
373
|
-
buffer = "";
|
|
374
|
-
if (tail) inspectFrame(tail);
|
|
375
|
-
return terminalSeen && !terminalBefore && !tail.endsWith("\n\n");
|
|
376
|
-
};
|
|
377
|
-
const reader = upstream.getReader();
|
|
378
|
-
let settled = false;
|
|
379
|
-
let bodyBytes = 0;
|
|
380
|
-
let tapController: ReadableStreamDefaultController<Uint8Array> | undefined;
|
|
381
|
-
|
|
382
|
-
const recordUsage = () => {
|
|
383
|
-
logCtx.usage = anthropicUsageToOcx(Object.keys(usageAcc).length > 0 ? usageAcc : undefined);
|
|
384
|
-
};
|
|
385
|
-
const closeWithErrorFrame = (errType: string, message: string) => {
|
|
386
|
-
const payload = JSON.stringify({ type: "error", error: { type: errType, message } });
|
|
387
|
-
try {
|
|
388
|
-
// Leading blank line terminates any partial SSE block so the frame parses cleanly
|
|
389
|
-
// (relaySseWithFailedTail policy, Anthropic wire shape).
|
|
390
|
-
tapController?.enqueue(encoder.encode(`\n\nevent: error\ndata: ${payload}\n\n`));
|
|
391
|
-
tapController?.close();
|
|
392
|
-
} catch { /* client already torn down */ }
|
|
393
|
-
};
|
|
394
|
-
const failBody = (closeReason: "body_stall" | "body_overflow", errType: string, message: string) => {
|
|
395
|
-
if (settled) return;
|
|
396
|
-
settled = true;
|
|
397
|
-
idle.cancel();
|
|
398
|
-
detachAbort();
|
|
399
|
-
const terminalInTail = flushTail();
|
|
400
|
-
recordUsage();
|
|
401
|
-
if (terminalSeen) {
|
|
402
|
-
// The turn already ended (message_stop or an upstream error event): an upstream that then
|
|
403
|
-
// idles or keeps sending did not cut it short. Same rule as the read-error branch,
|
|
404
|
-
// including the restored blank line for a terminal found only in the tail.
|
|
405
|
-
finalize(200, { closeReason: "terminal" });
|
|
406
|
-
try {
|
|
407
|
-
if (terminalInTail) tapController?.enqueue(encoder.encode("\n\n"));
|
|
408
|
-
tapController?.close();
|
|
409
|
-
} catch { /* client already torn down */ }
|
|
410
|
-
} else {
|
|
411
|
-
// A cut-short turn, logged as the Responses relay logs a stall-timeout incomplete
|
|
412
|
-
// (httpStatusForRequestLogTerminal: only a max_output_tokens incomplete is a 200). A 200
|
|
413
|
-
// row with no terminalStatus also lost its failure diagnostics in usage.jsonl.
|
|
414
|
-
logCtx.upstreamError = message.slice(0, 500);
|
|
415
|
-
finalize(502, { terminalStatus: "incomplete", closeReason });
|
|
416
|
-
closeWithErrorFrame(errType, message);
|
|
417
|
-
}
|
|
418
|
-
reader.cancel(new DOMException(message, closeReason === "body_stall" ? "TimeoutError" : "QuotaExceededError")).catch(() => {});
|
|
419
|
-
};
|
|
420
|
-
const idle = idleDeadline(guard?.stallMs ?? 0, () => {
|
|
421
|
-
failBody(
|
|
422
|
-
"body_stall",
|
|
423
|
-
"timeout_error",
|
|
424
|
-
`anthropic passthrough body stalled: no upstream bytes for ${Math.round((guard?.stallMs ?? 0) / 1000)}s`,
|
|
425
|
-
);
|
|
426
|
-
});
|
|
427
|
-
// Deterministic client-cancel classification: Bun may surface a client abort as a
|
|
428
|
-
// reader.read() rejection OR a resolved done (src/lib/abort.ts cancelBodyOnAbort
|
|
429
|
-
// rationale), so the listener performs first-wins settlement itself instead of
|
|
430
|
-
// relying on which shape the read takes.
|
|
431
|
-
const onClientAbort = () => {
|
|
432
|
-
if (settled) return;
|
|
433
|
-
settled = true;
|
|
434
|
-
idle.cancel();
|
|
435
|
-
detachAbort();
|
|
436
|
-
finalize(499, { closeReason: "client_cancel" });
|
|
437
|
-
try { tapController?.close(); } catch { /* downstream already torn down */ }
|
|
438
|
-
reader.cancel(guard?.reqSignal?.reason).catch(() => {});
|
|
439
|
-
};
|
|
440
|
-
const detachAbort = (() => {
|
|
441
|
-
const signal = guard?.reqSignal;
|
|
442
|
-
if (!signal) return () => {};
|
|
443
|
-
if (signal.aborted) {
|
|
444
|
-
queueMicrotask(onClientAbort);
|
|
445
|
-
return () => {};
|
|
446
|
-
}
|
|
447
|
-
signal.addEventListener("abort", onClientAbort, { once: true });
|
|
448
|
-
return () => signal.removeEventListener("abort", onClientAbort);
|
|
449
|
-
})();
|
|
289
|
+
import {
|
|
290
|
+
anthropicUsageToOcx,
|
|
291
|
+
estimateClaudeRequestTokens,
|
|
292
|
+
resolvePassthroughBodyGuard,
|
|
293
|
+
sanitizePassthroughToolCallIds,
|
|
294
|
+
tapAnthropicSseForLog,
|
|
295
|
+
type PassthroughBodyGuard,
|
|
296
|
+
type PassthroughCloseReason,
|
|
297
|
+
} from "./messages-shared";
|
|
450
298
|
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
if (settled) return; // stall/overflow/abort won the race while we awaited
|
|
462
|
-
if (done) {
|
|
463
|
-
settled = true;
|
|
464
|
-
idle.cancel();
|
|
465
|
-
detachAbort();
|
|
466
|
-
recordUsage();
|
|
467
|
-
finalize(200, { closeReason: "terminal" });
|
|
468
|
-
controller.close();
|
|
469
|
-
return;
|
|
470
|
-
}
|
|
471
|
-
if (value.byteLength > 0) {
|
|
472
|
-
bodyBytes += value.byteLength;
|
|
473
|
-
if (guard && guard.maxBytes > 0 && bodyBytes > guard.maxBytes) {
|
|
474
|
-
failBody(
|
|
475
|
-
"body_overflow",
|
|
476
|
-
"api_error",
|
|
477
|
-
`anthropic passthrough body exceeded ${guard.maxBytes} bytes`,
|
|
478
|
-
);
|
|
479
|
-
return;
|
|
480
|
-
}
|
|
481
|
-
}
|
|
482
|
-
inspect(value);
|
|
483
|
-
controller.enqueue(value);
|
|
484
|
-
} catch (err) {
|
|
485
|
-
if (settled) return;
|
|
486
|
-
// Bun can settle a fetch body read before it dispatches the abort listeners
|
|
487
|
-
// (consumeForInspection in relay.ts): read the signal itself before calling this
|
|
488
|
-
// rejection an upstream failure.
|
|
489
|
-
if (guard?.reqSignal?.aborted) {
|
|
490
|
-
onClientAbort();
|
|
491
|
-
return;
|
|
492
|
-
}
|
|
493
|
-
settled = true;
|
|
494
|
-
idle.cancel();
|
|
495
|
-
detachAbort();
|
|
496
|
-
const terminalInTail = flushTail();
|
|
497
|
-
recordUsage();
|
|
498
|
-
if (isTranslatorBudgetExceededError(err)) {
|
|
499
|
-
// A local cap, not an upstream failure: the non-streaming native Messages fold
|
|
500
|
-
// maps this error to a 413 itself, so it still errors the stream.
|
|
501
|
-
finalize(200, { closeReason: "terminal" });
|
|
502
|
-
try { controller.error(err); } catch { /* torn down */ }
|
|
503
|
-
return;
|
|
504
|
-
}
|
|
505
|
-
if (terminalSeen) {
|
|
506
|
-
// Only the transport trailer was lost; the client already has the turn's terminal.
|
|
507
|
-
// The Responses relay likewise reports a read error only without a seen terminal.
|
|
508
|
-
finalize(200, { closeReason: "terminal" });
|
|
509
|
-
try {
|
|
510
|
-
// An SSE parser drops an event that EOF cuts off before its blank line, so restore
|
|
511
|
-
// the delimiter when the terminal was only found in that unterminated tail.
|
|
512
|
-
if (terminalInTail) controller.enqueue(encoder.encode("\n\n"));
|
|
513
|
-
controller.close();
|
|
514
|
-
} catch { /* torn down */ }
|
|
515
|
-
reader.cancel(err).catch(() => {});
|
|
516
|
-
return;
|
|
517
|
-
}
|
|
518
|
-
// The upstream read failed after the 200 went out (a mid-stream socket reset). Log it
|
|
519
|
-
// the way the Responses relay does (onReadError): a truncated body is a failed turn,
|
|
520
|
-
// not a completed one. The client gets the same Anthropic error terminal as a stall,
|
|
521
|
-
// instead of a connection reset — or, on some Bun releases, a bare EOF that reads as
|
|
522
|
-
// a finished message.
|
|
523
|
-
const message = redactSecretString(`anthropic passthrough upstream stream failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
524
|
-
logCtx.transportPhase = "mid_stream";
|
|
525
|
-
logCtx.terminalSource = "synthetic";
|
|
526
|
-
logCtx.upstreamError = message.slice(0, 500);
|
|
527
|
-
if (logCtx.activeAttempt) logCtx.activeAttempt.streamAborted = true;
|
|
528
|
-
finalize(502, { terminalStatus: "failed", closeReason: "terminal" });
|
|
529
|
-
closeWithErrorFrame("api_error", message);
|
|
530
|
-
reader.cancel(err).catch(() => {});
|
|
531
|
-
}
|
|
532
|
-
},
|
|
533
|
-
cancel(reason) {
|
|
534
|
-
if (!settled) {
|
|
535
|
-
settled = true;
|
|
536
|
-
idle.cancel();
|
|
537
|
-
detachAbort();
|
|
538
|
-
finalize(499, { closeReason: "client_cancel" });
|
|
539
|
-
}
|
|
540
|
-
reader.cancel(reason).catch(() => {});
|
|
541
|
-
},
|
|
542
|
-
});
|
|
543
|
-
}
|
|
299
|
+
export {
|
|
300
|
+
anthropicUsageToOcx,
|
|
301
|
+
estimateClaudeRequestTokens,
|
|
302
|
+
resolvePassthroughBodyGuard,
|
|
303
|
+
sanitizePassthroughToolCallIds,
|
|
304
|
+
tapAnthropicSseForLog,
|
|
305
|
+
type PassthroughBodyGuard,
|
|
306
|
+
type PassthroughCloseReason,
|
|
307
|
+
type PassthroughFinalizeMeta,
|
|
308
|
+
} from "./messages-shared";
|
|
544
309
|
|
|
545
|
-
/**
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
* adapters/tool-call-id.ts (#1780); this passthrough bypasses that adapter, so the same
|
|
552
|
-
* allocator runs here. Stateless per request: conforming ids pass through byte-identical
|
|
553
|
-
* (prompt-cache keys untouched), rewritten ids keep call/result pairing stable.
|
|
554
|
-
* An empty id has no representable wire form, and forwarding `""` is what Anthropic
|
|
555
|
-
* rejects (#1767), so the request fails locally with a 400 before any upstream fetch.
|
|
556
|
-
*/
|
|
557
|
-
export function sanitizePassthroughToolCallIds(messages: unknown[]): void {
|
|
558
|
-
const blocks: Rec[] = [];
|
|
559
|
-
for (const message of messages) {
|
|
560
|
-
if (!isRec(message) || !Array.isArray(message.content)) continue;
|
|
561
|
-
for (const block of message.content) if (isRec(block)) blocks.push(block);
|
|
562
|
-
}
|
|
563
|
-
const fieldOf = (block: Rec): "id" | "tool_use_id" | undefined => {
|
|
564
|
-
if (typeof block.type !== "string") return undefined;
|
|
565
|
-
if (block.type.endsWith("tool_use")) return "id";
|
|
566
|
-
if (block.type.endsWith("tool_result")) return "tool_use_id";
|
|
567
|
-
return undefined;
|
|
568
|
-
};
|
|
569
|
-
const callIds = createToolCallIdAllocator();
|
|
570
|
-
for (const block of blocks) {
|
|
571
|
-
const field = fieldOf(block);
|
|
572
|
-
if (field && typeof block[field] === "string") callIds.reserve(block[field] as string);
|
|
573
|
-
}
|
|
574
|
-
for (const block of blocks) {
|
|
575
|
-
const field = fieldOf(block);
|
|
576
|
-
if (!field || typeof block[field] !== "string") continue;
|
|
577
|
-
const wire = callIds.allocate(block[field] as string);
|
|
578
|
-
if (wire === undefined) throw new AnthropicRequestError(`${block.type} block has an empty ${field}`);
|
|
579
|
-
block[field] = wire;
|
|
580
|
-
}
|
|
581
|
-
}
|
|
310
|
+
/** Upstream error types a non-stream passthrough failure may report in history. */
|
|
311
|
+
const PASSTHROUGH_ERROR_TYPES = [
|
|
312
|
+
"invalid_request_error", "authentication_error", "permission_error", "not_found_error",
|
|
313
|
+
"rate_limit_error", "api_error", "overloaded_error", "request_too_large",
|
|
314
|
+
] as const;
|
|
315
|
+
const PASSTHROUGH_ERROR_DETAIL_MAX_CHARS = 64 * 1024;
|
|
582
316
|
|
|
583
317
|
async function anthropicNativePassthrough(
|
|
584
318
|
req: Request,
|
|
@@ -694,35 +428,6 @@ async function anthropicNativePassthrough(
|
|
|
694
428
|
});
|
|
695
429
|
}
|
|
696
430
|
|
|
697
|
-
const PASSTHROUGH_ERROR_TYPES = [
|
|
698
|
-
"invalid_request_error", "authentication_error", "permission_error", "not_found_error",
|
|
699
|
-
"rate_limit_error", "api_error", "overloaded_error", "request_too_large",
|
|
700
|
-
] as const;
|
|
701
|
-
const PASSTHROUGH_ERROR_DETAIL_MAX_CHARS = 64 * 1024;
|
|
702
|
-
const DEFAULT_BODY_STALL_SEC = 90;
|
|
703
|
-
const DEFAULT_BODY_MAX_BYTES = 64 * 1024 * 1024;
|
|
704
|
-
|
|
705
|
-
/**
|
|
706
|
-
* Normalize the claudeCode body-guard config (devlog 260716_passthrough_followups/010).
|
|
707
|
-
* Policy: exactly 0 disables; finite positive values are honored (stall clamped to
|
|
708
|
-
* min 1s); negative/non-finite/absent values fall back to the defaults.
|
|
709
|
-
*/
|
|
710
|
-
export function resolvePassthroughBodyGuard(config: OcxConfig, reqSignal?: AbortSignal): PassthroughBodyGuard {
|
|
711
|
-
const rawSec = config.claudeCode?.bodyStallSec;
|
|
712
|
-
const stallSec = rawSec === 0
|
|
713
|
-
? 0
|
|
714
|
-
: typeof rawSec === "number" && Number.isFinite(rawSec) && rawSec > 0
|
|
715
|
-
? Math.max(1, rawSec)
|
|
716
|
-
: DEFAULT_BODY_STALL_SEC;
|
|
717
|
-
const rawBytes = config.claudeCode?.bodyMaxBytes;
|
|
718
|
-
const maxBytes = rawBytes === 0
|
|
719
|
-
? 0
|
|
720
|
-
: typeof rawBytes === "number" && Number.isFinite(rawBytes) && rawBytes > 0
|
|
721
|
-
? Math.floor(rawBytes)
|
|
722
|
-
: DEFAULT_BODY_MAX_BYTES;
|
|
723
|
-
return { stallMs: stallSec * 1000, maxBytes, ...(reqSignal ? { reqSignal } : {}) };
|
|
724
|
-
}
|
|
725
|
-
|
|
726
431
|
type BoundedPassthroughBody =
|
|
727
432
|
| { kind: "ok"; text: string }
|
|
728
433
|
| { kind: "stall" }
|
|
@@ -850,6 +555,21 @@ export async function handleClaudeMessages(
|
|
|
850
555
|
}
|
|
851
556
|
}
|
|
852
557
|
|
|
558
|
+
/** The client answer for a Messages body the translation (or the direct decode) refused. */
|
|
559
|
+
function translationFailure(err: unknown, logCtx: RequestLogContext, logIds: { requestId: string; start: number } | undefined): Response {
|
|
560
|
+
const overflow = isTranslatorBudgetExceededError(err);
|
|
561
|
+
const unavailable = err instanceof DesktopModelMappingUnavailableError;
|
|
562
|
+
const status = overflow ? 413 : unavailable ? 503 : err instanceof AnthropicRequestError ? 400 : 500;
|
|
563
|
+
if (logIds) addFinalRequestLog(logIds.requestId, logIds.start, logCtx, status, { closeReason: "non_stream" });
|
|
564
|
+
if (unavailable) return desktopMappingUnavailableResponse(err);
|
|
565
|
+
return anthropicErrorResponse(
|
|
566
|
+
status,
|
|
567
|
+
overflow ? "request translation buffer exceeded the safe limit" : err instanceof Error ? err.message : String(err),
|
|
568
|
+
overflow ? "request_too_large" : undefined,
|
|
569
|
+
overflow ? "translation_buffer_limit" : undefined,
|
|
570
|
+
);
|
|
571
|
+
}
|
|
572
|
+
|
|
853
573
|
/**
|
|
854
574
|
* Translate a Claude Messages request, route it through the Responses pipeline,
|
|
855
575
|
* and translate the reply back. Runs under a translator budget owned by the
|
|
@@ -885,6 +605,11 @@ async function handleClaudeMessagesWithBudget(
|
|
|
885
605
|
let effortRow: ParsedEffortRowId | null = null;
|
|
886
606
|
let fastRow: ParsedFastRowId | null = null;
|
|
887
607
|
let requestedModel = "";
|
|
608
|
+
// Antigravity direct lane (off by default): planned before translation, kept only if routing
|
|
609
|
+
// settles on the route the plan was made for.
|
|
610
|
+
let directPlan: MessagesDirectPlan | undefined;
|
|
611
|
+
/** The retained charge for the settings-only body, handed back if the plan is left. */
|
|
612
|
+
let directSettingsBodyBytes = 0;
|
|
888
613
|
// Built only under the reject policy; the legacy default leaves this request untouched.
|
|
889
614
|
let envelope: ProtocolEnvelope | undefined;
|
|
890
615
|
let messagesFeatures: () => Iterable<ProtocolFeature> = () => [];
|
|
@@ -1034,9 +759,16 @@ async function handleClaudeMessagesWithBudget(
|
|
|
1034
759
|
};
|
|
1035
760
|
delete anthropicBody.thinking;
|
|
1036
761
|
}
|
|
1037
|
-
|
|
762
|
+
// On the direct lane the core receives only the request settings; the conversation is
|
|
763
|
+
// already decoded into the IR. Without a plan the translation below runs as before.
|
|
764
|
+
directPlan = planMessagesAntigravityDirect(anthropicBody, cc, config, translatorBudget);
|
|
765
|
+
let translation = directPlan && {
|
|
766
|
+
body: directPlan.settingsBody,
|
|
767
|
+
cacheKeySource: directPlan.cacheKeySource,
|
|
768
|
+
nativeReasoningReplay: directPlan.conversation.nativeReasoningReplay,
|
|
769
|
+
};
|
|
1038
770
|
try {
|
|
1039
|
-
translation
|
|
771
|
+
translation ??= messagesToResponsesTranslation(anthropicBody, cc, translatorBudget);
|
|
1040
772
|
} catch (err) {
|
|
1041
773
|
// A native Messages route can discard an undecodable proxy envelope. Keep the
|
|
1042
774
|
// original error for any route that ultimately translates to Responses.
|
|
@@ -1052,21 +784,13 @@ async function handleClaudeMessagesWithBudget(
|
|
|
1052
784
|
// fields only, so the caller intent is applied to the TRANSLATED body rather than the
|
|
1053
785
|
// inbound one.
|
|
1054
786
|
if (fastRow) internalBody.service_tier = "priority";
|
|
1055
|
-
|
|
787
|
+
const internalBodyBytes = jsonUtf8Bytes(internalBody);
|
|
788
|
+
translatorBudget.chargeRetained(internalBodyBytes, { kind: "request_copies" });
|
|
789
|
+
if (directPlan) directSettingsBodyBytes = internalBodyBytes;
|
|
1056
790
|
cacheKeySource = translation.cacheKeySource;
|
|
1057
791
|
nativeReasoningReplay = translation.nativeReasoningReplay;
|
|
1058
792
|
} catch (err) {
|
|
1059
|
-
|
|
1060
|
-
const unavailable = err instanceof DesktopModelMappingUnavailableError;
|
|
1061
|
-
const status = overflow ? 413 : unavailable ? 503 : err instanceof AnthropicRequestError ? 400 : 500;
|
|
1062
|
-
if (logIds) addFinalRequestLog(logIds.requestId, logIds.start, logCtx, status, { closeReason: "non_stream" });
|
|
1063
|
-
if (unavailable) return desktopMappingUnavailableResponse(err);
|
|
1064
|
-
return anthropicErrorResponse(
|
|
1065
|
-
status,
|
|
1066
|
-
overflow ? "request translation buffer exceeded the safe limit" : err instanceof Error ? err.message : String(err),
|
|
1067
|
-
overflow ? "request_too_large" : undefined,
|
|
1068
|
-
overflow ? "translation_buffer_limit" : undefined,
|
|
1069
|
-
);
|
|
793
|
+
return translationFailure(err, logCtx, logIds);
|
|
1070
794
|
}
|
|
1071
795
|
|
|
1072
796
|
if (!requestedModel) requestedModel = (anthropicBody as Rec).model as string;
|
|
@@ -1098,8 +822,9 @@ async function handleClaudeMessagesWithBudget(
|
|
|
1098
822
|
|
|
1099
823
|
let clientEncoder: ClientEncoderOption | undefined;
|
|
1100
824
|
// Native ChatGPT passthrough (openai-responses forward) accepts only Codex-shaped
|
|
1101
|
-
// bodies: it 400s on sampling params (
|
|
1102
|
-
//
|
|
825
|
+
// bodies: it 400s on sampling params (verified live 2026-07-11). Every openai-responses
|
|
826
|
+
// route drops temperature/top_p/stop/user below; max_output_tokens is left to
|
|
827
|
+
// stripUnsupportedForwardParams (canonical-forward.ts), which drops it only for that backend.
|
|
1103
828
|
let settledRoute: RouteResult | undefined;
|
|
1104
829
|
/**
|
|
1105
830
|
* Which replayed thinking fields the send that actually happened will serialize.
|
|
@@ -1119,8 +844,30 @@ async function handleClaudeMessagesWithBudget(
|
|
|
1119
844
|
}
|
|
1120
845
|
return requestTokenFloor;
|
|
1121
846
|
};
|
|
847
|
+
/** Leave the direct plan: the core gets the full bridge translation, as without the switch. */
|
|
848
|
+
const leaveDirectPlan = (): unknown => {
|
|
849
|
+
// Hand back everything the plan retained: its decode and the settings-only body it replaces.
|
|
850
|
+
directPlan?.releaseDecodeCharges();
|
|
851
|
+
directPlan = undefined;
|
|
852
|
+
nativeReasoningReplay = undefined;
|
|
853
|
+
translatorBudget.releaseRetained(directSettingsBodyBytes, { kind: "request_copies" });
|
|
854
|
+
directSettingsBodyBytes = 0;
|
|
855
|
+
try {
|
|
856
|
+
const translation = messagesToResponsesTranslation(anthropicBody, cc, translatorBudget);
|
|
857
|
+
internalBody = translation.body;
|
|
858
|
+
if (fastRow) internalBody.service_tier = "priority";
|
|
859
|
+
internalBody.stream = true;
|
|
860
|
+
translatorBudget.chargeRetained(jsonUtf8Bytes(internalBody), { kind: "request_copies" });
|
|
861
|
+
cacheKeySource = translation.cacheKeySource;
|
|
862
|
+
nativeReasoningReplay = translation.nativeReasoningReplay;
|
|
863
|
+
return undefined;
|
|
864
|
+
} catch (err) {
|
|
865
|
+
return err;
|
|
866
|
+
}
|
|
867
|
+
};
|
|
868
|
+
let directLeaveError: unknown;
|
|
1122
869
|
try {
|
|
1123
|
-
const route = routeModel(config, internalBody.model as string, evidenceFromBody(internalBody));
|
|
870
|
+
const route = routeModel(config, internalBody.model as string, directPlan ? directPlan.evidence : evidenceFromBody(internalBody));
|
|
1124
871
|
// Settle the wire once so the sampling decision below reads the effective
|
|
1125
872
|
// adapter rather than the provider-wide default (#404).
|
|
1126
873
|
route.staticPolicy = captureRouteStaticPolicy(
|
|
@@ -1138,11 +885,14 @@ async function handleClaudeMessagesWithBudget(
|
|
|
1138
885
|
route.provider = resolveWireProtocolOverride(route.providerName, route.modelId, route.provider, "anthropic", route.staticPolicy);
|
|
1139
886
|
logCtx.routeDecision = route.routeDecision;
|
|
1140
887
|
settledRoute = route;
|
|
888
|
+
// Before any route-specific shaping, so a request that leaves the lane is shaped as the bridge's.
|
|
889
|
+
if (directPlan && !messagesDirectPlanHolds(directPlan, route, anthropicBody as Rec, config)) directLeaveError = leaveDirectPlan();
|
|
1141
890
|
if (directEncodersApply(config, route)) {
|
|
1142
891
|
clientEncoder = { protocol: "messages", stream, model: requestedModel, inputTokenFloor: claudeRequestTokenFloor() };
|
|
1143
892
|
}
|
|
1144
893
|
if (route.provider.adapter === "openai-responses") {
|
|
1145
|
-
|
|
894
|
+
// max_output_tokens is the caller's spend cap: the adapter strips it only for the canonical
|
|
895
|
+
// ChatGPT backend that rejects it (#5753), so other Responses gateways keep the cap.
|
|
1146
896
|
delete internalBody.temperature;
|
|
1147
897
|
delete internalBody.top_p;
|
|
1148
898
|
delete internalBody.stop;
|
|
@@ -1183,6 +933,14 @@ async function handleClaudeMessagesWithBudget(
|
|
|
1183
933
|
}
|
|
1184
934
|
/* unknown model: let handleResponses shape the 404 */
|
|
1185
935
|
}
|
|
936
|
+
if (directPlan && !settledRoute) directLeaveError ??= leaveDirectPlan();
|
|
937
|
+
if (directLeaveError !== undefined) return translationFailure(directLeaveError, logCtx, logIds);
|
|
938
|
+
const directRoute = directPlan && settledRoute ? settledRoute : undefined;
|
|
939
|
+
// The direct lane answers through the direct encoder whatever `directEncoders` says: neither
|
|
940
|
+
// direction passes through the internal Responses body.
|
|
941
|
+
if (directRoute) {
|
|
942
|
+
clientEncoder = { protocol: "messages", stream, model: requestedModel, inputTokenFloor: claudeRequestTokenFloor(), requestLane: "direct" };
|
|
943
|
+
}
|
|
1186
944
|
|
|
1187
945
|
// PF-08: a managed-key Anthropic route sends its Messages body natively. The caller-forward
|
|
1188
946
|
// passthrough above was decided on the caller's own credential and never reaches this point.
|
|
@@ -1206,11 +964,20 @@ async function handleClaudeMessagesWithBudget(
|
|
|
1206
964
|
return anthropicErrorResponse(400, nativeProjectionOnlyError.message, "invalid_request_error");
|
|
1207
965
|
}
|
|
1208
966
|
// With the switch on, a declined route says why on its bridge mark, as native Chat does.
|
|
1209
|
-
|
|
967
|
+
const antigravityDecline = settledRoute && !nativeMessagesRoute && !directRoute
|
|
968
|
+
? messagesDirectTraceDecline(settledRoute, anthropicBody, String(internalBody.model ?? ""), config)
|
|
969
|
+
: undefined;
|
|
970
|
+
if (directRoute) {
|
|
971
|
+
markProtocolEntry(logCtx, { inbound: "messages", lane: "direct", features: messagesFeatures });
|
|
972
|
+
} else if ((nativeDecline && nativeDecline !== "rollout-disabled") || antigravityDecline) {
|
|
1210
973
|
markProtocolEntry(logCtx, {
|
|
1211
974
|
inbound: "messages",
|
|
1212
975
|
lane: "bridge",
|
|
1213
|
-
reasonCodes: [
|
|
976
|
+
reasonCodes: [
|
|
977
|
+
...(effortRow ? ["effort-row" as const] : fastRow ? ["fast-row" as const] : []),
|
|
978
|
+
...(nativeDecline && nativeDecline !== "rollout-disabled" ? [nativeDecline] : []),
|
|
979
|
+
...(antigravityDecline ? [antigravityDecline] : []),
|
|
980
|
+
],
|
|
1214
981
|
features: messagesFeatures,
|
|
1215
982
|
});
|
|
1216
983
|
}
|
|
@@ -1218,7 +985,11 @@ async function handleClaudeMessagesWithBudget(
|
|
|
1218
985
|
if (envelope && settledRoute && !settledRoute.combo && settledRoute.routeKind !== "policy") {
|
|
1219
986
|
const verdict = checkRepresentable({
|
|
1220
987
|
inbound: "messages",
|
|
1221
|
-
requestPath: requestPathForLane(
|
|
988
|
+
requestPath: requestPathForLane(
|
|
989
|
+
"messages",
|
|
990
|
+
nativeMessagesRoute ? "native" : directRoute ? "direct" : "bridge",
|
|
991
|
+
upstreamWireForAdapter(settledRoute.provider.adapter),
|
|
992
|
+
),
|
|
1222
993
|
features: envelope.features(),
|
|
1223
994
|
policy: "reject",
|
|
1224
995
|
});
|
|
@@ -1337,7 +1108,11 @@ async function handleClaudeMessagesWithBudget(
|
|
|
1337
1108
|
// BEFORE translation (the translated Anthropic stream has no response.completed
|
|
1338
1109
|
// frame, so tapping it records a bogus 502 with no usage/cache detail).
|
|
1339
1110
|
const finalizeNativeLog = createFinalRequestLog(logIds, logCtx).finish;
|
|
1340
|
-
const upstream = await handleResponses(internalReq,
|
|
1111
|
+
const upstream = await handleResponses(internalReq, {
|
|
1112
|
+
...buildClaudeReplayConfig(config),
|
|
1113
|
+
// Operator config wins; otherwise the caller's own 1h breakpoints keep 1h here.
|
|
1114
|
+
cacheRetention: config.cacheRetention ?? callerCacheRetention(anthropicBody),
|
|
1115
|
+
}, logCtx, {
|
|
1341
1116
|
// Routing keeps Claude-only sidecar overrides; admission policy must follow the live owner.
|
|
1342
1117
|
codexAuthPolicy: config,
|
|
1343
1118
|
...(logIds?.admission ? { admission: logIds.admission } : {}),
|
|
@@ -1348,8 +1123,16 @@ async function handleClaudeMessagesWithBudget(
|
|
|
1348
1123
|
// Without this the replay would look native and a Responses-scoped wire default
|
|
1349
1124
|
// would fire, disagreeing with the pre-flight decision above.
|
|
1350
1125
|
inboundWire: "anthropic",
|
|
1126
|
+
// An explicit empty `tools` array is a deny-all declaration the translation drops with
|
|
1127
|
+
// the rest of the catalog; carried separately so the undeclared-tool guard can still
|
|
1128
|
+
// enforce it while partial and absent catalogs stay deferred.
|
|
1129
|
+
clientDeclaredEmptyToolCatalog: isRec(anthropicBody)
|
|
1130
|
+
&& Array.isArray(anthropicBody.tools)
|
|
1131
|
+
&& anthropicBody.tools.length === 0,
|
|
1351
1132
|
nativeReasoningReplay,
|
|
1352
1133
|
nativeReasoningMint,
|
|
1134
|
+
// Out of body like the replay above: the core hands these to an Anthropic destination only.
|
|
1135
|
+
anthropicServerTools: declaredAnthropicServerTools(anthropicBody),
|
|
1353
1136
|
claudeGoAffinity: { sessionLane: claudeGoSessionLane },
|
|
1354
1137
|
claudeNativeSessionId,
|
|
1355
1138
|
stripClaudeMainAuthForNoncanonicalForward: true,
|
|
@@ -1362,6 +1145,9 @@ async function handleClaudeMessagesWithBudget(
|
|
|
1362
1145
|
onNativePassthroughTerminal: status => finalizeNativeLog(httpStatusForRequestLogTerminal(status, logCtx), { terminalStatus: status, closeReason: "terminal" }),
|
|
1363
1146
|
onNativePassthroughCancel: () => finalizeNativeLog(499, { closeReason: "client_cancel" }),
|
|
1364
1147
|
...(clientEncoder ? { clientEncoder } : {}),
|
|
1148
|
+
...(directRoute && directPlan
|
|
1149
|
+
? { directRequest: messagesDirectRequestOption(directPlan, anthropicBody, cc, translatorBudget) }
|
|
1150
|
+
: {}),
|
|
1365
1151
|
});
|
|
1366
1152
|
const response = logIds ? responseWithDeferredRequestLog(upstream, logIds.requestId, logIds.start, logCtx) : upstream;
|
|
1367
1153
|
// Already in the Messages wire (direct encoder): no conversion.
|
|
@@ -1374,11 +1160,13 @@ async function handleClaudeMessagesWithBudget(
|
|
|
1374
1160
|
// Re-shape the OpenAI-style error envelope into the Anthropic one, preserving status.
|
|
1375
1161
|
let message = `upstream error (${response.status})`;
|
|
1376
1162
|
let contextError = false;
|
|
1163
|
+
let catalogRefusal = false;
|
|
1377
1164
|
try {
|
|
1378
1165
|
const text = await response.text();
|
|
1379
1166
|
try {
|
|
1380
1167
|
const parsed = JSON.parse(text) as { error?: { message?: string; type?: string; code?: unknown } | string; message?: string };
|
|
1381
1168
|
contextError = typeof parsed?.error === "object" && parsed.error?.code === "context_length_exceeded";
|
|
1169
|
+
catalogRefusal = typeof parsed?.error === "object" && parsed.error?.code === UNDECLARED_TOOL_CALL_ERROR_CODE;
|
|
1382
1170
|
const nested = typeof parsed?.error === "object" && parsed.error ? parsed.error.message : undefined;
|
|
1383
1171
|
const flat = typeof parsed?.error === "string" ? parsed.error : parsed?.message;
|
|
1384
1172
|
message = nested || flat || (text ? `upstream error (${response.status}): ${text.slice(0, 400)}` : message);
|
|
@@ -1407,7 +1195,10 @@ async function handleClaudeMessagesWithBudget(
|
|
|
1407
1195
|
const nativeMainFence = response.status === 503
|
|
1408
1196
|
&& upstreamRetryAfter?.trim() === "1"
|
|
1409
1197
|
&& message === CODEX_MAIN_PROFILE_MAINTENANCE_MESSAGE;
|
|
1410
|
-
|
|
1198
|
+
// The undeclared-tool guard's refusal is deterministic: the same request produces the
|
|
1199
|
+
// same verdict, so it must not arrive as a retryable overload like a transient 5xx.
|
|
1200
|
+
const transient = !replayRefusal && !nativeMainFence && !contextError && !catalogRefusal
|
|
1201
|
+
&& isTransientUpstreamStatus(response.status);
|
|
1411
1202
|
const outStatus = replayRefusal
|
|
1412
1203
|
? REPLAY_REFUSED_STATUS
|
|
1413
1204
|
: nativeMainFence ? 503 : contextError ? 400 : transient ? 529 : response.status;
|
|
@@ -1419,7 +1210,11 @@ async function handleClaudeMessagesWithBudget(
|
|
|
1419
1210
|
outStatus,
|
|
1420
1211
|
message,
|
|
1421
1212
|
undefined,
|
|
1422
|
-
replayRefusal
|
|
1213
|
+
replayRefusal
|
|
1214
|
+
? UPSTREAM_RESET_REPLAY_REFUSED_CODE
|
|
1215
|
+
: contextError
|
|
1216
|
+
? "context_length_exceeded"
|
|
1217
|
+
: catalogRefusal ? UNDECLARED_TOOL_CALL_ERROR_CODE : undefined,
|
|
1423
1218
|
)), {
|
|
1424
1219
|
status: outStatus,
|
|
1425
1220
|
headers: outHeaders,
|
|
@@ -1470,8 +1265,17 @@ async function handleClaudeMessagesWithBudget(
|
|
|
1470
1265
|
"translation_buffer_limit",
|
|
1471
1266
|
);
|
|
1472
1267
|
}
|
|
1268
|
+
// A declared client refusal keeps its 4xx: as a 502 Claude Code retries a verdict that cannot
|
|
1269
|
+
// change. Server classes and rate limits keep the 502 this path has always answered. A 400
|
|
1270
|
+
// worded as a client close answers the 499 httpStatusFromTerminalError logs for it.
|
|
1271
|
+
const refusalStatus = recognizedUpstreamError(translatedError ?? {})?.httpStatus;
|
|
1473
1272
|
return new Response(JSON.stringify(message), {
|
|
1474
|
-
status: isError
|
|
1273
|
+
status: isError
|
|
1274
|
+
? translatedError?.code === "context_length_exceeded" ? 400
|
|
1275
|
+
: refusalStatus !== undefined && refusalStatus < 429
|
|
1276
|
+
? refusalStatus === 400 && isClientClosedMessage(String(translatedError?.message ?? "")) ? 499 : refusalStatus
|
|
1277
|
+
: 502
|
|
1278
|
+
: 200,
|
|
1475
1279
|
headers: { "Content-Type": "application/json" },
|
|
1476
1280
|
});
|
|
1477
1281
|
}
|
|
@@ -1533,79 +1337,6 @@ async function handleClaudeMessagesWithBudget(
|
|
|
1533
1337
|
});
|
|
1534
1338
|
}
|
|
1535
1339
|
|
|
1536
|
-
/** Per-attachment token estimate for a base64 payload: real image dimensions when the
|
|
1537
|
-
* header is sniffable (Anthropic prices images at ~pixels/750), else decoded bytes/512,
|
|
1538
|
-
* min 256 — the same shape as the Kiro usage estimator (estimateKiroImageTokens). */
|
|
1539
|
-
function estimateBase64AttachmentTokens(data: string): number {
|
|
1540
|
-
const dims = sniffImageDimensions(data);
|
|
1541
|
-
if (dims) return Math.max(256, Math.ceil((dims.width * dims.height) / 750));
|
|
1542
|
-
const unpadded = data.endsWith("==") ? data.length - 2 : data.endsWith("=") ? data.length - 1 : data.length;
|
|
1543
|
-
return Math.max(256, Math.ceil(Math.floor((unpadded * 3) / 4) / 512));
|
|
1544
|
-
}
|
|
1545
|
-
|
|
1546
|
-
/**
|
|
1547
|
-
* Char-based token estimate for an Anthropic-shaped request body. Base64 attachment
|
|
1548
|
-
* payloads (image/document blocks in message content, including blocks nested in
|
|
1549
|
-
* tool_result.content) are counted as a bounded per-attachment estimate instead of raw
|
|
1550
|
-
* characters: one 2MB screenshot is ~2.7M base64 chars, which the plain chars/token
|
|
1551
|
-
* divide reports as hundreds of thousands of tokens versus a real cost around 1.6k.
|
|
1552
|
-
* That breaks the >2x drift bound the estimator is held to (devlog 260711_claude_inbound
|
|
1553
|
-
* 040 §3); a live 260-message turn whose replayed thinking was 78.8% of the body breached
|
|
1554
|
-
* it at 3.28x, which is why the estimate is projected onto the settled route. Text and url
|
|
1555
|
-
* sources are left in place and counted as characters, as is
|
|
1556
|
-
* anything outside protocol content positions (tool_use.input, tool schemas).
|
|
1557
|
-
*
|
|
1558
|
-
* `thinking` selects which replayed thinking fields the SETTLED route serializes, so the measure
|
|
1559
|
-
* describes the prompt this proxy forwards rather than the one the caller typed. Omitted, the
|
|
1560
|
-
* whole body counts — correct for the Anthropic-native wire, where nothing is projected away.
|
|
1561
|
-
* See `claude-request-projection.ts` for why a routed wire must project it out.
|
|
1562
|
-
*/
|
|
1563
|
-
export function estimateClaudeRequestTokens(
|
|
1564
|
-
raw: { system?: unknown; messages?: unknown; tools?: unknown },
|
|
1565
|
-
modelId: string | undefined,
|
|
1566
|
-
thinking: ClaudeThinkingProjection = CLAUDE_NATIVE_THINKING,
|
|
1567
|
-
): number {
|
|
1568
|
-
let attachmentTokens = 0;
|
|
1569
|
-
// Blank base64 payloads ONLY in protocol content positions: message content blocks and
|
|
1570
|
-
// blocks nested in tool_result.content. tool_use.input and tool schemas can legitimately
|
|
1571
|
-
// contain attachment-shaped JSON, and those bytes ARE serialized into function_call
|
|
1572
|
-
// arguments / tool definitions for routed providers, so they must keep counting as text.
|
|
1573
|
-
// system is text-only per the Anthropic protocol (no attachment sources), so it is
|
|
1574
|
-
// stringified as-is.
|
|
1575
|
-
const sanitizeBlock = (block: unknown): unknown => {
|
|
1576
|
-
if (!block || typeof block !== "object") return block;
|
|
1577
|
-
const b = block as Record<string, unknown>;
|
|
1578
|
-
if (b.type === "image" || b.type === "document") {
|
|
1579
|
-
const source = b.source as { type?: unknown; data?: unknown } | undefined;
|
|
1580
|
-
if (source && typeof source === "object" && source.type === "base64" && typeof source.data === "string") {
|
|
1581
|
-
attachmentTokens += estimateBase64AttachmentTokens(source.data);
|
|
1582
|
-
return { ...b, source: { ...(source as Record<string, unknown>), data: "" } };
|
|
1583
|
-
}
|
|
1584
|
-
return block;
|
|
1585
|
-
}
|
|
1586
|
-
if (b.type === "tool_result" && Array.isArray(b.content)) {
|
|
1587
|
-
return { ...b, content: (b.content as unknown[]).map(sanitizeBlock) };
|
|
1588
|
-
}
|
|
1589
|
-
return block;
|
|
1590
|
-
};
|
|
1591
|
-
const sanitizedMessages = (messages: unknown): unknown =>
|
|
1592
|
-
Array.isArray(messages)
|
|
1593
|
-
? messages.map(message => {
|
|
1594
|
-
if (!message || typeof message !== "object") return message;
|
|
1595
|
-
const m = message as Record<string, unknown>;
|
|
1596
|
-
return Array.isArray(m.content) ? { ...m, content: (m.content as unknown[]).map(sanitizeBlock) } : message;
|
|
1597
|
-
})
|
|
1598
|
-
: messages;
|
|
1599
|
-
const parts: string[] = [];
|
|
1600
|
-
if (raw.system !== undefined) parts.push(typeof raw.system === "string" ? raw.system : JSON.stringify(raw.system));
|
|
1601
|
-
if (raw.messages !== undefined) {
|
|
1602
|
-
const projected = projectClaudeRequest(raw, thinking);
|
|
1603
|
-
parts.push(JSON.stringify(sanitizedMessages(projected.messages)));
|
|
1604
|
-
}
|
|
1605
|
-
if (raw.tools !== undefined) parts.push(JSON.stringify(raw.tools));
|
|
1606
|
-
return Math.max(1, estimateTokens(parts.join("\n"), modelId) + attachmentTokens);
|
|
1607
|
-
}
|
|
1608
|
-
|
|
1609
1340
|
/**
|
|
1610
1341
|
* The projection for a route that has already settled.
|
|
1611
1342
|
*
|