@bitkyc08/opencodex 2.59.0 → 2.61.0-preview.20260922
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/AGENTS_INSTALL.md +64 -0
- package/README.md +28 -1
- package/bin/ocx.mjs +382 -209
- package/gui/dist/assets/App-E64Rzjap.js +50 -0
- package/gui/dist/assets/Tray-_nfzD8k4.js +1 -0
- package/gui/dist/assets/index-DpdfZWMK.js +86 -0
- package/gui/dist/assets/index-_bpvxJu0.css +1 -0
- package/gui/dist/assets/usage-companion-chart-DtoK7T6h.js +1 -0
- package/gui/dist/favicon.png +0 -0
- package/gui/dist/index.html +2 -2
- package/gui/dist/provider-icons/stepfun-color.svg +1 -0
- package/package.json +8 -1
- package/src/adapters/anthropic-image-codec.ts +16 -2
- package/src/adapters/anthropic-image-normalize.ts +49 -2
- package/src/adapters/anthropic.ts +20 -1
- package/src/adapters/base.ts +23 -0
- package/src/adapters/coding-agent/protocol.ts +36 -6
- package/src/adapters/coding-agent/turn.ts +32 -4
- package/src/adapters/command-code.ts +52 -4
- package/src/adapters/cursor/catalog.ts +51 -7
- package/src/adapters/cursor/checkpoint-store.ts +3 -0
- package/src/adapters/cursor/discovery.ts +11 -8
- package/src/adapters/cursor/live-transport.ts +26 -9
- package/src/adapters/cursor/protobuf-request.ts +6 -3
- package/src/adapters/cursor/request-builder.ts +20 -4
- package/src/adapters/cursor/transport.ts +19 -0
- package/src/adapters/cursor.ts +14 -3
- package/src/adapters/declaration-carrier.ts +45 -0
- package/src/adapters/devin/cloud-direct/chat.ts +3 -1
- package/src/adapters/devin/cloud-direct/stated-reset-retry.ts +42 -5
- package/src/adapters/devin.ts +125 -36
- package/src/adapters/google-antigravity-replay.ts +1 -1
- package/src/adapters/google-antigravity-wire.ts +34 -7
- package/src/adapters/google-errors.ts +7 -1
- package/src/adapters/google-http.ts +49 -10
- package/src/adapters/google-tool-schema.ts +595 -31
- package/src/adapters/google-wire-compiler.ts +93 -10
- package/src/adapters/google-wire-shape.ts +461 -0
- package/src/adapters/google.ts +66 -11
- package/src/adapters/image.ts +4 -1
- package/src/adapters/input-media-guard.ts +21 -9
- package/src/adapters/kiro/usage.ts +3 -2
- package/src/adapters/kiro-tool-fallback.ts +1 -1
- package/src/adapters/ollama-native.ts +6 -0
- package/src/adapters/openai-chat/developer-role.ts +61 -0
- package/src/adapters/openai-chat/messages.ts +46 -27
- package/src/adapters/openai-chat/parallel-tool-calls.ts +32 -0
- package/src/adapters/openai-chat/passthrough.ts +33 -9
- package/src/adapters/openai-chat/reasoning-wire.ts +89 -0
- package/src/adapters/openai-chat-images.ts +3 -1
- package/src/adapters/openai-chat.ts +23 -58
- package/src/adapters/openai-responses/image-gen.ts +8 -6
- package/src/adapters/openai-responses/passthrough.ts +17 -3
- package/src/adapters/openai-responses/reasoning.ts +7 -0
- package/src/adapters/opencode-go-additional-tools.ts +12 -2
- package/src/adapters/registry.ts +3 -2
- package/src/adapters/run-turn-queue.ts +178 -29
- package/src/adapters/xai-web-search.ts +16 -1
- package/src/bridge/errors.ts +8 -2
- package/src/bridge/response-json.ts +9 -1
- package/src/bridge/sse.ts +13 -151
- package/src/chat/inbound.ts +141 -5
- package/src/claude/desktop-3p.ts +7 -1
- package/src/claude/desktop-first-party.ts +183 -0
- package/src/claude/desktop-gateway-state.ts +41 -0
- package/src/claude/inbound-content-options.ts +6 -0
- package/src/claude/inbound.ts +32 -6
- package/src/claude/intercept/connect-proxy.ts +179 -0
- package/src/claude/intercept/listener.ts +122 -0
- package/src/claude/intercept/local-ca.ts +298 -0
- package/src/claude/intercept/runtime.ts +98 -0
- package/src/claude/intercept/settings.ts +189 -0
- package/src/cli/access.ts +87 -0
- package/src/cli/account-auth.ts +19 -0
- package/src/cli/account-extended.ts +4 -4
- package/src/cli/capabilities.ts +31 -0
- package/src/cli/claude-desktop.ts +206 -16
- package/src/cli/codex-shim-autorestore.ts +3 -0
- package/src/cli/companion.ts +56 -0
- package/src/cli/dispatch.ts +46 -7
- package/src/cli/doctor.ts +28 -9
- package/src/cli/ensure-desired-integrations.ts +43 -5
- package/src/cli/help.ts +7 -9
- package/src/cli/hub.ts +3 -2
- package/src/cli/index.ts +203 -59
- package/src/cli/init.ts +8 -0
- package/src/cli/integrations.ts +7 -1
- package/src/cli/opencode.ts +2 -2
- package/src/cli/provider.ts +13 -1
- package/src/cli/registry.ts +41 -2
- package/src/cli/resolve.ts +230 -0
- package/src/cli/root.ts +24 -1
- package/src/cli/start-ownership-publication.ts +56 -0
- package/src/cli/status-probes.ts +2 -18
- package/src/cli/status.ts +62 -0
- package/src/cli/stop-report.ts +143 -0
- package/src/cli/uninstall-plan.ts +9 -0
- package/src/client/machine-api.ts +2 -2
- package/src/client/machine-listener.ts +4 -7
- package/src/client/runtime.ts +26 -2
- package/src/clients/aside-profiles.ts +4 -0
- package/src/clients/config-export/zcode-store.ts +157 -0
- package/src/clients/config-export.ts +36 -0
- package/src/codex/account-store.ts +65 -0
- package/src/codex/app-server-processes.ts +72 -40
- package/src/codex/auth-api/account-list.ts +19 -11
- package/src/codex/auth-api/login-flow.ts +6 -1
- package/src/codex/auth-api/pool-quota-probe.ts +30 -7
- package/src/codex/autostart-health.ts +28 -0
- package/src/codex/catalog/build-entries.ts +2 -2
- package/src/codex/catalog/effort.ts +3 -3
- package/src/codex/catalog/gather-capture.ts +21 -2
- package/src/codex/catalog/model-hints.ts +29 -28
- package/src/codex/catalog/parsing.ts +7 -0
- package/src/codex/catalog/provider-models.ts +43 -17
- package/src/codex/catalog/retained-sync.ts +24 -28
- package/src/codex/catalog/routed-gather.ts +19 -0
- package/src/codex/context-compat.ts +5 -2
- package/src/codex/convergence.ts +2 -2
- package/src/codex/desired-state.ts +4 -1
- package/src/codex/history-job.ts +6 -6
- package/src/codex/history-provider.ts +31 -166
- package/src/codex/history-rollout-read.ts +174 -0
- package/src/codex/inject/config-toml.ts +41 -6
- package/src/codex/inject/paginated-openai-compat.ts +90 -0
- package/src/codex/inject.ts +18 -15
- package/src/codex/injected-marker.ts +18 -0
- package/src/codex/internal/catalog-writer.ts +33 -1
- package/src/codex/main-account.ts +6 -0
- package/src/codex/model-cache.ts +99 -6
- package/src/codex/model-entitlement-admission.ts +59 -0
- package/src/codex/model-entitlements.ts +116 -54
- package/src/codex/native-main-admission.ts +83 -0
- package/src/codex/observed-model-denials.ts +101 -8
- package/src/codex/prompt-text-probe.ts +9 -6
- package/src/codex/routing/health-store.ts +39 -0
- package/src/codex/routing/selection.ts +37 -1
- package/src/codex/routing.ts +12 -42
- package/src/codex/shim-templates.ts +29 -3
- package/src/codex/shim.ts +1 -1
- package/src/codex/subagent-model-fallback.ts +22 -4
- package/src/combos/failover.ts +3 -0
- package/src/companion/settings.ts +132 -0
- package/src/config/admitted-identity.ts +222 -0
- package/src/config/atomic-write.ts +117 -5
- package/src/config/diagnostics.ts +22 -1
- package/src/config/feature-flags.ts +5 -0
- package/src/config/load-degrade.ts +52 -7
- package/src/config/process-state.ts +1 -1
- package/src/config/proxy-env.ts +8 -2
- package/src/config/schema/compaction-triggers.ts +11 -0
- package/src/config/schema/config-schema.ts +31 -1
- package/src/config/schema/leaf-validators.ts +59 -0
- package/src/config.ts +3 -3
- package/src/generated/compatibility-version.json +604 -280
- package/src/grok/reset-coupons.ts +38 -19
- package/src/images/loop.ts +6 -1
- package/src/integrations/aside-profile-context.ts +37 -3
- package/src/integrations/aside-profile-journal.ts +68 -3
- package/src/integrations/aside-profiles.ts +128 -3
- package/src/integrations/config-io.ts +44 -10
- package/src/integrations/merge.ts +120 -13
- package/src/integrations/mutation-plan.ts +921 -0
- package/src/integrations/registry.ts +38 -0
- package/src/integrations/state.ts +78 -45
- package/src/integrations/target.ts +208 -0
- package/src/integrations/writer.ts +134 -110
- package/src/lab/conformance/fixture-provider.ts +5 -0
- package/src/lab/live/transport.ts +4 -0
- package/src/lab/live/types.ts +5 -0
- package/src/lab/subject/behavior-fingerprint.ts +1 -1
- package/src/lib/admin-secrets.ts +9 -1
- package/src/lib/browser-launch-notice.ts +59 -0
- package/src/lib/bun-runtime.ts +6 -2
- package/src/lib/debug-log-buffer.ts +6 -1
- package/src/lib/debug.ts +63 -0
- package/src/lib/errors.ts +79 -0
- package/src/lib/http-response-semantics.ts +57 -0
- package/src/lib/lab-live-pinned-sender.ts +26 -12
- package/src/lib/open-url.ts +51 -7
- package/src/lib/package-tree-integrity.ts +2 -1
- package/src/lib/package-version.ts +8 -0
- package/src/lib/pinned-http.ts +142 -2
- package/src/lib/plain-data.ts +103 -0
- package/src/lib/process-control.ts +13 -5
- package/src/lib/provider-egress.ts +310 -0
- package/src/lib/provider-outbound.ts +109 -16
- package/src/lib/proxy-env.ts +82 -7
- package/src/lib/request-execution-budget.ts +72 -0
- package/src/lib/request-failure-attribution.ts +183 -0
- package/src/lib/request-failure-model.ts +236 -0
- package/src/lib/request-resend-gate.ts +138 -0
- package/src/lib/socks5-fetch.ts +136 -26
- package/src/lib/spend-ledger-owner.ts +364 -0
- package/src/lib/spend-reservation-ledger.ts +218 -27
- package/src/lib/standalone.ts +16 -0
- package/src/lib/upstream-retry.ts +167 -16
- package/src/lib/windows-system-proxy.ts +16 -11
- package/src/lib/winsw.ts +2 -2
- package/src/oauth/callback-server.ts +4 -3
- package/src/oauth/generic-account-failover.ts +1 -0
- package/src/oauth/health.ts +12 -1
- package/src/oauth/index.ts +27 -108
- package/src/oauth/login-cli.ts +80 -29
- package/src/oauth/login-flow-state.ts +127 -0
- package/src/providers/api-key-resolve.ts +133 -0
- package/src/providers/api-key-selection.ts +5 -1
- package/src/providers/derive.ts +34 -17
- package/src/providers/devin-cli-authmode-migration.ts +14 -10
- package/src/providers/key-failover.ts +97 -19
- package/src/providers/key-store.ts +34 -110
- package/src/providers/model-rename-fields.ts +147 -0
- package/src/providers/model-rename-migration.ts +179 -38
- package/src/providers/model-rename-startup.ts +7 -5
- package/src/providers/openai-virtual-models.ts +42 -2
- package/src/providers/quota/antigravity.ts +22 -2
- package/src/providers/quota/vendor-probes-key.ts +38 -23
- package/src/providers/reasoning-metadata.ts +43 -18
- package/src/providers/registry/entries-core.ts +47 -20
- package/src/providers/registry/entries-extended.ts +63 -4
- package/src/providers/registry/model-ids.ts +168 -0
- package/src/providers/registry/model-seeds.ts +56 -10
- package/src/providers/registry/types.ts +2 -0
- package/src/providers/resolved-model-policy-merge.ts +167 -0
- package/src/providers/resolved-model-policy.ts +406 -0
- package/src/providers/stale-vision-classification-migration.ts +137 -0
- package/src/providers/xai-transport.ts +12 -1
- package/src/reasoning-effort.ts +8 -0
- package/src/responses/apply-patch-envelope.ts +0 -12
- package/src/responses/freeform-wrapper-scan.ts +279 -0
- package/src/responses/function-call-compat.ts +38 -1
- package/src/responses/inline-document.ts +65 -0
- package/src/responses/input-media.ts +42 -8
- package/src/responses/legacy-dotted-tool-name-repair.ts +134 -0
- package/src/responses/muse-tool-name-alias.ts +19 -0
- package/src/responses/parser-content.ts +8 -2
- package/src/responses/parser-tools.ts +3 -0
- package/src/responses/parser.ts +3 -1
- package/src/responses/progressive-freeform-input.ts +130 -0
- package/src/responses/reasoning-envelope.ts +30 -0
- package/src/responses/schema.ts +3 -0
- package/src/responses/state.ts +5 -12
- package/src/responses/tool-name-aliases.ts +15 -1
- package/src/router.ts +108 -117
- package/src/routing/compatibility/behavior.ts +9 -0
- package/src/routing/compatibility/subject.ts +16 -1
- package/src/server/adapter-resolve.ts +9 -0
- package/src/server/admission-model-scope.ts +219 -0
- package/src/server/audio-live.ts +9 -3
- package/src/server/audio-upstream.ts +18 -0
- package/src/server/auth-cors.ts +29 -0
- package/src/server/chat-completions.ts +60 -4
- package/src/server/chat-native.ts +19 -4
- package/src/server/claude-messages.ts +61 -20
- package/src/server/effort-row.ts +11 -3
- package/src/server/grok-responses-control-frame.ts +160 -1
- package/src/server/grok-responses-snapshot-repair.ts +113 -11
- package/src/server/gui-freshness.ts +103 -0
- package/src/server/gui-static.ts +7 -9
- package/src/server/images.ts +59 -6
- package/src/server/index/claude-intercept-lifecycle.ts +49 -0
- package/src/server/index/serve-options.ts +142 -39
- package/src/server/index/spend-ledger-lifecycle.ts +92 -0
- package/src/server/index/startup-warnings.ts +24 -0
- package/src/server/index/websocket-handler.ts +6 -1
- package/src/server/index.ts +34 -38
- package/src/server/lifecycle.ts +4 -4
- package/src/server/live-call-bindings.ts +6 -0
- package/src/server/live.ts +88 -3
- package/src/server/management/agent-settings-routes.ts +121 -36
- package/src/server/management/aside-profile-routes.ts +266 -7
- package/src/server/management/companion-routes.ts +77 -0
- package/src/server/management/config-routes.ts +18 -2
- package/src/server/management/context.ts +3 -0
- package/src/server/management/integration-routes.ts +287 -5
- package/src/server/management/logs-usage-routes.ts +19 -0
- package/src/server/management/metrics-routes.ts +20 -0
- package/src/server/management/model-rows.ts +224 -12
- package/src/server/management/native-integration-routes.ts +103 -6
- package/src/server/management/oauth-account-routes.ts +45 -7
- package/src/server/management/route-registry.ts +20 -0
- package/src/server/management/shared.ts +28 -4
- package/src/server/management/system-restart.ts +7 -2
- package/src/server/management/system-routes.ts +2 -0
- package/src/server/management/usage-aggregate-cache.ts +4 -0
- package/src/server/management/usage-timeline-routes.ts +44 -0
- package/src/server/management-api.ts +10 -9
- package/src/server/management-auth.ts +15 -1
- package/src/server/proxy-liveness.ts +75 -0
- package/src/server/readiness.ts +29 -10
- package/src/server/relay-eager.ts +24 -2
- package/src/server/relay.ts +138 -12
- package/src/server/request-log-failure-attribution.ts +99 -0
- package/src/server/request-log.ts +169 -2
- package/src/server/request-metrics.ts +298 -0
- package/src/server/responses/adapter-continuation.ts +3 -3
- package/src/server/responses/adapter-dispatch.ts +11 -6
- package/src/server/responses/codex-ws-wire.ts +34 -8
- package/src/server/responses/combo-stream-preflight.ts +168 -6
- package/src/server/responses/compact.ts +43 -10
- package/src/server/responses/compaction-routing.ts +111 -0
- package/src/server/responses/core-codex-account.ts +8 -3
- package/src/server/responses/core-combo.ts +7 -7
- package/src/server/responses/core-normalize.ts +6 -12
- package/src/server/responses/core-opaque-recovery.ts +91 -0
- package/src/server/responses/core-options.ts +4 -0
- package/src/server/responses/encrypted-payload.ts +20 -2
- package/src/server/responses/fetch-helpers.ts +124 -8
- package/src/server/responses/input-admission.ts +10 -0
- package/src/server/responses/passthrough-delivery.ts +45 -15
- package/src/server/responses/passthrough-dispatch.ts +215 -44
- package/src/server/responses/passthrough-error.ts +27 -8
- package/src/server/responses/policy-fallback.ts +5 -13
- package/src/server/responses/request-prepare.ts +109 -18
- package/src/server/responses/request-send-budget.ts +17 -1
- package/src/server/responses/request-sidecar-auth.ts +1 -1
- package/src/server/responses/request-transport.ts +26 -6
- package/src/server/responses/reset-replay.ts +108 -0
- package/src/server/responses/run-turn-execution.ts +25 -3
- package/src/server/responses/sidecar-execution.ts +17 -2
- package/src/server/responses/ws-upstream.ts +14 -27
- package/src/server/responses-custom-tool-repair.ts +27 -54
- package/src/server/responses-request-tool-scope.ts +214 -0
- package/src/server/responses-undeclared-tool-guard.ts +35 -2
- package/src/server/search.ts +25 -1
- package/src/server/sse-payload-rewrite.ts +1 -1
- package/src/server/usage-ledger-retention.ts +73 -0
- package/src/service/cli.ts +48 -2
- package/src/service/health.ts +3 -2
- package/src/service/install-state-contract.d.mts +27 -0
- package/src/service/install-state-contract.mjs +34 -0
- package/src/service/launchd.ts +1 -1
- package/src/service/orchestration.ts +2 -4
- package/src/service/ownership-compatibility.ts +164 -0
- package/src/service/ownership-mutation-lease.d.mts +32 -0
- package/src/service/ownership-mutation-lease.mjs +211 -0
- package/src/service/repair.ts +45 -1
- package/src/service/state-lock.ts +269 -0
- package/src/service/state-record.d.mts +36 -0
- package/src/service/state-record.mjs +138 -0
- package/src/service/state.ts +582 -68
- package/src/service/windows-taskxml.ts +11 -10
- package/src/service.ts +7 -3
- package/src/tray/windows-tray.ps1 +156 -4
- package/src/types/config.ts +48 -3
- package/src/types/provider.ts +91 -0
- package/src/types/request.ts +30 -2
- package/src/types/tools.ts +33 -0
- package/src/types.ts +4 -0
- package/src/update/index.ts +207 -63
- package/src/update/job.ts +9 -5
- package/src/update/ownership-transaction.ts +47 -0
- package/src/update/restart-ownership.ts +54 -0
- package/src/update/runtime-ownership.d.mts +40 -0
- package/src/update/runtime-ownership.mjs +122 -0
- package/src/usage/attempt-delivery.ts +198 -0
- package/src/usage/cache-diagnostic.ts +305 -0
- package/src/usage/failure-fingerprint.ts +118 -0
- package/src/usage/failure-projection-cache.ts +174 -0
- package/src/usage/failure-projection.ts +174 -0
- package/src/usage/ledger-retention.ts +165 -0
- package/src/usage/log.ts +126 -79
- package/src/usage/request-outcome.ts +150 -0
- package/src/usage/retention-contract.ts +28 -0
- package/src/usage/summary.ts +2 -2
- package/src/usage/telemetry-contract.ts +237 -0
- package/src/usage/timeline.ts +236 -0
- package/src/vision/eligibility.ts +88 -9
- package/src/vision/plan.ts +34 -10
- package/src/web-search/alpha-search.ts +21 -1
- package/src/web-search/executor.ts +41 -2
- package/src/web-search/loop.ts +6 -1
- package/gui/dist/assets/index-C5IebErG.js +0 -136
- package/gui/dist/assets/index-OESInAjC.css +0 -1
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
import type { ResponsesTerminalStatus } from "../bridge";
|
|
2
|
+
import type { AttemptRecoveryKind } from "../usage/log";
|
|
3
|
+
import {
|
|
4
|
+
REQUEST_FAILURE_CAUSES,
|
|
5
|
+
type RequestFailureCause,
|
|
6
|
+
causeForRecoveryKind,
|
|
7
|
+
} from "../lib/request-failure-model";
|
|
8
|
+
import {
|
|
9
|
+
REQUEST_OUTCOME_CLASSES,
|
|
10
|
+
classifyRequestOutcome,
|
|
11
|
+
type RequestOutcomeClass,
|
|
12
|
+
} from "../usage/request-outcome";
|
|
13
|
+
|
|
14
|
+
export const REQUEST_METRICS_PROTOCOLS = Object.freeze(["responses", "chat", "messages", "unknown"] as const);
|
|
15
|
+
/**
|
|
16
|
+
* The exporter's result label set IS the shared outcome vocabulary, not a copy of it. Restating
|
|
17
|
+
* these four strings here is what let the exporter and the dashboard drift into disagreeing about
|
|
18
|
+
* the same request.
|
|
19
|
+
*/
|
|
20
|
+
export const REQUEST_METRICS_RESULTS = REQUEST_OUTCOME_CLASSES;
|
|
21
|
+
/**
|
|
22
|
+
* Closed recovery classes exported as Prometheus label values.
|
|
23
|
+
*
|
|
24
|
+
* Bounded by construction: the label can only ever take one of these strings, so no user, model,
|
|
25
|
+
* account or request identifier can reach a series name. `quota`, `policy` and `ciphertext` are
|
|
26
|
+
* separate members because an operator seeing a spike needs to know which one it is -- waiting
|
|
27
|
+
* out a rate limit, changing accounts, changing the prompt and dropping stale ciphertext are
|
|
28
|
+
* four different responses, and collapsing them is what made the existing counter unactionable.
|
|
29
|
+
*/
|
|
30
|
+
export const REQUEST_METRICS_RECOVERY_CLASSES = Object.freeze([
|
|
31
|
+
"transient",
|
|
32
|
+
"connection",
|
|
33
|
+
"credential",
|
|
34
|
+
"rate_limit",
|
|
35
|
+
"quota",
|
|
36
|
+
"policy",
|
|
37
|
+
"ciphertext",
|
|
38
|
+
"payload",
|
|
39
|
+
"empty_completion",
|
|
40
|
+
"effort_downgrade",
|
|
41
|
+
"other",
|
|
42
|
+
] as const);
|
|
43
|
+
|
|
44
|
+
export const REQUEST_DURATION_BUCKETS_SECONDS = Object.freeze([0.1, 0.25, 0.5, 1, 2.5, 5, 10, 30, 60] as const);
|
|
45
|
+
export const REQUEST_TTFT_BUCKETS_SECONDS = Object.freeze([0.05, 0.1, 0.25, 0.5, 1, 2.5, 5, 10, 30] as const);
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* The failure-cause label set IS the shared dictionary, for the same reason the result label set
|
|
49
|
+
* is the shared outcome vocabulary: a restated copy is what let two surfaces drift into
|
|
50
|
+
* disagreeing about the same request.
|
|
51
|
+
*
|
|
52
|
+
* It labels a COUNTER and never a histogram. Fifteen causes across four protocols is sixty
|
|
53
|
+
* series, fixed for the lifetime of the roster, and every value comes from a frozen list, so no
|
|
54
|
+
* user, model, account or request identifier can reach a series name. A histogram labelled by
|
|
55
|
+
* cause would multiply that by its bucket count for no question anyone asks.
|
|
56
|
+
*/
|
|
57
|
+
export const REQUEST_METRICS_FAILURE_CAUSES = REQUEST_FAILURE_CAUSES;
|
|
58
|
+
|
|
59
|
+
export type RequestMetricsProtocol = typeof REQUEST_METRICS_PROTOCOLS[number];
|
|
60
|
+
export type RequestMetricsResult = RequestOutcomeClass;
|
|
61
|
+
export type RequestMetricsRecoveryClass = typeof REQUEST_METRICS_RECOVERY_CLASSES[number];
|
|
62
|
+
|
|
63
|
+
export interface RequestMetricFinalFact {
|
|
64
|
+
protocol?: "responses" | "chat" | "messages";
|
|
65
|
+
status: number;
|
|
66
|
+
durationMs: number;
|
|
67
|
+
firstOutputMs?: number;
|
|
68
|
+
terminalStatus?: ResponsesTerminalStatus;
|
|
69
|
+
closeReason?: "terminal" | "client_cancel" | "non_stream" | "body_stall" | "body_overflow";
|
|
70
|
+
attempts?: ReadonlyArray<{
|
|
71
|
+
sendCount: number;
|
|
72
|
+
recoveryKinds: readonly AttemptRecoveryKind[];
|
|
73
|
+
}>;
|
|
74
|
+
spendSends?: number;
|
|
75
|
+
/**
|
|
76
|
+
* Why this request failed, as the recorder derived it. Absent when it did not fail, which is
|
|
77
|
+
* why the counter below cannot be reconstructed by subtracting completions from totals.
|
|
78
|
+
*/
|
|
79
|
+
failureCause?: RequestFailureCause;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export interface RequestMetricsRecorder {
|
|
83
|
+
recordFinalRequest(fact: RequestMetricFinalFact): void;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export interface RequestMetricsSnapshotter {
|
|
87
|
+
snapshot(): string;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export interface RequestMetricsOwner extends RequestMetricsRecorder, RequestMetricsSnapshotter {
|
|
91
|
+
resetForTests(): void;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
interface HistogramCell {
|
|
95
|
+
buckets: number[];
|
|
96
|
+
count: number;
|
|
97
|
+
sum: number;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const protocolCell = (value: RequestMetricsProtocol): number => REQUEST_METRICS_PROTOCOLS.indexOf(value);
|
|
101
|
+
const resultCell = (value: RequestMetricsResult): number => REQUEST_METRICS_RESULTS.indexOf(value);
|
|
102
|
+
const recoveryCell = (value: RequestMetricsRecoveryClass): number => REQUEST_METRICS_RECOVERY_CLASSES.indexOf(value);
|
|
103
|
+
const failureCauseCell = (value: RequestFailureCause): number => REQUEST_METRICS_FAILURE_CAUSES.indexOf(value);
|
|
104
|
+
|
|
105
|
+
function matrix(rows: number, columns: number): number[][] {
|
|
106
|
+
return Array.from({ length: rows }, () => Array.from({ length: columns }, () => 0));
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function histograms(bounds: readonly number[]): HistogramCell[][] {
|
|
110
|
+
return Array.from({ length: REQUEST_METRICS_PROTOCOLS.length }, () => (
|
|
111
|
+
Array.from({ length: REQUEST_METRICS_RESULTS.length }, () => ({
|
|
112
|
+
buckets: Array.from({ length: bounds.length + 1 }, () => 0),
|
|
113
|
+
count: 0,
|
|
114
|
+
sum: 0,
|
|
115
|
+
}))
|
|
116
|
+
));
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Metrics class for each shared failure cause.
|
|
121
|
+
*
|
|
122
|
+
* Keyed on the cause rather than on the recovery kind so this projection and the durable log
|
|
123
|
+
* speak one vocabulary. Total by construction: the previous switch ended in `default: "other"`,
|
|
124
|
+
* which meant a recovery kind added later compiled cleanly and then disappeared into an
|
|
125
|
+
* unactionable bucket. A missing member is now a typecheck failure.
|
|
126
|
+
*/
|
|
127
|
+
const CAUSE_METRICS_CLASS = {
|
|
128
|
+
"transport-unsent": "connection",
|
|
129
|
+
"transport-ambiguous": "connection",
|
|
130
|
+
"upstream-declined": "transient",
|
|
131
|
+
"rate-limit": "rate_limit",
|
|
132
|
+
"quota-exhausted": "quota",
|
|
133
|
+
"credential-rejected": "credential",
|
|
134
|
+
"policy-refusal": "policy",
|
|
135
|
+
"parameter-rejected": "effort_downgrade",
|
|
136
|
+
"ciphertext-refusal": "ciphertext",
|
|
137
|
+
"payload-too-large": "payload",
|
|
138
|
+
"payload-rejected": "payload",
|
|
139
|
+
"upstream-fault": "transient",
|
|
140
|
+
"empty-output": "empty_completion",
|
|
141
|
+
"client-cancelled": "other",
|
|
142
|
+
"local-refusal": "other",
|
|
143
|
+
} as const satisfies Record<RequestFailureCause, RequestMetricsRecoveryClass>;
|
|
144
|
+
|
|
145
|
+
function recoveryClass(kind: AttemptRecoveryKind): RequestMetricsRecoveryClass {
|
|
146
|
+
return CAUSE_METRICS_CLASS[causeForRecoveryKind(kind)];
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function observeHistogram(cell: HistogramCell, bounds: readonly number[], value: number): void {
|
|
150
|
+
if (!Number.isFinite(value) || value < 0) return;
|
|
151
|
+
cell.count += 1;
|
|
152
|
+
cell.sum += value;
|
|
153
|
+
for (let index = 0; index < bounds.length; index += 1) {
|
|
154
|
+
if (value <= bounds[index]!) cell.buckets[index]! += 1;
|
|
155
|
+
}
|
|
156
|
+
cell.buckets[bounds.length]! += 1;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function sampleLabels(protocol: RequestMetricsProtocol, result?: RequestMetricsResult): string {
|
|
160
|
+
return result === undefined
|
|
161
|
+
? `{protocol="${protocol}"}`
|
|
162
|
+
: `{protocol="${protocol}",result="${result}"}`;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function appendHistogram(
|
|
166
|
+
lines: string[],
|
|
167
|
+
name: string,
|
|
168
|
+
help: string,
|
|
169
|
+
values: HistogramCell[][],
|
|
170
|
+
bounds: readonly number[],
|
|
171
|
+
): void {
|
|
172
|
+
lines.push(`# HELP ${name} ${help}`, `# TYPE ${name} histogram`);
|
|
173
|
+
for (const protocol of REQUEST_METRICS_PROTOCOLS) {
|
|
174
|
+
for (const result of REQUEST_METRICS_RESULTS) {
|
|
175
|
+
const cell = values[protocolCell(protocol)]![resultCell(result)]!;
|
|
176
|
+
for (let index = 0; index < bounds.length; index += 1) {
|
|
177
|
+
lines.push(`${name}_bucket{protocol="${protocol}",result="${result}",le="${bounds[index]}"} ${cell.buckets[index]}`);
|
|
178
|
+
}
|
|
179
|
+
lines.push(`${name}_bucket{protocol="${protocol}",result="${result}",le="+Inf"} ${cell.buckets[bounds.length]}`);
|
|
180
|
+
lines.push(`${name}_sum${sampleLabels(protocol, result)} ${cell.sum}`);
|
|
181
|
+
lines.push(`${name}_count${sampleLabels(protocol, result)} ${cell.count}`);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
export function createRequestMetricsOwner(
|
|
187
|
+
processStartTimeSeconds = Date.now() / 1000,
|
|
188
|
+
): RequestMetricsOwner {
|
|
189
|
+
let logicalRequests = matrix(REQUEST_METRICS_PROTOCOLS.length, REQUEST_METRICS_RESULTS.length);
|
|
190
|
+
let physicalSends = Array.from({ length: REQUEST_METRICS_PROTOCOLS.length }, () => 0);
|
|
191
|
+
let recoveries = matrix(REQUEST_METRICS_PROTOCOLS.length, REQUEST_METRICS_RECOVERY_CLASSES.length);
|
|
192
|
+
let failureCauses = matrix(REQUEST_METRICS_PROTOCOLS.length, REQUEST_METRICS_FAILURE_CAUSES.length);
|
|
193
|
+
let durations = histograms(REQUEST_DURATION_BUCKETS_SECONDS);
|
|
194
|
+
let ttft = histograms(REQUEST_TTFT_BUCKETS_SECONDS);
|
|
195
|
+
let missingTtft = matrix(REQUEST_METRICS_PROTOCOLS.length, REQUEST_METRICS_RESULTS.length);
|
|
196
|
+
|
|
197
|
+
return {
|
|
198
|
+
recordFinalRequest(fact): void {
|
|
199
|
+
const protocol: RequestMetricsProtocol = fact.protocol ?? "unknown";
|
|
200
|
+
const result = classifyRequestOutcome(fact);
|
|
201
|
+
const protocolIndex = protocolCell(protocol);
|
|
202
|
+
const resultIndex = resultCell(result);
|
|
203
|
+
logicalRequests[protocolIndex]![resultIndex]! += 1;
|
|
204
|
+
|
|
205
|
+
const attempts = fact.attempts;
|
|
206
|
+
const sends = attempts === undefined
|
|
207
|
+
? (Number.isInteger(fact.spendSends) && fact.spendSends! >= 0 ? fact.spendSends! : 0)
|
|
208
|
+
: attempts.reduce((total, attempt) => (
|
|
209
|
+
Number.isInteger(attempt.sendCount) && attempt.sendCount >= 0 ? total + attempt.sendCount : total
|
|
210
|
+
), 0);
|
|
211
|
+
physicalSends[protocolIndex]! += sends;
|
|
212
|
+
|
|
213
|
+
// Counted from the cause the recorder derived, not re-derived here. Two derivations of one
|
|
214
|
+
// answer is the disagreement this batch exists to remove, and the recorder is the only
|
|
215
|
+
// place that sees the transport facts a cause needs.
|
|
216
|
+
if (fact.failureCause !== undefined) {
|
|
217
|
+
failureCauses[protocolIndex]![failureCauseCell(fact.failureCause)]! += 1;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
for (const attempt of attempts ?? []) {
|
|
221
|
+
for (const kind of new Set(attempt.recoveryKinds)) {
|
|
222
|
+
recoveries[protocolIndex]![recoveryCell(recoveryClass(kind))]! += 1;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
observeHistogram(durations[protocolIndex]![resultIndex]!, REQUEST_DURATION_BUCKETS_SECONDS, fact.durationMs / 1000);
|
|
227
|
+
if (typeof fact.firstOutputMs === "number" && Number.isFinite(fact.firstOutputMs) && fact.firstOutputMs >= 0) {
|
|
228
|
+
observeHistogram(ttft[protocolIndex]![resultIndex]!, REQUEST_TTFT_BUCKETS_SECONDS, fact.firstOutputMs / 1000);
|
|
229
|
+
} else {
|
|
230
|
+
missingTtft[protocolIndex]![resultIndex]! += 1;
|
|
231
|
+
}
|
|
232
|
+
},
|
|
233
|
+
|
|
234
|
+
snapshot(): string {
|
|
235
|
+
const lines: string[] = [
|
|
236
|
+
"# HELP opencodex_logical_requests_total Finalized logical requests in this process.",
|
|
237
|
+
"# TYPE opencodex_logical_requests_total counter",
|
|
238
|
+
];
|
|
239
|
+
for (const protocol of REQUEST_METRICS_PROTOCOLS) {
|
|
240
|
+
for (const result of REQUEST_METRICS_RESULTS) {
|
|
241
|
+
lines.push(`opencodex_logical_requests_total${sampleLabels(protocol, result)} ${logicalRequests[protocolCell(protocol)]![resultCell(result)]}`);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
lines.push(
|
|
245
|
+
"# HELP opencodex_physical_sends_total Upstream sends made by finalized logical requests in this process.",
|
|
246
|
+
"# TYPE opencodex_physical_sends_total counter",
|
|
247
|
+
);
|
|
248
|
+
for (const protocol of REQUEST_METRICS_PROTOCOLS) {
|
|
249
|
+
lines.push(`opencodex_physical_sends_total${sampleLabels(protocol)} ${physicalSends[protocolCell(protocol)]}`);
|
|
250
|
+
}
|
|
251
|
+
lines.push(
|
|
252
|
+
"# HELP opencodex_recoveries_total Distinct recovery kinds observed per physical attempt in this process.",
|
|
253
|
+
"# TYPE opencodex_recoveries_total counter",
|
|
254
|
+
);
|
|
255
|
+
for (const protocol of REQUEST_METRICS_PROTOCOLS) {
|
|
256
|
+
for (const recovery of REQUEST_METRICS_RECOVERY_CLASSES) {
|
|
257
|
+
lines.push(`opencodex_recoveries_total{protocol="${protocol}",recovery="${recovery}"} ${recoveries[protocolCell(protocol)]![recoveryCell(recovery)]}`);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
lines.push(
|
|
261
|
+
"# HELP opencodex_request_failures_total Finalized logical requests that did not deliver an answer, by derived cause.",
|
|
262
|
+
"# TYPE opencodex_request_failures_total counter",
|
|
263
|
+
);
|
|
264
|
+
for (const protocol of REQUEST_METRICS_PROTOCOLS) {
|
|
265
|
+
for (const cause of REQUEST_METRICS_FAILURE_CAUSES) {
|
|
266
|
+
lines.push(`opencodex_request_failures_total{protocol="${protocol}",cause="${cause}"} ${failureCauses[protocolCell(protocol)]![failureCauseCell(cause)]}`);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
appendHistogram(lines, "opencodex_request_duration_seconds", "Finalized logical request duration in seconds.", durations, REQUEST_DURATION_BUCKETS_SECONDS);
|
|
270
|
+
appendHistogram(lines, "opencodex_ttft_seconds", "Observed time to first output in seconds.", ttft, REQUEST_TTFT_BUCKETS_SECONDS);
|
|
271
|
+
lines.push(
|
|
272
|
+
"# HELP opencodex_ttft_missing_total Finalized logical requests without an observed time to first output.",
|
|
273
|
+
"# TYPE opencodex_ttft_missing_total counter",
|
|
274
|
+
);
|
|
275
|
+
for (const protocol of REQUEST_METRICS_PROTOCOLS) {
|
|
276
|
+
for (const result of REQUEST_METRICS_RESULTS) {
|
|
277
|
+
lines.push(`opencodex_ttft_missing_total${sampleLabels(protocol, result)} ${missingTtft[protocolCell(protocol)]![resultCell(result)]}`);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
lines.push(
|
|
281
|
+
"# HELP opencodex_metrics_process_start_time_seconds Unix time when this process metrics owner started.",
|
|
282
|
+
"# TYPE opencodex_metrics_process_start_time_seconds gauge",
|
|
283
|
+
`opencodex_metrics_process_start_time_seconds ${processStartTimeSeconds}`,
|
|
284
|
+
);
|
|
285
|
+
return `${lines.join("\n")}\n`;
|
|
286
|
+
},
|
|
287
|
+
|
|
288
|
+
resetForTests(): void {
|
|
289
|
+
logicalRequests = matrix(REQUEST_METRICS_PROTOCOLS.length, REQUEST_METRICS_RESULTS.length);
|
|
290
|
+
physicalSends = Array.from({ length: REQUEST_METRICS_PROTOCOLS.length }, () => 0);
|
|
291
|
+
recoveries = matrix(REQUEST_METRICS_PROTOCOLS.length, REQUEST_METRICS_RECOVERY_CLASSES.length);
|
|
292
|
+
failureCauses = matrix(REQUEST_METRICS_PROTOCOLS.length, REQUEST_METRICS_FAILURE_CAUSES.length);
|
|
293
|
+
durations = histograms(REQUEST_DURATION_BUCKETS_SECONDS);
|
|
294
|
+
ttft = histograms(REQUEST_TTFT_BUCKETS_SECONDS);
|
|
295
|
+
missingTtft = matrix(REQUEST_METRICS_PROTOCOLS.length, REQUEST_METRICS_RESULTS.length);
|
|
296
|
+
},
|
|
297
|
+
};
|
|
298
|
+
}
|
|
@@ -338,7 +338,7 @@ export function createAdapterContinuations(
|
|
|
338
338
|
route.provider = rotated;
|
|
339
339
|
invalidateSameTargetRequest();
|
|
340
340
|
transportState.activeAdapter = resolveSelectionAdapter(
|
|
341
|
-
resolveWireProtocolOverride(route.providerName, route.modelId, route.provider, inboundWire),
|
|
341
|
+
resolveWireProtocolOverride(route.providerName, route.modelId, route.provider, inboundWire, route.staticPolicy),
|
|
342
342
|
config.cacheRetention,
|
|
343
343
|
);
|
|
344
344
|
bindRouteReasoningReplayScope({
|
|
@@ -384,7 +384,7 @@ export function createAdapterContinuations(
|
|
|
384
384
|
invalidateSameTargetRequest();
|
|
385
385
|
logCtx.provider = formatAnthropicProviderForLog("anthropic", admitted.accountId, config);
|
|
386
386
|
transportState.activeAdapter = resolveSelectionAdapter(
|
|
387
|
-
resolveWireProtocolOverride(route.providerName, route.modelId, route.provider, inboundWire),
|
|
387
|
+
resolveWireProtocolOverride(route.providerName, route.modelId, route.provider, inboundWire, route.staticPolicy),
|
|
388
388
|
config.cacheRetention,
|
|
389
389
|
);
|
|
390
390
|
sealRequestAttemptIdentity(logCtx.activeAttempt, logCtx.provider, transportState.activeAdapter.name, logCtx.accountLogLabel);
|
|
@@ -449,7 +449,7 @@ export function createAdapterContinuations(
|
|
|
449
449
|
if (applied) {
|
|
450
450
|
invalidateSameTargetRequest();
|
|
451
451
|
transportState.activeAdapter = resolveSelectionAdapter(
|
|
452
|
-
resolveWireProtocolOverride(route.providerName, route.modelId, route.provider, inboundWire),
|
|
452
|
+
resolveWireProtocolOverride(route.providerName, route.modelId, route.provider, inboundWire, route.staticPolicy),
|
|
453
453
|
config.cacheRetention,
|
|
454
454
|
);
|
|
455
455
|
bindRouteReasoningReplayScope({
|
|
@@ -246,7 +246,11 @@ export async function prepareAdapterExchange(
|
|
|
246
246
|
return new Response(JSON.stringify(json), { headers: { "Content-Type": "application/json" } });
|
|
247
247
|
}
|
|
248
248
|
try {
|
|
249
|
-
initialRequest = await transportState.activeAdapter.buildRequest(parsed, {
|
|
249
|
+
initialRequest = await transportState.activeAdapter.buildRequest(parsed, {
|
|
250
|
+
headers: requestState.selectedForwardHeaders,
|
|
251
|
+
translatorBudget,
|
|
252
|
+
abortSignal: upstream.signal,
|
|
253
|
+
});
|
|
250
254
|
refreshRequestToolAliases(initialRequest);
|
|
251
255
|
recordAdapterReasoning(logCtx, initialRequest);
|
|
252
256
|
recordAdapterTier(logCtx, initialRequest);
|
|
@@ -405,6 +409,7 @@ export async function prepareAdapterExchange(
|
|
|
405
409
|
retryRequest = await transportState.activeAdapter.buildRequest(parsed, {
|
|
406
410
|
headers: requestState.selectedForwardHeaders,
|
|
407
411
|
translatorBudget,
|
|
412
|
+
abortSignal: upstream.signal,
|
|
408
413
|
...(transportState.imageTierBias > 0 ? { imageTierBias: transportState.imageTierBias } : {}),
|
|
409
414
|
});
|
|
410
415
|
recordAdapterReasoning(logCtx, retryRequest);
|
|
@@ -581,7 +586,7 @@ export async function prepareAdapterExchange(
|
|
|
581
586
|
route.provider = refreshedProvider;
|
|
582
587
|
invalidateSameTargetRequest();
|
|
583
588
|
transportState.activeAdapter = resolveSelectionAdapter(
|
|
584
|
-
resolveWireProtocolOverride(route.providerName, route.modelId, refreshedProvider, inboundWire),
|
|
589
|
+
resolveWireProtocolOverride(route.providerName, route.modelId, refreshedProvider, inboundWire, route.staticPolicy),
|
|
585
590
|
config.cacheRetention,
|
|
586
591
|
);
|
|
587
592
|
bindRouteReasoningReplayScope({
|
|
@@ -614,7 +619,7 @@ export async function prepareAdapterExchange(
|
|
|
614
619
|
route.provider = rotated;
|
|
615
620
|
invalidateSameTargetRequest();
|
|
616
621
|
transportState.activeAdapter = resolveSelectionAdapter(
|
|
617
|
-
resolveWireProtocolOverride(route.providerName, route.modelId, route.provider, inboundWire),
|
|
622
|
+
resolveWireProtocolOverride(route.providerName, route.modelId, route.provider, inboundWire, route.staticPolicy),
|
|
618
623
|
config.cacheRetention,
|
|
619
624
|
);
|
|
620
625
|
bindRouteReasoningReplayScope({
|
|
@@ -714,7 +719,7 @@ export async function prepareAdapterExchange(
|
|
|
714
719
|
route.provider = rotated;
|
|
715
720
|
invalidateSameTargetRequest();
|
|
716
721
|
transportState.activeAdapter = resolveSelectionAdapter(
|
|
717
|
-
resolveWireProtocolOverride(route.providerName, route.modelId, route.provider, inboundWire),
|
|
722
|
+
resolveWireProtocolOverride(route.providerName, route.modelId, route.provider, inboundWire, route.staticPolicy),
|
|
718
723
|
config.cacheRetention,
|
|
719
724
|
);
|
|
720
725
|
bindRouteReasoningReplayScope({
|
|
@@ -757,7 +762,7 @@ export async function prepareAdapterExchange(
|
|
|
757
762
|
invalidateSameTargetRequest();
|
|
758
763
|
logCtx.provider = formatAnthropicProviderForLog("anthropic", admitted.accountId, config);
|
|
759
764
|
transportState.activeAdapter = resolveSelectionAdapter(
|
|
760
|
-
resolveWireProtocolOverride(route.providerName, route.modelId, route.provider, inboundWire),
|
|
765
|
+
resolveWireProtocolOverride(route.providerName, route.modelId, route.provider, inboundWire, route.staticPolicy),
|
|
761
766
|
config.cacheRetention,
|
|
762
767
|
);
|
|
763
768
|
sealRequestAttemptIdentity(logCtx.activeAttempt, logCtx.provider, transportState.activeAdapter.name, logCtx.accountLogLabel);
|
|
@@ -832,7 +837,7 @@ export async function prepareAdapterExchange(
|
|
|
832
837
|
}
|
|
833
838
|
invalidateSameTargetRequest();
|
|
834
839
|
transportState.activeAdapter = resolveSelectionAdapter(
|
|
835
|
-
resolveWireProtocolOverride(route.providerName, route.modelId, route.provider, inboundWire),
|
|
840
|
+
resolveWireProtocolOverride(route.providerName, route.modelId, route.provider, inboundWire, route.staticPolicy),
|
|
836
841
|
config.cacheRetention,
|
|
837
842
|
);
|
|
838
843
|
sealRequestAttemptIdentity(logCtx.activeAttempt, logCtx.provider, transportState.activeAdapter.name, logCtx.accountLogLabel);
|
|
@@ -4,7 +4,8 @@ import {
|
|
|
4
4
|
UPSTREAM_CLOSED_BEFORE_RESPONSE_CODE,
|
|
5
5
|
UPSTREAM_NO_RESPONSE_CODE,
|
|
6
6
|
} from "../../lib/upstream-retry";
|
|
7
|
-
import {
|
|
7
|
+
import type { RequestFailureCause, RequestFailureStage } from "../../lib/request-failure-model";
|
|
8
|
+
import { packageVersion } from "../../lib/package-version";
|
|
8
9
|
// If the 101 never arrives (network black hole), give SSE a chance well before
|
|
9
10
|
// the caller's connect timeout (default 200s) would fire.
|
|
10
11
|
export const UPGRADE_DEADLINE_MS = 10_000;
|
|
@@ -72,13 +73,7 @@ export function markCodexWsResponse(response: Response, observed: boolean): void
|
|
|
72
73
|
* importing management-api from the transport layer would invert the
|
|
73
74
|
* layering and pull the management surface into every WS exchange.
|
|
74
75
|
*/
|
|
75
|
-
const OCX_VERSION = (
|
|
76
|
-
try {
|
|
77
|
-
return JSON.parse(readFileSync(new URL("../../../package.json", import.meta.url), "utf8")).version as string;
|
|
78
|
-
} catch {
|
|
79
|
-
return "0.0.0";
|
|
80
|
-
}
|
|
81
|
-
})();
|
|
76
|
+
const OCX_VERSION = packageVersion("0.0.0");
|
|
82
77
|
|
|
83
78
|
/**
|
|
84
79
|
* The durable form of the stage counters, carried out of the exchange on the
|
|
@@ -210,6 +205,37 @@ export function classifyCodexWsFailure(stage: CodexWsFailureStage): CodexWsFailu
|
|
|
210
205
|
return "no-response-event";
|
|
211
206
|
}
|
|
212
207
|
|
|
208
|
+
/**
|
|
209
|
+
* The same four outcomes said in the shared stage-and-cause vocabulary (#4191).
|
|
210
|
+
*
|
|
211
|
+
* A projection, not a second classifier: {@link classifyCodexWsFailure} stays the one place that
|
|
212
|
+
* reads the counters, and this only restates its answer in the words the durable log, the metrics
|
|
213
|
+
* projection and the HTTP path already use. Without it the WebSocket transport is the one surface
|
|
214
|
+
* whose failures cannot be compared with anything else, which is the reported symptom -- every
|
|
215
|
+
* such failure reached the user as one of two bare sentences.
|
|
216
|
+
*
|
|
217
|
+
* It does not relax the transport's own rule. The no-replay-after-send contract in
|
|
218
|
+
* `codex-ws-exchange.ts` holds regardless of what this returns, and the stage below is
|
|
219
|
+
* deliberately not consulted as a fallback-eligibility signal; it reports where the exchange got
|
|
220
|
+
* to, and `resendPermission` happens to agree that everything past `before-send` is refused.
|
|
221
|
+
*/
|
|
222
|
+
export const CODEX_WS_FAILURE_PROJECTION = {
|
|
223
|
+
/** The create frame never left, so the origin provably never saw this turn. */
|
|
224
|
+
"before-send": { stage: "pre-header", cause: "transport-unsent" },
|
|
225
|
+
/** The frame left and the socket said nothing at all. The turn may be running upstream. */
|
|
226
|
+
"no-upstream-frame": { stage: "pre-header", cause: "transport-ambiguous" },
|
|
227
|
+
/** Control frames only: the peer is alive and answered, but no Responses event arrived. */
|
|
228
|
+
"no-response-event": { stage: "protocol-prelude", cause: "transport-ambiguous" },
|
|
229
|
+
/** Events already reached the caller, so a resend would duplicate output they have seen. */
|
|
230
|
+
"after-response-started": { stage: "semantic-output", cause: "transport-ambiguous" },
|
|
231
|
+
} as const satisfies Record<CodexWsFailureCause, { stage: RequestFailureStage; cause: RequestFailureCause }>;
|
|
232
|
+
|
|
233
|
+
export function projectCodexWsFailure(
|
|
234
|
+
stage: CodexWsFailureStage,
|
|
235
|
+
): { stage: RequestFailureStage; cause: RequestFailureCause } {
|
|
236
|
+
return CODEX_WS_FAILURE_PROJECTION[classifyCodexWsFailure(stage)];
|
|
237
|
+
}
|
|
238
|
+
|
|
213
239
|
/**
|
|
214
240
|
* Render the stage as a suffix appended to an existing failure message.
|
|
215
241
|
*
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { ResponsesTerminalStatus } from "../../bridge";
|
|
2
2
|
import { comboFailureDecision } from "../../combos";
|
|
3
3
|
import { httpStatusFromTerminalError } from "../../lib/errors";
|
|
4
|
+
import type { RequestFailureStage } from "../../lib/request-failure-model";
|
|
4
5
|
import type { RequestLogContext } from "../request-log";
|
|
5
6
|
import { createSseInspector } from "../relay";
|
|
6
7
|
import { MAX_CLIENT_SSE_FRAME_BYTES } from "../sse-frame-buffer";
|
|
@@ -108,9 +109,48 @@ export function comboStreamPayloadCommitsOutput(payload: unknown): boolean {
|
|
|
108
109
|
if (!payload || typeof payload !== "object" || Array.isArray(payload)) return true;
|
|
109
110
|
const type = (payload as { type?: unknown }).type;
|
|
110
111
|
if (typeof type !== "string") return true;
|
|
112
|
+
if (type === "response.created") {
|
|
113
|
+
// A created event is a control frame only while its snapshot is empty. An origin that
|
|
114
|
+
// resumes a turn can put completed items in it, and treating that as a prelude would let
|
|
115
|
+
// a replacement re-emit output the caller already received.
|
|
116
|
+
const response = (payload as { response?: unknown }).response;
|
|
117
|
+
if (response && typeof response === "object" && !Array.isArray(response)) {
|
|
118
|
+
const output = (response as { output?: unknown }).output;
|
|
119
|
+
if (Array.isArray(output) && output.length > 0) return true;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
111
122
|
return !PRE_OUTPUT_CONTROL_EVENTS.has(type) && !TERMINAL_EVENTS.has(type);
|
|
112
123
|
}
|
|
113
124
|
|
|
125
|
+
/**
|
|
126
|
+
* How far this SSE body got, in the vocabulary of src/lib/request-failure-model.ts.
|
|
127
|
+
*
|
|
128
|
+
* The preflight cannot separate `semantic-output` from `side-effect`: it classifies any event
|
|
129
|
+
* that is not a lifecycle control frame as committing, without reading item types. Both stages
|
|
130
|
+
* refuse a resend, so the distinction would change no decision -- it is named here so a later
|
|
131
|
+
* reader does not mistake the collapse for an omission.
|
|
132
|
+
*
|
|
133
|
+
* A terminal that settled carrying no output is `protocol-prelude`, not `terminal`. That is
|
|
134
|
+
* the failure model's own rule: `terminal` means the answer was delivered, and an empty
|
|
135
|
+
* completion delivered none.
|
|
136
|
+
*
|
|
137
|
+
* Only the two nothing-observed stages actually reach a read error today: the loop below hands
|
|
138
|
+
* the body back as `accepted` the moment output commits or a terminal arrives, so a stream
|
|
139
|
+
* that committed anything never reports a stage at all. The committed branches stay because
|
|
140
|
+
* this has to be total for any other caller, and because a later change to that loop must not
|
|
141
|
+
* be able to promote a committed stream into a replaceable one by omission.
|
|
142
|
+
*/
|
|
143
|
+
function observedResponsesStage(state: {
|
|
144
|
+
readonly outputCommitted: boolean;
|
|
145
|
+
readonly terminalStatus: ResponsesTerminalStatus | undefined;
|
|
146
|
+
readonly responseCreated: boolean;
|
|
147
|
+
}): RequestFailureStage {
|
|
148
|
+
if (state.outputCommitted) return "semantic-output";
|
|
149
|
+
if (state.terminalStatus === "completed") return "terminal";
|
|
150
|
+
if (state.responseCreated || state.terminalStatus !== undefined) return "protocol-prelude";
|
|
151
|
+
return "headers-only";
|
|
152
|
+
}
|
|
153
|
+
|
|
114
154
|
function replayBufferedResponse(
|
|
115
155
|
response: Response,
|
|
116
156
|
reader: ReadableStreamDefaultReader<Uint8Array>,
|
|
@@ -186,13 +226,22 @@ function failedTerminalResponse(
|
|
|
186
226
|
|
|
187
227
|
export type ComboStreamPreflightResult =
|
|
188
228
|
| { kind: "accepted"; response: Response }
|
|
189
|
-
| { kind: "failed"; response: Response }
|
|
229
|
+
| { kind: "failed"; response: Response }
|
|
230
|
+
/**
|
|
231
|
+
* The body errored mid-stream and `replayReadErrors` asked for the prefix back rather than
|
|
232
|
+
* a rethrow. `stage` is how far the inspection actually got; whether that permits a
|
|
233
|
+
* replacement is the resend gate's decision, not this function's. Callers that only act on
|
|
234
|
+
* a projected terminal can treat this exactly as `accepted`, which is what it was before
|
|
235
|
+
* the stage became observable.
|
|
236
|
+
*/
|
|
237
|
+
| { kind: "read-error"; response: Response; error: unknown; stage: RequestFailureStage };
|
|
190
238
|
|
|
191
239
|
/**
|
|
192
|
-
* Buffer a
|
|
193
|
-
*
|
|
194
|
-
*
|
|
195
|
-
*
|
|
240
|
+
* Buffer a Responses SSE only until the request becomes unsafe to replay or reaches a
|
|
241
|
+
* terminal. Combo failover and native post-header reset recovery share this protocol
|
|
242
|
+
* boundary, because they are asking the same question about the same bytes. This owns exactly
|
|
243
|
+
* one body reader. The aggregate buffer is capped by bytes and retained chunks; hitting either
|
|
244
|
+
* cap commits the current target instead of growing memory or guessing that replay is safe.
|
|
196
245
|
*/
|
|
197
246
|
export async function preflightComboStreamResponse(
|
|
198
247
|
response: Response,
|
|
@@ -211,12 +260,18 @@ export async function preflightComboStreamResponse(
|
|
|
211
260
|
const buffered: Uint8Array[] = [];
|
|
212
261
|
let bufferedBytes = 0;
|
|
213
262
|
let outputCommitted = false;
|
|
263
|
+
let responseCreated = false;
|
|
214
264
|
let terminalStatus: ResponsesTerminalStatus | undefined;
|
|
215
265
|
let retryableTerminalPayload: Record<string, unknown> | undefined;
|
|
216
266
|
const inspector = createSseInspector({
|
|
217
267
|
logCtx,
|
|
268
|
+
// A payload the inspector could not parse still reached this proxy, and it may be output.
|
|
269
|
+
// Committing on it is what keeps an unreadable frame from reading as an empty prelude.
|
|
270
|
+
onOpaquePayload: () => { outputCommitted = true; },
|
|
218
271
|
onParsedPayload: payload => {
|
|
219
272
|
if (terminalStatus !== undefined || outputCommitted || retryableTerminalPayload) return;
|
|
273
|
+
if (payload !== null && typeof payload === "object" && !Array.isArray(payload)
|
|
274
|
+
&& (payload as { type?: unknown }).type === "response.created") responseCreated = true;
|
|
220
275
|
const retryable = retryableTerminal(payload);
|
|
221
276
|
const matchedBareError = retryable && payload !== null && typeof payload === "object"
|
|
222
277
|
&& !Array.isArray(payload) && (payload as { type?: unknown }).type === "error";
|
|
@@ -239,7 +294,9 @@ export async function preflightComboStreamResponse(
|
|
|
239
294
|
// The native relay still owns post-header transport failures. Preserve
|
|
240
295
|
// the bounded prefix and the errored reader; cancelling it here would
|
|
241
296
|
// erase the failure before either client relay or inspection sees it.
|
|
242
|
-
|
|
297
|
+
const replay = replayBufferedResponse(response, reader, buffered);
|
|
298
|
+
const stage = observedResponsesStage({ outputCommitted, terminalStatus, responseCreated });
|
|
299
|
+
return { kind: "read-error", response: replay, error, stage };
|
|
243
300
|
}
|
|
244
301
|
if (next.done) {
|
|
245
302
|
inspector.finish();
|
|
@@ -277,3 +334,108 @@ export async function preflightComboStreamResponse(
|
|
|
277
334
|
inspector.dispose();
|
|
278
335
|
}
|
|
279
336
|
}
|
|
337
|
+
|
|
338
|
+
/** Produce a replacement body for a mid-stream failure at `stage`, or null to keep the error. */
|
|
339
|
+
export type ProtocolSafeResetRecovery = (
|
|
340
|
+
error: unknown,
|
|
341
|
+
stage: RequestFailureStage,
|
|
342
|
+
) => Promise<Response | null>;
|
|
343
|
+
|
|
344
|
+
/**
|
|
345
|
+
* Defer protocol inspection until the downstream actually pulls the body.
|
|
346
|
+
*
|
|
347
|
+
* Direct passthrough must return response headers before the first SSE event arrives, so the
|
|
348
|
+
* inspection cannot be awaited at the dispatch site the way combo routing awaits it. Wrapping
|
|
349
|
+
* the body moves it to the first pull, which is the earliest moment the client is willing to
|
|
350
|
+
* wait anyway.
|
|
351
|
+
*/
|
|
352
|
+
export function deferProtocolSafeResetRecovery(
|
|
353
|
+
response: Response,
|
|
354
|
+
logCtx: RequestLogContext,
|
|
355
|
+
recover: ProtocolSafeResetRecovery,
|
|
356
|
+
options?: { allowMissingContentType?: boolean },
|
|
357
|
+
): Response {
|
|
358
|
+
if (!response.body) return response;
|
|
359
|
+
|
|
360
|
+
let reader: ReadableStreamDefaultReader<Uint8Array> | undefined;
|
|
361
|
+
let initialization: Promise<void> | undefined;
|
|
362
|
+
let closed = false;
|
|
363
|
+
|
|
364
|
+
const cancelBody = (body: ReadableStream<Uint8Array> | null, reason?: unknown): void => {
|
|
365
|
+
try { void body?.cancel(reason).catch(() => {}); } catch { /* already locked or closed */ }
|
|
366
|
+
};
|
|
367
|
+
const initialize = async (): Promise<void> => {
|
|
368
|
+
const preflight = await preflightComboStreamResponse(
|
|
369
|
+
response,
|
|
370
|
+
logCtx,
|
|
371
|
+
() => false,
|
|
372
|
+
{ allowMissingContentType: options?.allowMissingContentType === true, replayReadErrors: true },
|
|
373
|
+
);
|
|
374
|
+
let selected = preflight.response;
|
|
375
|
+
if (preflight.kind === "read-error") {
|
|
376
|
+
const replacement = await recover(preflight.error, preflight.stage);
|
|
377
|
+
if (replacement) {
|
|
378
|
+
cancelBody(selected.body, "using protocol-safe replacement stream");
|
|
379
|
+
selected = replacement;
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
if (closed) {
|
|
383
|
+
cancelBody(selected.body, "downstream cancelled before protocol preflight completed");
|
|
384
|
+
return;
|
|
385
|
+
}
|
|
386
|
+
reader = selected.body?.getReader();
|
|
387
|
+
};
|
|
388
|
+
|
|
389
|
+
const body = new ReadableStream<Uint8Array>({
|
|
390
|
+
async pull(controller) {
|
|
391
|
+
try {
|
|
392
|
+
initialization ??= initialize();
|
|
393
|
+
await initialization;
|
|
394
|
+
if (closed) return;
|
|
395
|
+
if (!reader) {
|
|
396
|
+
closed = true;
|
|
397
|
+
controller.close();
|
|
398
|
+
return;
|
|
399
|
+
}
|
|
400
|
+
const next = await reader.read();
|
|
401
|
+
if (closed) return;
|
|
402
|
+
if (next.done) {
|
|
403
|
+
closed = true;
|
|
404
|
+
try { reader.releaseLock(); } catch { /* already released */ }
|
|
405
|
+
reader = undefined;
|
|
406
|
+
controller.close();
|
|
407
|
+
return;
|
|
408
|
+
}
|
|
409
|
+
controller.enqueue(next.value);
|
|
410
|
+
} catch (error) {
|
|
411
|
+
if (closed) return;
|
|
412
|
+
closed = true;
|
|
413
|
+
try { reader?.releaseLock(); } catch { /* errored reader */ }
|
|
414
|
+
reader = undefined;
|
|
415
|
+
controller.error(error);
|
|
416
|
+
}
|
|
417
|
+
},
|
|
418
|
+
cancel(reason) {
|
|
419
|
+
if (closed) return;
|
|
420
|
+
closed = true;
|
|
421
|
+
if (reader) {
|
|
422
|
+
try { void reader.cancel(reason).catch(() => {}); } catch { /* already closed */ }
|
|
423
|
+
try { reader.releaseLock(); } catch { /* already released */ }
|
|
424
|
+
reader = undefined;
|
|
425
|
+
} else if (initialization === undefined) {
|
|
426
|
+
// Nothing has read the upstream yet, so this body is still ours to cancel.
|
|
427
|
+
cancelBody(response.body, reason);
|
|
428
|
+
}
|
|
429
|
+
// A cancel while the preflight is mid-flight falls through deliberately. That body is
|
|
430
|
+
// locked by the preflight's own reader, so cancelling it here would reject and be
|
|
431
|
+
// swallowed; `initialize` sees `closed` when it settles and releases whichever body it
|
|
432
|
+
// ended up selecting, which is the one that actually has to be let go.
|
|
433
|
+
},
|
|
434
|
+
}, { highWaterMark: 0 });
|
|
435
|
+
|
|
436
|
+
return new Response(body, {
|
|
437
|
+
status: response.status,
|
|
438
|
+
statusText: response.statusText,
|
|
439
|
+
headers: response.headers,
|
|
440
|
+
});
|
|
441
|
+
}
|