@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
|
@@ -15,10 +15,8 @@ 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
22
|
import {
|
|
@@ -28,13 +26,14 @@ import {
|
|
|
28
26
|
planMessagesAntigravityDirect,
|
|
29
27
|
type MessagesDirectPlan,
|
|
30
28
|
} from "./messages-antigravity-direct";
|
|
31
|
-
import { AnthropicRequestError, DesktopModelMappingUnavailableError, extractOcxEffortDirective, extractOcxRouteDirective, nativeAnthropicProjection, resolveInboundModel, type ClaudeCacheKeySource } from "../claude/inbound";
|
|
29
|
+
import { AnthropicRequestError, callerCacheRetention, DesktopModelMappingUnavailableError, extractOcxEffortDirective, extractOcxRouteDirective, nativeAnthropicProjection, resolveInboundModel, type ClaudeCacheKeySource } from "../claude/inbound";
|
|
32
30
|
import { nativeReasoningTag, type NativeReasoningOwner } from "../responses/reasoning-replay-cache";
|
|
33
31
|
import { isKnownDesktop3pModelId, resolveDesktop3pAlias } from "../claude/desktop-3p";
|
|
34
32
|
import { resolveAlias, claudeCodeNativeAlias, legacyAliasForNative } from "../claude/alias";
|
|
35
33
|
import { recordDesktopRequest } from "../claude/desktop-health";
|
|
36
34
|
import { stripOneMillionMarker } from "../claude/context-windows";
|
|
37
35
|
import { captureClaudeInbound } from "../claude/inbound-debug";
|
|
36
|
+
import { declaredAnthropicServerTools, replayedAnthropicServerBlocks } from "../claude/inbound-content-options";
|
|
38
37
|
import { claudeCodeForIngress } from "../claude/intercept/model-bindings";
|
|
39
38
|
import { classifyInterceptClient } from "../claude/intercept/client-class";
|
|
40
39
|
import { analyzeClaudeCompatibility, isClaudeCompatibilityMode } from "../claude/compatibility";
|
|
@@ -47,7 +46,7 @@ import {
|
|
|
47
46
|
REPLAY_REFUSED_STATUS,
|
|
48
47
|
UPSTREAM_RESET_REPLAY_REFUSED_CODE,
|
|
49
48
|
} from "../lib/upstream-retry";
|
|
50
|
-
import { UNDECLARED_TOOL_CALL_ERROR_CODE } from "../lib/errors";
|
|
49
|
+
import { isClientClosedMessage, recognizedUpstreamError, UNDECLARED_TOOL_CALL_ERROR_CODE } from "../lib/errors";
|
|
51
50
|
import { resolveClientRetryAfter } from "../lib/retry-after";
|
|
52
51
|
import { anthropicRateLimitHeaders } from "./anthropic-rate-limit-headers";
|
|
53
52
|
import { upstreamMessagesRequestIdHeaders } from "./messages-response-headers";
|
|
@@ -59,10 +58,8 @@ import {
|
|
|
59
58
|
responsesSseToAnthropicSse,
|
|
60
59
|
} from "../claude/outbound";
|
|
61
60
|
import { clearableDeadline, idleDeadline } from "../lib/abort";
|
|
62
|
-
import { estimateTokens } from "../lib/token-estimate";
|
|
63
61
|
import {
|
|
64
62
|
CLAUDE_NATIVE_THINKING,
|
|
65
|
-
projectClaudeRequest,
|
|
66
63
|
type ClaudeThinkingProjection,
|
|
67
64
|
} from "../lib/claude-request-projection";
|
|
68
65
|
import { captureRouteStaticPolicy, NoEligiblePolicyCandidateError, previewRouteModel, routedProviderConfig, UnknownRoutingPolicyError, routeModel, type RouteResult } from "../router";
|
|
@@ -71,7 +68,6 @@ import { resolveWireProtocolOverride } from "./adapter-resolve";
|
|
|
71
68
|
import type { OcxConfig } from "../types";
|
|
72
69
|
import { readJsonRequestBody, resolveInboundBodyLimitBytes } from "./request-decompress";
|
|
73
70
|
import { addFinalRequestLog, httpStatusForRequestLogTerminal, recordFirstOutput, type RequestLogContext } from "./request-log";
|
|
74
|
-
import { recordGenerationEvent } from "./request-log-generation-window";
|
|
75
71
|
import { createFinalRequestLog } from "./inference/final-log";
|
|
76
72
|
import {
|
|
77
73
|
conversationIdFromClaudeMetadata,
|
|
@@ -284,309 +280,39 @@ function uuidFromHex(hex32: string): string {
|
|
|
284
280
|
return `${h.slice(0, 8)}-${h.slice(8, 12)}-4${h.slice(13, 16)}-8${h.slice(17, 20)}-${h.slice(20, 32)}`;
|
|
285
281
|
}
|
|
286
282
|
|
|
287
|
-
export function anthropicUsageToOcx(usage: Rec | undefined): { inputTokens: number; outputTokens: number; cachedInputTokens?: number; cacheReadInputTokens?: number; cacheCreationInputTokens?: number } | undefined {
|
|
288
|
-
if (!usage) return undefined;
|
|
289
|
-
const num = (v: unknown) => typeof v === "number" ? v : 0;
|
|
290
|
-
const hasCache = usage.cache_read_input_tokens !== undefined || usage.cache_creation_input_tokens !== undefined;
|
|
291
|
-
const read = num(usage.cache_read_input_tokens);
|
|
292
|
-
const write = num(usage.cache_creation_input_tokens);
|
|
293
|
-
// Anthropic input_tokens excludes cache read/write; normalize to the canonical
|
|
294
|
-
// inclusive convention (types.ts OcxUsage / devlog 070). cached = READS only.
|
|
295
|
-
return {
|
|
296
|
-
inputTokens: num(usage.input_tokens) + read + write,
|
|
297
|
-
outputTokens: num(usage.output_tokens),
|
|
298
|
-
...(hasCache ? {
|
|
299
|
-
cachedInputTokens: read,
|
|
300
|
-
cacheReadInputTokens: read,
|
|
301
|
-
cacheCreationInputTokens: write,
|
|
302
|
-
} : {}),
|
|
303
|
-
};
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
/** Body-occupancy guard for the native passthrough (devlog 260716_passthrough_followups/010). */
|
|
307
|
-
export interface PassthroughBodyGuard {
|
|
308
|
-
/** Idle window in ms — raw upstream-byte inactivity while a read is pending. 0 disables. */
|
|
309
|
-
stallMs: number;
|
|
310
|
-
/** Cumulative body byte cap. 0 disables. */
|
|
311
|
-
maxBytes: number;
|
|
312
|
-
/** Client request signal for deterministic cancel classification. */
|
|
313
|
-
reqSignal?: AbortSignal;
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
type PassthroughCloseReason = "terminal" | "client_cancel" | "body_stall" | "body_overflow";
|
|
317
|
-
type PassthroughFinalizeMeta = { closeReason: PassthroughCloseReason; terminalStatus?: "failed" | "incomplete" };
|
|
318
|
-
|
|
319
283
|
/**
|
|
320
|
-
*
|
|
321
|
-
*
|
|
322
|
-
*
|
|
323
|
-
*
|
|
324
|
-
* headers went out, it appends a protocol-compatible Anthropic `event: error` terminal
|
|
325
|
-
* frame after a blank-line boundary, closes, and cancels the upstream reader — never a
|
|
326
|
-
* total-wall-clock bound (slow-but-alive streams live).
|
|
327
|
-
* 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.
|
|
328
288
|
*/
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
let usageAcc: Rec = {};
|
|
339
|
-
// message_stop or an upstream error event: the turn's own terminal already went through.
|
|
340
|
-
let terminalSeen = false;
|
|
341
|
-
const inspectFrame = (frame: string) => {
|
|
342
|
-
const dataLine = frame
|
|
343
|
-
.split("\n")
|
|
344
|
-
.map(l => sseFieldValue(l, "data"))
|
|
345
|
-
.filter((v): v is string => v !== null)
|
|
346
|
-
.join("");
|
|
347
|
-
if (!dataLine) return;
|
|
348
|
-
let data: unknown;
|
|
349
|
-
try { data = JSON.parse(dataLine); } catch { return; }
|
|
350
|
-
if (!isRec(data)) return;
|
|
351
|
-
recordGenerationEvent(logCtx, data.type);
|
|
352
|
-
if (data.type === "message_start" && isRec(data.message) && isRec(data.message.usage)) {
|
|
353
|
-
usageAcc = { ...usageAcc, ...data.message.usage };
|
|
354
|
-
} else if (data.type === "message_delta" && isRec(data.usage)) {
|
|
355
|
-
usageAcc = { ...usageAcc, ...data.usage };
|
|
356
|
-
} else if (data.type === "message_stop" || data.type === "error") {
|
|
357
|
-
terminalSeen = true;
|
|
358
|
-
}
|
|
359
|
-
};
|
|
360
|
-
const inspect = (chunk: Uint8Array) => {
|
|
361
|
-
buffer += decoder.decode(chunk, { stream: true });
|
|
362
|
-
// SSE lines may end in CRLF, LF or CR. Normalize the inspection copy to LF (the forwarded
|
|
363
|
-
// bytes are untouched), holding a trailing CR until the next chunk shows whether an LF
|
|
364
|
-
// follows it, so a CRLF split across chunks stays one line ending.
|
|
365
|
-
const heldCr = buffer.endsWith("\r");
|
|
366
|
-
buffer = (heldCr ? buffer.slice(0, -1) : buffer).replace(/\r\n?/g, "\n") + (heldCr ? "\r" : "");
|
|
367
|
-
let sep: number;
|
|
368
|
-
while ((sep = buffer.indexOf("\n\n")) !== -1) {
|
|
369
|
-
const frame = buffer.slice(0, sep);
|
|
370
|
-
buffer = buffer.slice(sep + 2);
|
|
371
|
-
inspectFrame(frame);
|
|
372
|
-
}
|
|
373
|
-
};
|
|
374
|
-
// The last block can sit in the buffer without its blank line: an upstream that stopped after
|
|
375
|
-
// it, or a held trailing CR. Count it before deciding how the turn ended (the Responses relay
|
|
376
|
-
// flushes the same candidate). Returns true when this flush found the terminal in a block the
|
|
377
|
-
// client has not seen a blank line after, so the caller restores one.
|
|
378
|
-
const flushTail = (): boolean => {
|
|
379
|
-
const terminalBefore = terminalSeen;
|
|
380
|
-
const tail = (buffer + decoder.decode()).replace(/\r\n?/g, "\n");
|
|
381
|
-
buffer = "";
|
|
382
|
-
if (tail) inspectFrame(tail);
|
|
383
|
-
return terminalSeen && !terminalBefore && !tail.endsWith("\n\n");
|
|
384
|
-
};
|
|
385
|
-
const reader = upstream.getReader();
|
|
386
|
-
let settled = false;
|
|
387
|
-
let bodyBytes = 0;
|
|
388
|
-
let tapController: ReadableStreamDefaultController<Uint8Array> | undefined;
|
|
289
|
+
import {
|
|
290
|
+
anthropicUsageToOcx,
|
|
291
|
+
estimateClaudeRequestTokens,
|
|
292
|
+
resolvePassthroughBodyGuard,
|
|
293
|
+
sanitizePassthroughToolCallIds,
|
|
294
|
+
tapAnthropicSseForLog,
|
|
295
|
+
type PassthroughBodyGuard,
|
|
296
|
+
type PassthroughCloseReason,
|
|
297
|
+
} from "./messages-shared";
|
|
389
298
|
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
} catch { /* client already torn down */ }
|
|
401
|
-
};
|
|
402
|
-
const failBody = (closeReason: "body_stall" | "body_overflow", errType: string, message: string) => {
|
|
403
|
-
if (settled) return;
|
|
404
|
-
settled = true;
|
|
405
|
-
idle.cancel();
|
|
406
|
-
detachAbort();
|
|
407
|
-
const terminalInTail = flushTail();
|
|
408
|
-
recordUsage();
|
|
409
|
-
if (terminalSeen) {
|
|
410
|
-
// The turn already ended (message_stop or an upstream error event): an upstream that then
|
|
411
|
-
// idles or keeps sending did not cut it short. Same rule as the read-error branch,
|
|
412
|
-
// including the restored blank line for a terminal found only in the tail.
|
|
413
|
-
finalize(200, { closeReason: "terminal" });
|
|
414
|
-
try {
|
|
415
|
-
if (terminalInTail) tapController?.enqueue(encoder.encode("\n\n"));
|
|
416
|
-
tapController?.close();
|
|
417
|
-
} catch { /* client already torn down */ }
|
|
418
|
-
} else {
|
|
419
|
-
// A cut-short turn, logged as the Responses relay logs a stall-timeout incomplete
|
|
420
|
-
// (httpStatusForRequestLogTerminal: only a max_output_tokens incomplete is a 200). A 200
|
|
421
|
-
// row with no terminalStatus also lost its failure diagnostics in usage.jsonl.
|
|
422
|
-
logCtx.upstreamError = message.slice(0, 500);
|
|
423
|
-
finalize(502, { terminalStatus: "incomplete", closeReason });
|
|
424
|
-
closeWithErrorFrame(errType, message);
|
|
425
|
-
}
|
|
426
|
-
reader.cancel(new DOMException(message, closeReason === "body_stall" ? "TimeoutError" : "QuotaExceededError")).catch(() => {});
|
|
427
|
-
};
|
|
428
|
-
const idle = idleDeadline(guard?.stallMs ?? 0, () => {
|
|
429
|
-
failBody(
|
|
430
|
-
"body_stall",
|
|
431
|
-
"timeout_error",
|
|
432
|
-
`anthropic passthrough body stalled: no upstream bytes for ${Math.round((guard?.stallMs ?? 0) / 1000)}s`,
|
|
433
|
-
);
|
|
434
|
-
});
|
|
435
|
-
// Deterministic client-cancel classification: Bun may surface a client abort as a
|
|
436
|
-
// reader.read() rejection OR a resolved done (src/lib/abort.ts cancelBodyOnAbort
|
|
437
|
-
// rationale), so the listener performs first-wins settlement itself instead of
|
|
438
|
-
// relying on which shape the read takes.
|
|
439
|
-
const onClientAbort = () => {
|
|
440
|
-
if (settled) return;
|
|
441
|
-
settled = true;
|
|
442
|
-
idle.cancel();
|
|
443
|
-
detachAbort();
|
|
444
|
-
finalize(499, { closeReason: "client_cancel" });
|
|
445
|
-
try { tapController?.close(); } catch { /* downstream already torn down */ }
|
|
446
|
-
reader.cancel(guard?.reqSignal?.reason).catch(() => {});
|
|
447
|
-
};
|
|
448
|
-
const detachAbort = (() => {
|
|
449
|
-
const signal = guard?.reqSignal;
|
|
450
|
-
if (!signal) return () => {};
|
|
451
|
-
if (signal.aborted) {
|
|
452
|
-
queueMicrotask(onClientAbort);
|
|
453
|
-
return () => {};
|
|
454
|
-
}
|
|
455
|
-
signal.addEventListener("abort", onClientAbort, { once: true });
|
|
456
|
-
return () => signal.removeEventListener("abort", onClientAbort);
|
|
457
|
-
})();
|
|
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";
|
|
458
309
|
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
try {
|
|
466
|
-
idle.reset();
|
|
467
|
-
const { done, value } = await reader.read();
|
|
468
|
-
idle.pause();
|
|
469
|
-
if (settled) return; // stall/overflow/abort won the race while we awaited
|
|
470
|
-
if (done) {
|
|
471
|
-
settled = true;
|
|
472
|
-
idle.cancel();
|
|
473
|
-
detachAbort();
|
|
474
|
-
recordUsage();
|
|
475
|
-
finalize(200, { closeReason: "terminal" });
|
|
476
|
-
controller.close();
|
|
477
|
-
return;
|
|
478
|
-
}
|
|
479
|
-
if (value.byteLength > 0) {
|
|
480
|
-
bodyBytes += value.byteLength;
|
|
481
|
-
if (guard && guard.maxBytes > 0 && bodyBytes > guard.maxBytes) {
|
|
482
|
-
failBody(
|
|
483
|
-
"body_overflow",
|
|
484
|
-
"api_error",
|
|
485
|
-
`anthropic passthrough body exceeded ${guard.maxBytes} bytes`,
|
|
486
|
-
);
|
|
487
|
-
return;
|
|
488
|
-
}
|
|
489
|
-
}
|
|
490
|
-
inspect(value);
|
|
491
|
-
controller.enqueue(value);
|
|
492
|
-
} catch (err) {
|
|
493
|
-
if (settled) return;
|
|
494
|
-
// Bun can settle a fetch body read before it dispatches the abort listeners
|
|
495
|
-
// (consumeForInspection in relay.ts): read the signal itself before calling this
|
|
496
|
-
// rejection an upstream failure.
|
|
497
|
-
if (guard?.reqSignal?.aborted) {
|
|
498
|
-
onClientAbort();
|
|
499
|
-
return;
|
|
500
|
-
}
|
|
501
|
-
settled = true;
|
|
502
|
-
idle.cancel();
|
|
503
|
-
detachAbort();
|
|
504
|
-
const terminalInTail = flushTail();
|
|
505
|
-
recordUsage();
|
|
506
|
-
if (isTranslatorBudgetExceededError(err)) {
|
|
507
|
-
// A local cap, not an upstream failure: the non-streaming native Messages fold
|
|
508
|
-
// maps this error to a 413 itself, so it still errors the stream.
|
|
509
|
-
finalize(200, { closeReason: "terminal" });
|
|
510
|
-
try { controller.error(err); } catch { /* torn down */ }
|
|
511
|
-
return;
|
|
512
|
-
}
|
|
513
|
-
if (terminalSeen) {
|
|
514
|
-
// Only the transport trailer was lost; the client already has the turn's terminal.
|
|
515
|
-
// The Responses relay likewise reports a read error only without a seen terminal.
|
|
516
|
-
finalize(200, { closeReason: "terminal" });
|
|
517
|
-
try {
|
|
518
|
-
// An SSE parser drops an event that EOF cuts off before its blank line, so restore
|
|
519
|
-
// the delimiter when the terminal was only found in that unterminated tail.
|
|
520
|
-
if (terminalInTail) controller.enqueue(encoder.encode("\n\n"));
|
|
521
|
-
controller.close();
|
|
522
|
-
} catch { /* torn down */ }
|
|
523
|
-
reader.cancel(err).catch(() => {});
|
|
524
|
-
return;
|
|
525
|
-
}
|
|
526
|
-
// The upstream read failed after the 200 went out (a mid-stream socket reset). Log it
|
|
527
|
-
// the way the Responses relay does (onReadError): a truncated body is a failed turn,
|
|
528
|
-
// not a completed one. The client gets the same Anthropic error terminal as a stall,
|
|
529
|
-
// instead of a connection reset — or, on some Bun releases, a bare EOF that reads as
|
|
530
|
-
// a finished message.
|
|
531
|
-
const message = redactSecretString(`anthropic passthrough upstream stream failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
532
|
-
logCtx.transportPhase = "mid_stream";
|
|
533
|
-
logCtx.terminalSource = "synthetic";
|
|
534
|
-
logCtx.upstreamError = message.slice(0, 500);
|
|
535
|
-
if (logCtx.activeAttempt) logCtx.activeAttempt.streamAborted = true;
|
|
536
|
-
finalize(502, { terminalStatus: "failed", closeReason: "terminal" });
|
|
537
|
-
closeWithErrorFrame("api_error", message);
|
|
538
|
-
reader.cancel(err).catch(() => {});
|
|
539
|
-
}
|
|
540
|
-
},
|
|
541
|
-
cancel(reason) {
|
|
542
|
-
if (!settled) {
|
|
543
|
-
settled = true;
|
|
544
|
-
idle.cancel();
|
|
545
|
-
detachAbort();
|
|
546
|
-
finalize(499, { closeReason: "client_cancel" });
|
|
547
|
-
}
|
|
548
|
-
reader.cancel(reason).catch(() => {});
|
|
549
|
-
},
|
|
550
|
-
});
|
|
551
|
-
}
|
|
552
|
-
|
|
553
|
-
/**
|
|
554
|
-
* `tool_use.id` / `tool_result.tool_use_id` must match Anthropic's wire contract
|
|
555
|
-
* (`^[a-zA-Z0-9_-]+$`, <=64 chars). Third-party models mint other shapes — Devin's
|
|
556
|
-
* swe-2 emits `Bash:0#<hex>` — and a session history carrying them 400s the moment it
|
|
557
|
-
* is switched to a native Anthropic model ("messages.N.content.M.tool_use.id: String
|
|
558
|
-
* should match pattern"). The adapter path normalizes these via
|
|
559
|
-
* adapters/tool-call-id.ts (#1780); this passthrough bypasses that adapter, so the same
|
|
560
|
-
* allocator runs here. Stateless per request: conforming ids pass through byte-identical
|
|
561
|
-
* (prompt-cache keys untouched), rewritten ids keep call/result pairing stable.
|
|
562
|
-
* An empty id has no representable wire form, and forwarding `""` is what Anthropic
|
|
563
|
-
* rejects (#1767), so the request fails locally with a 400 before any upstream fetch.
|
|
564
|
-
*/
|
|
565
|
-
export function sanitizePassthroughToolCallIds(messages: unknown[]): void {
|
|
566
|
-
const blocks: Rec[] = [];
|
|
567
|
-
for (const message of messages) {
|
|
568
|
-
if (!isRec(message) || !Array.isArray(message.content)) continue;
|
|
569
|
-
for (const block of message.content) if (isRec(block)) blocks.push(block);
|
|
570
|
-
}
|
|
571
|
-
const fieldOf = (block: Rec): "id" | "tool_use_id" | undefined => {
|
|
572
|
-
if (typeof block.type !== "string") return undefined;
|
|
573
|
-
if (block.type.endsWith("tool_use")) return "id";
|
|
574
|
-
if (block.type.endsWith("tool_result")) return "tool_use_id";
|
|
575
|
-
return undefined;
|
|
576
|
-
};
|
|
577
|
-
const callIds = createToolCallIdAllocator();
|
|
578
|
-
for (const block of blocks) {
|
|
579
|
-
const field = fieldOf(block);
|
|
580
|
-
if (field && typeof block[field] === "string") callIds.reserve(block[field] as string);
|
|
581
|
-
}
|
|
582
|
-
for (const block of blocks) {
|
|
583
|
-
const field = fieldOf(block);
|
|
584
|
-
if (!field || typeof block[field] !== "string") continue;
|
|
585
|
-
const wire = callIds.allocate(block[field] as string);
|
|
586
|
-
if (wire === undefined) throw new AnthropicRequestError(`${block.type} block has an empty ${field}`);
|
|
587
|
-
block[field] = wire;
|
|
588
|
-
}
|
|
589
|
-
}
|
|
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;
|
|
590
316
|
|
|
591
317
|
async function anthropicNativePassthrough(
|
|
592
318
|
req: Request,
|
|
@@ -702,35 +428,6 @@ async function anthropicNativePassthrough(
|
|
|
702
428
|
});
|
|
703
429
|
}
|
|
704
430
|
|
|
705
|
-
const PASSTHROUGH_ERROR_TYPES = [
|
|
706
|
-
"invalid_request_error", "authentication_error", "permission_error", "not_found_error",
|
|
707
|
-
"rate_limit_error", "api_error", "overloaded_error", "request_too_large",
|
|
708
|
-
] as const;
|
|
709
|
-
const PASSTHROUGH_ERROR_DETAIL_MAX_CHARS = 64 * 1024;
|
|
710
|
-
const DEFAULT_BODY_STALL_SEC = 90;
|
|
711
|
-
const DEFAULT_BODY_MAX_BYTES = 64 * 1024 * 1024;
|
|
712
|
-
|
|
713
|
-
/**
|
|
714
|
-
* Normalize the claudeCode body-guard config (devlog 260716_passthrough_followups/010).
|
|
715
|
-
* Policy: exactly 0 disables; finite positive values are honored (stall clamped to
|
|
716
|
-
* min 1s); negative/non-finite/absent values fall back to the defaults.
|
|
717
|
-
*/
|
|
718
|
-
export function resolvePassthroughBodyGuard(config: OcxConfig, reqSignal?: AbortSignal): PassthroughBodyGuard {
|
|
719
|
-
const rawSec = config.claudeCode?.bodyStallSec;
|
|
720
|
-
const stallSec = rawSec === 0
|
|
721
|
-
? 0
|
|
722
|
-
: typeof rawSec === "number" && Number.isFinite(rawSec) && rawSec > 0
|
|
723
|
-
? Math.max(1, rawSec)
|
|
724
|
-
: DEFAULT_BODY_STALL_SEC;
|
|
725
|
-
const rawBytes = config.claudeCode?.bodyMaxBytes;
|
|
726
|
-
const maxBytes = rawBytes === 0
|
|
727
|
-
? 0
|
|
728
|
-
: typeof rawBytes === "number" && Number.isFinite(rawBytes) && rawBytes > 0
|
|
729
|
-
? Math.floor(rawBytes)
|
|
730
|
-
: DEFAULT_BODY_MAX_BYTES;
|
|
731
|
-
return { stallMs: stallSec * 1000, maxBytes, ...(reqSignal ? { reqSignal } : {}) };
|
|
732
|
-
}
|
|
733
|
-
|
|
734
431
|
type BoundedPassthroughBody =
|
|
735
432
|
| { kind: "ok"; text: string }
|
|
736
433
|
| { kind: "stall" }
|
|
@@ -1125,8 +822,9 @@ async function handleClaudeMessagesWithBudget(
|
|
|
1125
822
|
|
|
1126
823
|
let clientEncoder: ClientEncoderOption | undefined;
|
|
1127
824
|
// Native ChatGPT passthrough (openai-responses forward) accepts only Codex-shaped
|
|
1128
|
-
// bodies: it 400s on sampling params (
|
|
1129
|
-
//
|
|
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.
|
|
1130
828
|
let settledRoute: RouteResult | undefined;
|
|
1131
829
|
/**
|
|
1132
830
|
* Which replayed thinking fields the send that actually happened will serialize.
|
|
@@ -1193,7 +891,8 @@ async function handleClaudeMessagesWithBudget(
|
|
|
1193
891
|
clientEncoder = { protocol: "messages", stream, model: requestedModel, inputTokenFloor: claudeRequestTokenFloor() };
|
|
1194
892
|
}
|
|
1195
893
|
if (route.provider.adapter === "openai-responses") {
|
|
1196
|
-
|
|
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.
|
|
1197
896
|
delete internalBody.temperature;
|
|
1198
897
|
delete internalBody.top_p;
|
|
1199
898
|
delete internalBody.stop;
|
|
@@ -1409,7 +1108,11 @@ async function handleClaudeMessagesWithBudget(
|
|
|
1409
1108
|
// BEFORE translation (the translated Anthropic stream has no response.completed
|
|
1410
1109
|
// frame, so tapping it records a bogus 502 with no usage/cache detail).
|
|
1411
1110
|
const finalizeNativeLog = createFinalRequestLog(logIds, logCtx).finish;
|
|
1412
|
-
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, {
|
|
1413
1116
|
// Routing keeps Claude-only sidecar overrides; admission policy must follow the live owner.
|
|
1414
1117
|
codexAuthPolicy: config,
|
|
1415
1118
|
...(logIds?.admission ? { admission: logIds.admission } : {}),
|
|
@@ -1428,6 +1131,9 @@ async function handleClaudeMessagesWithBudget(
|
|
|
1428
1131
|
&& anthropicBody.tools.length === 0,
|
|
1429
1132
|
nativeReasoningReplay,
|
|
1430
1133
|
nativeReasoningMint,
|
|
1134
|
+
// Out of body like the replay above: the core hands these to an Anthropic destination only.
|
|
1135
|
+
anthropicServerTools: declaredAnthropicServerTools(anthropicBody),
|
|
1136
|
+
anthropicServerReplay: replayedAnthropicServerBlocks(anthropicBody),
|
|
1431
1137
|
claudeGoAffinity: { sessionLane: claudeGoSessionLane },
|
|
1432
1138
|
claudeNativeSessionId,
|
|
1433
1139
|
stripClaudeMainAuthForNoncanonicalForward: true,
|
|
@@ -1560,8 +1266,17 @@ async function handleClaudeMessagesWithBudget(
|
|
|
1560
1266
|
"translation_buffer_limit",
|
|
1561
1267
|
);
|
|
1562
1268
|
}
|
|
1269
|
+
// A declared client refusal keeps its 4xx: as a 502 Claude Code retries a verdict that cannot
|
|
1270
|
+
// change. Server classes and rate limits keep the 502 this path has always answered. A 400
|
|
1271
|
+
// worded as a client close answers the 499 httpStatusFromTerminalError logs for it.
|
|
1272
|
+
const refusalStatus = recognizedUpstreamError(translatedError ?? {})?.httpStatus;
|
|
1563
1273
|
return new Response(JSON.stringify(message), {
|
|
1564
|
-
status: isError
|
|
1274
|
+
status: isError
|
|
1275
|
+
? translatedError?.code === "context_length_exceeded" ? 400
|
|
1276
|
+
: refusalStatus !== undefined && refusalStatus < 429
|
|
1277
|
+
? refusalStatus === 400 && isClientClosedMessage(String(translatedError?.message ?? "")) ? 499 : refusalStatus
|
|
1278
|
+
: 502
|
|
1279
|
+
: 200,
|
|
1565
1280
|
headers: { "Content-Type": "application/json" },
|
|
1566
1281
|
});
|
|
1567
1282
|
}
|
|
@@ -1623,79 +1338,6 @@ async function handleClaudeMessagesWithBudget(
|
|
|
1623
1338
|
});
|
|
1624
1339
|
}
|
|
1625
1340
|
|
|
1626
|
-
/** Per-attachment token estimate for a base64 payload: real image dimensions when the
|
|
1627
|
-
* header is sniffable (Anthropic prices images at ~pixels/750), else decoded bytes/512,
|
|
1628
|
-
* min 256 — the same shape as the Kiro usage estimator (estimateKiroImageTokens). */
|
|
1629
|
-
function estimateBase64AttachmentTokens(data: string): number {
|
|
1630
|
-
const dims = sniffImageDimensions(data);
|
|
1631
|
-
if (dims) return Math.max(256, Math.ceil((dims.width * dims.height) / 750));
|
|
1632
|
-
const unpadded = data.endsWith("==") ? data.length - 2 : data.endsWith("=") ? data.length - 1 : data.length;
|
|
1633
|
-
return Math.max(256, Math.ceil(Math.floor((unpadded * 3) / 4) / 512));
|
|
1634
|
-
}
|
|
1635
|
-
|
|
1636
|
-
/**
|
|
1637
|
-
* Char-based token estimate for an Anthropic-shaped request body. Base64 attachment
|
|
1638
|
-
* payloads (image/document blocks in message content, including blocks nested in
|
|
1639
|
-
* tool_result.content) are counted as a bounded per-attachment estimate instead of raw
|
|
1640
|
-
* characters: one 2MB screenshot is ~2.7M base64 chars, which the plain chars/token
|
|
1641
|
-
* divide reports as hundreds of thousands of tokens versus a real cost around 1.6k.
|
|
1642
|
-
* That breaks the >2x drift bound the estimator is held to (devlog 260711_claude_inbound
|
|
1643
|
-
* 040 §3); a live 260-message turn whose replayed thinking was 78.8% of the body breached
|
|
1644
|
-
* it at 3.28x, which is why the estimate is projected onto the settled route. Text and url
|
|
1645
|
-
* sources are left in place and counted as characters, as is
|
|
1646
|
-
* anything outside protocol content positions (tool_use.input, tool schemas).
|
|
1647
|
-
*
|
|
1648
|
-
* `thinking` selects which replayed thinking fields the SETTLED route serializes, so the measure
|
|
1649
|
-
* describes the prompt this proxy forwards rather than the one the caller typed. Omitted, the
|
|
1650
|
-
* whole body counts — correct for the Anthropic-native wire, where nothing is projected away.
|
|
1651
|
-
* See `claude-request-projection.ts` for why a routed wire must project it out.
|
|
1652
|
-
*/
|
|
1653
|
-
export function estimateClaudeRequestTokens(
|
|
1654
|
-
raw: { system?: unknown; messages?: unknown; tools?: unknown },
|
|
1655
|
-
modelId: string | undefined,
|
|
1656
|
-
thinking: ClaudeThinkingProjection = CLAUDE_NATIVE_THINKING,
|
|
1657
|
-
): number {
|
|
1658
|
-
let attachmentTokens = 0;
|
|
1659
|
-
// Blank base64 payloads ONLY in protocol content positions: message content blocks and
|
|
1660
|
-
// blocks nested in tool_result.content. tool_use.input and tool schemas can legitimately
|
|
1661
|
-
// contain attachment-shaped JSON, and those bytes ARE serialized into function_call
|
|
1662
|
-
// arguments / tool definitions for routed providers, so they must keep counting as text.
|
|
1663
|
-
// system is text-only per the Anthropic protocol (no attachment sources), so it is
|
|
1664
|
-
// stringified as-is.
|
|
1665
|
-
const sanitizeBlock = (block: unknown): unknown => {
|
|
1666
|
-
if (!block || typeof block !== "object") return block;
|
|
1667
|
-
const b = block as Record<string, unknown>;
|
|
1668
|
-
if (b.type === "image" || b.type === "document") {
|
|
1669
|
-
const source = b.source as { type?: unknown; data?: unknown } | undefined;
|
|
1670
|
-
if (source && typeof source === "object" && source.type === "base64" && typeof source.data === "string") {
|
|
1671
|
-
attachmentTokens += estimateBase64AttachmentTokens(source.data);
|
|
1672
|
-
return { ...b, source: { ...(source as Record<string, unknown>), data: "" } };
|
|
1673
|
-
}
|
|
1674
|
-
return block;
|
|
1675
|
-
}
|
|
1676
|
-
if (b.type === "tool_result" && Array.isArray(b.content)) {
|
|
1677
|
-
return { ...b, content: (b.content as unknown[]).map(sanitizeBlock) };
|
|
1678
|
-
}
|
|
1679
|
-
return block;
|
|
1680
|
-
};
|
|
1681
|
-
const sanitizedMessages = (messages: unknown): unknown =>
|
|
1682
|
-
Array.isArray(messages)
|
|
1683
|
-
? messages.map(message => {
|
|
1684
|
-
if (!message || typeof message !== "object") return message;
|
|
1685
|
-
const m = message as Record<string, unknown>;
|
|
1686
|
-
return Array.isArray(m.content) ? { ...m, content: (m.content as unknown[]).map(sanitizeBlock) } : message;
|
|
1687
|
-
})
|
|
1688
|
-
: messages;
|
|
1689
|
-
const parts: string[] = [];
|
|
1690
|
-
if (raw.system !== undefined) parts.push(typeof raw.system === "string" ? raw.system : JSON.stringify(raw.system));
|
|
1691
|
-
if (raw.messages !== undefined) {
|
|
1692
|
-
const projected = projectClaudeRequest(raw, thinking);
|
|
1693
|
-
parts.push(JSON.stringify(sanitizedMessages(projected.messages)));
|
|
1694
|
-
}
|
|
1695
|
-
if (raw.tools !== undefined) parts.push(JSON.stringify(raw.tools));
|
|
1696
|
-
return Math.max(1, estimateTokens(parts.join("\n"), modelId) + attachmentTokens);
|
|
1697
|
-
}
|
|
1698
|
-
|
|
1699
1341
|
/**
|
|
1700
1342
|
* The projection for a route that has already settled.
|
|
1701
1343
|
*
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
import type { OcxConfig, OcxParsedRequest, OcxProviderConfig } from "../types";
|
|
16
16
|
import { modelInList } from "../types";
|
|
17
17
|
import { codexEffortRank, configuredReasoningEfforts, isCodexReasoningEffort, isDeclaredReasoningEffort, modelRecordValue } from "../reasoning-effort";
|
|
18
|
-
import { catalogModelEfforts } from "../codex/catalog";
|
|
18
|
+
import { catalogModelEfforts } from "../codex/catalog/effort";
|
|
19
19
|
|
|
20
20
|
/**
|
|
21
21
|
* True when the request carries codex-rs's spawned-child markers, matched EXACTLY.
|
package/src/server/images.ts
CHANGED
|
@@ -27,9 +27,11 @@ import { formatCodexProviderForLog } from "../codex/routing";
|
|
|
27
27
|
import { signalWithTimeout } from "../lib/abort";
|
|
28
28
|
import { readBoundedResponseBytes, type BoundedBytesResult } from "../lib/bounded-body";
|
|
29
29
|
import { sidecarEnter } from "../lib/sidecar-tracker";
|
|
30
|
+
import { isNonReplayableResponse } from "../lib/upstream-retry";
|
|
30
31
|
import type { OcxConfig } from "../types";
|
|
31
32
|
import { resolveFirstUsableOpenAiSidecar, selectImagesProvider } from "../providers/openai-sidecar";
|
|
32
|
-
import { selectProactiveApiKeyTransport } from "../providers/key-failover";
|
|
33
|
+
import { recordDistributedKeyFailure, selectProactiveApiKeyTransport } from "../providers/key-failover";
|
|
34
|
+
import { requestPacingOverloadResponse } from "./responses/pacing-overload";
|
|
33
35
|
import { getProviderRegistryEntry } from "../providers/registry";
|
|
34
36
|
import { readJsonRequestBody, resolveInboundBodyLimitBytes } from "./request-decompress";
|
|
35
37
|
import { ForwardAdmissionCredentialError, validateForwardAdmissionCredential } from "./auth-cors";
|
|
@@ -756,6 +758,7 @@ export async function handleImages(
|
|
|
756
758
|
catch { console.error("[images] Failed to release probe lease"); }
|
|
757
759
|
};
|
|
758
760
|
let url: string;
|
|
761
|
+
let recordKeyFailure = (_status: 401 | 429, _retryAfter: string | null) => {};
|
|
759
762
|
// Each branch is re-checked as it is entered: the forward check is defence in
|
|
760
763
|
// depth for the pre-filter above, and the keyed check runs before a key
|
|
761
764
|
// rotation is committed.
|
|
@@ -805,8 +808,15 @@ export async function handleImages(
|
|
|
805
808
|
//
|
|
806
809
|
// Transport variant: this branch reads `provider.baseUrl` and `provider.headers` to build
|
|
807
810
|
// the URL and the request, and it comes back with the credential already resolved.
|
|
808
|
-
|
|
811
|
+
let warmKeyProvider;
|
|
812
|
+
try { warmKeyProvider = selectProactiveApiKeyTransport(config, providerName, candidates.keyed.provider); }
|
|
813
|
+
catch (error) {
|
|
814
|
+
const refusal = requestPacingOverloadResponse(error);
|
|
815
|
+
if (refusal) return refusal;
|
|
816
|
+
throw error;
|
|
817
|
+
}
|
|
809
818
|
const provider = warmKeyProvider ?? candidates.keyed.provider;
|
|
819
|
+
recordKeyFailure = (status, retryAfter) => { recordDistributedKeyFailure(config, providerName, provider._apiKeyAttempt, status, Date.now(), retryAfter); };
|
|
810
820
|
// No fall back to the earlier snapshot once a pick has happened. The picker COMMITS its
|
|
811
821
|
// choice before returning, so if the chosen reference will not resolve -- revoked keychain
|
|
812
822
|
// entry, unset env var -- sending the previous key would authenticate a non-idempotent
|
|
@@ -865,6 +875,9 @@ export async function handleImages(
|
|
|
865
875
|
// compact already set this; the credential-bearing sidecars did not.
|
|
866
876
|
redirect: "manual",
|
|
867
877
|
});
|
|
878
|
+
if ((upstreamResponse.status === 401 || upstreamResponse.status === 429) && !isNonReplayableResponse(upstreamResponse)) {
|
|
879
|
+
recordKeyFailure(upstreamResponse.status, upstreamResponse.headers.get("retry-after"));
|
|
880
|
+
}
|
|
868
881
|
const observed = await readImageResponseBytes(upstreamResponse, {
|
|
869
882
|
maxBytes: IMAGES_RESPONSE_MAX_BYTES,
|
|
870
883
|
signal: linkedSignal.signal,
|
|
@@ -40,8 +40,9 @@ export const MAX_WS_FRAME_BYTES = 50 * 1024 * 1024;
|
|
|
40
40
|
*
|
|
41
41
|
* What must never happen is neither: an immortal socket plus a destination that silently accepts
|
|
42
42
|
* the orphaned delta. `tests/responses/ws-endpoint.test.ts` holds exactly that pair together.
|
|
43
|
-
*
|
|
44
|
-
*
|
|
43
|
+
* Responses admission separately reclaims its least recently used idle connection under
|
|
44
|
+
* capacity pressure. That is not a time limit, so expired-reference validation remains required.
|
|
45
|
+
* Keeping this shared timeout at 0 also preserves quiet live/voice sessions.
|
|
45
46
|
*/
|
|
46
47
|
export const WEBSOCKET_IDLE_TIMEOUT_SECONDS = 0;
|
|
47
48
|
/** Ceiling a finite websocket idle timeout must stay under, in seconds. See above. */
|