@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
package/src/server/relay.ts
CHANGED
|
@@ -5,10 +5,14 @@ import {
|
|
|
5
5
|
CYBER_POLICY_FALLBACK_MESSAGE,
|
|
6
6
|
isCyberPolicyCode,
|
|
7
7
|
isCyberPolicyMessage,
|
|
8
|
+
isTerminalRefusalCode,
|
|
9
|
+
safetyRefusalCodeFromMessage,
|
|
10
|
+
terminalRefusalFallbackMessage,
|
|
8
11
|
upstreamErrorMessageFromPayload,
|
|
9
12
|
} from "../lib/errors";
|
|
10
13
|
import { redactSecretString } from "../lib/redact";
|
|
11
14
|
import { isTranslatorBudgetExceededError } from "../lib/translator-budget";
|
|
15
|
+
import { carryReplayRefusal } from "../lib/upstream-retry";
|
|
12
16
|
import { isUsageDebugEnabled } from "../usage/debug";
|
|
13
17
|
import {
|
|
14
18
|
addRequestLog,
|
|
@@ -150,16 +154,61 @@ export function failedTailFrame(encoder: TextEncoder, err: unknown): Uint8Array
|
|
|
150
154
|
return encoder.encode(`\n\nevent: response.failed\ndata: ${payload}\n\n${DONE_SSE_FRAME_TEXT}`);
|
|
151
155
|
}
|
|
152
156
|
|
|
153
|
-
|
|
157
|
+
/**
|
|
158
|
+
* Close a turn the upstream ended without a Responses terminal.
|
|
159
|
+
*
|
|
160
|
+
* `refusalCode` carries the upstream's own verdict when it gave one. Codex
|
|
161
|
+
* classifies this terminal by `error.code` alone and retries everything outside
|
|
162
|
+
* its fatal set (codex-rs/codex-api/src/sse/responses.rs:423-450), so stamping
|
|
163
|
+
* `upstream_server_error` on a refusal delivered it as a retryable disconnect
|
|
164
|
+
* and drove the reconnect loop in #5176. Without a refusal code the terminal is
|
|
165
|
+
* unchanged: a genuine transport failure stays retryable, which is what it is.
|
|
166
|
+
*/
|
|
167
|
+
export function upstreamErrorTailFrame(
|
|
168
|
+
encoder: TextEncoder,
|
|
169
|
+
message: string,
|
|
170
|
+
refusalCode?: string,
|
|
171
|
+
): Uint8Array {
|
|
172
|
+
return encoder.encode(
|
|
173
|
+
`event: response.failed\ndata: ${upstreamErrorFailedPayload(message, refusalCode)}\n\n`,
|
|
174
|
+
);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function upstreamErrorFailedPayload(message: string, refusalCode?: string): string {
|
|
154
178
|
const error = {
|
|
155
|
-
type: "upstream_error",
|
|
156
|
-
code: "upstream_server_error",
|
|
179
|
+
type: refusalCode === undefined ? "upstream_error" : "invalid_request_error",
|
|
180
|
+
code: refusalCode ?? "upstream_server_error",
|
|
157
181
|
message: redactSecretString(message).slice(0, MAX_TAIL_ERROR_MESSAGE_CHARS),
|
|
158
182
|
};
|
|
159
|
-
return
|
|
183
|
+
return JSON.stringify({
|
|
160
184
|
type: "response.failed",
|
|
161
|
-
response: {
|
|
162
|
-
|
|
185
|
+
response: {
|
|
186
|
+
status: "failed",
|
|
187
|
+
error,
|
|
188
|
+
last_error: error,
|
|
189
|
+
...(refusalCode === undefined ? {} : { retryable: false }),
|
|
190
|
+
},
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Terminal for a read that failed after the upstream had already refused.
|
|
196
|
+
*
|
|
197
|
+
* Framed exactly like {@link failedTailFrame} — leading blank line to close a
|
|
198
|
+
* partial block, then the sentinel — but carrying the refusal instead of the
|
|
199
|
+
* generic reset. The refusal is the real outcome of the turn and the socket
|
|
200
|
+
* teardown that followed it is not, so reporting `upstream_reset` here would
|
|
201
|
+
* restart a turn the upstream has already ended (#5176).
|
|
202
|
+
*/
|
|
203
|
+
export function refusalFailedTailFrame(
|
|
204
|
+
encoder: TextEncoder,
|
|
205
|
+
message: string,
|
|
206
|
+
refusalCode: string,
|
|
207
|
+
): Uint8Array {
|
|
208
|
+
const payload = upstreamErrorFailedPayload(message, refusalCode);
|
|
209
|
+
return encoder.encode(
|
|
210
|
+
`\n\nevent: response.failed\ndata: ${payload}\n\n${DONE_SSE_FRAME_TEXT}`,
|
|
211
|
+
);
|
|
163
212
|
}
|
|
164
213
|
|
|
165
214
|
function boundedBareUpstreamErrorMessage(payload: unknown): string | undefined {
|
|
@@ -169,12 +218,57 @@ function boundedBareUpstreamErrorMessage(payload: unknown): string | undefined {
|
|
|
169
218
|
return message ? redactSecretString(message).slice(0, MAX_TAIL_ERROR_MESSAGE_CHARS) : undefined;
|
|
170
219
|
}
|
|
171
220
|
|
|
221
|
+
/**
|
|
222
|
+
* A bare upstream `error` event reduced to what the synthesized terminal needs.
|
|
223
|
+
*
|
|
224
|
+
* The structured code is authoritative whenever the upstream sent one: a code
|
|
225
|
+
* that is not a refusal means the upstream did not refuse, whatever its
|
|
226
|
+
* diagnostic text happens to quote. Recognized refusal copy is read only when
|
|
227
|
+
* no code was carried anywhere on the event, which is the shape #5176 reports.
|
|
228
|
+
* A refusal code with no message still yields a terminal, because Codex accepts
|
|
229
|
+
* that shape and supplies its own copy for it.
|
|
230
|
+
*
|
|
231
|
+
* The candidate topology mirrors {@link upstreamErrorMessageFromPayload}: code
|
|
232
|
+
* and message must be read from the same places, or an event whose message is
|
|
233
|
+
* nested under `response.error` would contribute text while its verdict went
|
|
234
|
+
* unseen.
|
|
235
|
+
*/
|
|
236
|
+
function boundedBareUpstreamError(payload: unknown): {
|
|
237
|
+
message: string;
|
|
238
|
+
refusalCode: string | undefined;
|
|
239
|
+
} | undefined {
|
|
240
|
+
const root = asJsonRecord(payload);
|
|
241
|
+
if (!root || root.type !== "error") return undefined;
|
|
242
|
+
const message = boundedBareUpstreamErrorMessage(payload);
|
|
243
|
+
const response = asJsonRecord(root.response);
|
|
244
|
+
// Precedence is {@link upstreamErrorMessageFromPayload}'s, so the envelope
|
|
245
|
+
// that supplied the message also supplies the verdict. Taking the FIRST code
|
|
246
|
+
// rather than searching for a refusal is what stops a refusal nested below a
|
|
247
|
+
// transient one from overruling it.
|
|
248
|
+
const code = [
|
|
249
|
+
asJsonRecord(root.error),
|
|
250
|
+
asJsonRecord(root.last_error),
|
|
251
|
+
asJsonRecord(response?.error),
|
|
252
|
+
asJsonRecord(response?.incomplete_details),
|
|
253
|
+
root,
|
|
254
|
+
]
|
|
255
|
+
.map(candidate => stringField(candidate, "code"))
|
|
256
|
+
.find(candidate => candidate !== undefined);
|
|
257
|
+
const refusalCode = code !== undefined
|
|
258
|
+
? (isTerminalRefusalCode(code) ? code : undefined)
|
|
259
|
+
: message === undefined ? undefined : safetyRefusalCodeFromMessage(message);
|
|
260
|
+
if (message !== undefined) return { message, refusalCode };
|
|
261
|
+
if (refusalCode === undefined) return undefined;
|
|
262
|
+
return { message: terminalRefusalFallbackMessage(refusalCode), refusalCode };
|
|
263
|
+
}
|
|
264
|
+
|
|
172
265
|
export type SseTerminalOutputBoundary = {
|
|
173
266
|
feed(chunk: Uint8Array): Uint8Array;
|
|
174
267
|
finish(): Uint8Array;
|
|
175
268
|
terminalSeen(): boolean;
|
|
176
269
|
doneSeen(): boolean;
|
|
177
270
|
upstreamError(): string | undefined;
|
|
271
|
+
upstreamRefusalCode(): string | undefined;
|
|
178
272
|
dispose(): void;
|
|
179
273
|
};
|
|
180
274
|
|
|
@@ -197,6 +291,7 @@ export function createSseTerminalOutputBoundary(
|
|
|
197
291
|
let pendingDone: { block: Uint8Array; delimiter: Uint8Array } | null = null;
|
|
198
292
|
let disposed = false;
|
|
199
293
|
let upstreamError: string | undefined;
|
|
294
|
+
let upstreamRefusalCode: string | undefined;
|
|
200
295
|
|
|
201
296
|
const processFrames = (
|
|
202
297
|
frames: ReturnType<BoundedSseFrameBuffer["feed"]>,
|
|
@@ -210,8 +305,11 @@ export function createSseTerminalOutputBoundary(
|
|
|
210
305
|
const parsed = payload === null ? undefined : parseSsePayload(payload);
|
|
211
306
|
// Observe on the client reader itself: a tee inspection branch may lag
|
|
212
307
|
// behind EOF, so its log context cannot determine the outgoing terminal.
|
|
213
|
-
const
|
|
214
|
-
if (
|
|
308
|
+
const bare = boundedBareUpstreamError(parsed);
|
|
309
|
+
if (bare !== undefined) {
|
|
310
|
+
upstreamError = bare.message;
|
|
311
|
+
upstreamRefusalCode = bare.refusalCode;
|
|
312
|
+
}
|
|
215
313
|
const safetyBuffering = dropSafetyBuffering && parsed !== undefined
|
|
216
314
|
? codexSafetyBufferingBlockAction(parsed) : "keep";
|
|
217
315
|
if (safetyBuffering === "drop") continue;
|
|
@@ -278,6 +376,7 @@ export function createSseTerminalOutputBoundary(
|
|
|
278
376
|
terminalSeen: () => terminal,
|
|
279
377
|
doneSeen: () => done,
|
|
280
378
|
upstreamError: () => upstreamError,
|
|
379
|
+
upstreamRefusalCode: () => upstreamRefusalCode,
|
|
281
380
|
dispose() {
|
|
282
381
|
if (disposed) return;
|
|
283
382
|
disposed = true;
|
|
@@ -349,7 +448,11 @@ export function relaySseWithFailedTail(
|
|
|
349
448
|
const upstreamError = terminalBoundary.upstreamError() ?? opts?.upstreamError;
|
|
350
449
|
controller.enqueue(upstreamError === undefined
|
|
351
450
|
? adapterEofIncompleteFrame(encoder)
|
|
352
|
-
: upstreamErrorTailFrame(
|
|
451
|
+
: upstreamErrorTailFrame(
|
|
452
|
+
encoder,
|
|
453
|
+
upstreamError,
|
|
454
|
+
terminalBoundary.upstreamRefusalCode(),
|
|
455
|
+
));
|
|
353
456
|
controller.enqueue(doneFrame(encoder));
|
|
354
457
|
}
|
|
355
458
|
terminalBoundary.dispose();
|
|
@@ -378,7 +481,11 @@ export function relaySseWithFailedTail(
|
|
|
378
481
|
if (!terminalBoundary.doneSeen()) controller.enqueue(doneFrame(encoder));
|
|
379
482
|
} else {
|
|
380
483
|
// Leading blank line terminates a partial SSE block so the failed frame parses cleanly.
|
|
381
|
-
|
|
484
|
+
const refusalCode = terminalBoundary.upstreamRefusalCode();
|
|
485
|
+
const refusalMessage = terminalBoundary.upstreamError();
|
|
486
|
+
controller.enqueue(refusalCode !== undefined && refusalMessage !== undefined
|
|
487
|
+
? refusalFailedTailFrame(encoder, refusalMessage, refusalCode)
|
|
488
|
+
: failedTailFrame(encoder, err));
|
|
382
489
|
}
|
|
383
490
|
controller.close();
|
|
384
491
|
} catch { /* client already torn down */ }
|
|
@@ -720,15 +827,21 @@ export function responseWithDeferredRequestLog(
|
|
|
720
827
|
// convention for a client cancellation or upstream read failure.
|
|
721
828
|
const status = reason === "cancel" ? 499 : reason === "read_error" ? 502 : response.status;
|
|
722
829
|
addFinalRequestLog(requestId, start, logCtx, status, {
|
|
830
|
+
...(reason === "eof" && logCtx.observedTerminalStatus
|
|
831
|
+
? { terminalStatus: logCtx.observedTerminalStatus }
|
|
832
|
+
: {}),
|
|
723
833
|
closeReason: reason === "cancel" ? "client_cancel" : "non_stream",
|
|
724
834
|
}, addLog);
|
|
725
835
|
},
|
|
726
836
|
});
|
|
727
|
-
|
|
837
|
+
// Logging re-wraps the response, and an in-process verdict does not survive a re-wrap on
|
|
838
|
+
// its own. A replay refusal that lost it here would read to a later quota recorder or
|
|
839
|
+
// Retry-After synthesizer as a 429 some upstream produced.
|
|
840
|
+
return carryReplayRefusal(response, new Response(body, {
|
|
728
841
|
status: response.status,
|
|
729
842
|
statusText: response.statusText,
|
|
730
843
|
headers: response.headers,
|
|
731
|
-
});
|
|
844
|
+
}));
|
|
732
845
|
}
|
|
733
846
|
if (isUsageDebugEnabled() && logCtx.usageDebugBodyKind === undefined) {
|
|
734
847
|
logCtx.usageDebugBodyKind = response.body ? "other" : "none";
|
|
@@ -880,6 +993,14 @@ export type SseInspectorHandlers = {
|
|
|
880
993
|
* with an empty `output`.
|
|
881
994
|
*/
|
|
882
995
|
onParsedPayload?: (payload: unknown) => void;
|
|
996
|
+
/**
|
|
997
|
+
* A complete data payload that did not parse as a JSON event, `[DONE]` included.
|
|
998
|
+
*
|
|
999
|
+
* An inspector that only hears about parsed events cannot tell "nothing has been emitted"
|
|
1000
|
+
* from "something was emitted that I could not read", and a replay decision needs that
|
|
1001
|
+
* difference: an unreadable payload is a payload the caller may already have seen.
|
|
1002
|
+
*/
|
|
1003
|
+
onOpaquePayload?: () => void;
|
|
883
1004
|
onFirstOutput?: () => void;
|
|
884
1005
|
/**
|
|
885
1006
|
* Provider-scoped compatibility: persist the completed snapshot under the
|
|
@@ -1068,6 +1189,11 @@ export function createSseInspector(handlers: SseInspectorHandlers): SseInspector
|
|
|
1068
1189
|
if (handlers.onParsedPayload && parsed !== undefined) {
|
|
1069
1190
|
try { handlers.onParsedPayload(parsed); } catch { /* inspection must never throw into the pump */ }
|
|
1070
1191
|
}
|
|
1192
|
+
// The other half of the same observation. A payload that did not parse still reached the
|
|
1193
|
+
// caller, so a consumer deciding whether anything has been emitted has to hear about it.
|
|
1194
|
+
if (handlers.onOpaquePayload && parsed === undefined) {
|
|
1195
|
+
try { handlers.onOpaquePayload(); } catch { /* inspection must never throw into the pump */ }
|
|
1196
|
+
}
|
|
1071
1197
|
reportFirstOutput.parsed(parsed);
|
|
1072
1198
|
const status = terminalStatusFromParsed(parsed);
|
|
1073
1199
|
const policyTerminal = status === "failed"
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Where a request's failure attribution is derived and stamped.
|
|
3
|
+
*
|
|
4
|
+
* A sibling of `request-log.ts` rather than a section inside it. That file carries the whole
|
|
5
|
+
* request-logging surface and sits against the 2,000-line repository ceiling, which only ever
|
|
6
|
+
* moves down; this is the extraction that rule asks for, not a cap that was negotiated.
|
|
7
|
+
*
|
|
8
|
+
* It holds the two places a stage and cause are decided and written. Both read the recorder's
|
|
9
|
+
* own closed facts and neither reads `errorCode` or `upstreamError`, which are assembled partly
|
|
10
|
+
* from upstream text.
|
|
11
|
+
*/
|
|
12
|
+
import { deriveRequestFailureAttribution } from "../lib/request-failure-attribution";
|
|
13
|
+
import { causeForRecoveryKind } from "../lib/request-failure-model";
|
|
14
|
+
import type { AttemptRecoveryKind, PersistedUsageAttempt, RequestFailureCause, RequestFailureStage } from "../usage/log";
|
|
15
|
+
import type { ResponsesTerminalStatus } from "../bridge";
|
|
16
|
+
|
|
17
|
+
/** The subset of the log context this derivation may see. Narrow on purpose. */
|
|
18
|
+
export interface FinalRequestAttributionFacts {
|
|
19
|
+
readonly status: number;
|
|
20
|
+
readonly terminalStatus?: ResponsesTerminalStatus | undefined;
|
|
21
|
+
readonly closeReason?: "terminal" | "client_cancel" | "non_stream" | "body_stall" | "body_overflow" | undefined;
|
|
22
|
+
readonly transportPhase?: "pre_headers" | "mid_stream" | "terminal_sse" | undefined;
|
|
23
|
+
readonly terminalSource?: "upstream" | "synthetic" | undefined;
|
|
24
|
+
/** The REQUEST's first-output observation, not the final attempt's. See below. */
|
|
25
|
+
readonly outputObserved: boolean;
|
|
26
|
+
readonly locallyAnswered: boolean;
|
|
27
|
+
readonly attempt?: PersistedUsageAttempt | undefined;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface StampedAttribution {
|
|
31
|
+
failureStage?: RequestFailureStage;
|
|
32
|
+
failureCause?: RequestFailureCause;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Derive the attribution for a finished logical request and stamp the attempt that ended it.
|
|
37
|
+
*
|
|
38
|
+
* Called from the one seam every request passes exactly once, and BEFORE the attempt snapshot,
|
|
39
|
+
* so the row that reaches disk and the live attempt object carry the same pair. Deriving it at
|
|
40
|
+
* each transport's own exit would give the same request a different attribution per transport,
|
|
41
|
+
* which is the disagreement the shared terminal classifier already removed once.
|
|
42
|
+
*
|
|
43
|
+
* `outputObserved` is the request's observation rather than the final attempt's. A request that
|
|
44
|
+
* relayed output on its first attempt and then failed over has committed that output to the
|
|
45
|
+
* caller whatever the last attempt saw, so the logical row and the attempt that ended it carry
|
|
46
|
+
* the same answer. Reading the attempt-local value would produce a MORE permissive resend verdict
|
|
47
|
+
* for exactly that case, and a permission decision has to fail in the safe direction.
|
|
48
|
+
*/
|
|
49
|
+
export function attributeFinalRequest(facts: FinalRequestAttributionFacts): StampedAttribution {
|
|
50
|
+
const attribution = deriveRequestFailureAttribution({
|
|
51
|
+
status: facts.status,
|
|
52
|
+
...(facts.terminalStatus ? { terminalStatus: facts.terminalStatus } : {}),
|
|
53
|
+
...(facts.closeReason ? { closeReason: facts.closeReason } : {}),
|
|
54
|
+
...(facts.transportPhase ? { transportPhase: facts.transportPhase } : {}),
|
|
55
|
+
...(facts.terminalSource ? { terminalSource: facts.terminalSource } : {}),
|
|
56
|
+
...(facts.attempt?.streamAborted === true ? { streamAborted: true } : {}),
|
|
57
|
+
outputObserved: facts.outputObserved,
|
|
58
|
+
// The one fact that can raise a stage above `semantic-output`, and the reason it is counted
|
|
59
|
+
// at the transport rather than at the adapter: an emitted tool call the client never
|
|
60
|
+
// received has committed nothing, and a resend for it is still safe (#3983).
|
|
61
|
+
sideEffectObserved: (facts.attempt?.deliverySummary?.sideEffectEvents ?? 0) > 0,
|
|
62
|
+
locallyAnswered: facts.locallyAnswered,
|
|
63
|
+
recoveryKinds: facts.attempt?.recoveryKinds ?? [],
|
|
64
|
+
});
|
|
65
|
+
// The final row and the attempt that ended it describe the same exchange, so they carry the
|
|
66
|
+
// same pair rather than each deriving one from a different slice of the facts.
|
|
67
|
+
if (facts.attempt) {
|
|
68
|
+
if (attribution?.stage) facts.attempt.failureStage = attribution.stage;
|
|
69
|
+
else delete facts.attempt.failureStage;
|
|
70
|
+
if (attribution?.cause) facts.attempt.failureCause = attribution.cause;
|
|
71
|
+
else delete facts.attempt.failureCause;
|
|
72
|
+
}
|
|
73
|
+
return {
|
|
74
|
+
...(attribution?.stage ? { failureStage: attribution.stage } : {}),
|
|
75
|
+
...(attribution?.cause ? { failureCause: attribution.cause } : {}),
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Attribute an attempt being sealed because a named recovery rejected it.
|
|
81
|
+
*
|
|
82
|
+
* The recovery kind is direct evidence here rather than an inference from history: this attempt
|
|
83
|
+
* is ending precisely because that recovery was needed. Without it the sealed attempt would reach
|
|
84
|
+
* the ledger with no attribution at all, because the finalization seam only ever sees the last
|
|
85
|
+
* attempt of a request.
|
|
86
|
+
*/
|
|
87
|
+
export function attributeSealedAttempt(
|
|
88
|
+
attempt: PersistedUsageAttempt,
|
|
89
|
+
recovery: AttemptRecoveryKind | undefined,
|
|
90
|
+
): void {
|
|
91
|
+
const attribution = deriveRequestFailureAttribution({
|
|
92
|
+
status: attempt.status,
|
|
93
|
+
outputObserved: attempt.firstOutputMs !== undefined,
|
|
94
|
+
sideEffectObserved: (attempt.deliverySummary?.sideEffectEvents ?? 0) > 0,
|
|
95
|
+
...(recovery ? { causeHint: causeForRecoveryKind(recovery) } : {}),
|
|
96
|
+
});
|
|
97
|
+
if (attribution?.stage) attempt.failureStage = attribution.stage;
|
|
98
|
+
if (attribution?.cause) attempt.failureCause = attribution.cause;
|
|
99
|
+
}
|
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
isCyberPolicyCode,
|
|
13
13
|
isCyberPolicyMessage,
|
|
14
14
|
isRateLimitOrQuotaFailureMessage,
|
|
15
|
+
isUpstreamResetReplayRefusedMessage,
|
|
15
16
|
upstreamErrorMessageFromPayload,
|
|
16
17
|
} from "../lib/errors";
|
|
17
18
|
import { CODEX_CONFIG_PATH, readRootTomlString } from "../codex/paths";
|
|
@@ -22,6 +23,7 @@ import { normalizeRouteDecisionTrace, type RouteDecisionTraceV1 } from "../routi
|
|
|
22
23
|
import type { AdapterRequest } from "../adapters/base";
|
|
23
24
|
import type { RequestSpendSettlement } from "./responses/request-spend";
|
|
24
25
|
import type { AdapterTierMetadata } from "../providers/fastwire";
|
|
26
|
+
import { UPSTREAM_RESET_REPLAY_REFUSED_CODE } from "../lib/upstream-retry";
|
|
25
27
|
import { redactSecretString, sanitizeLogMetadataString } from "../lib/redact";
|
|
26
28
|
import {
|
|
27
29
|
appendUsageEntry,
|
|
@@ -38,6 +40,7 @@ import {
|
|
|
38
40
|
isLogicalRequestId,
|
|
39
41
|
isValidReasoningWireValue,
|
|
40
42
|
normalizeClaudeCompatibilityUsageLog,
|
|
43
|
+
normalizeRequestFailureAttribution,
|
|
41
44
|
normalizeRequestSpend,
|
|
42
45
|
readRecentUsageEntries,
|
|
43
46
|
usageForFinalLog,
|
|
@@ -50,9 +53,13 @@ import {
|
|
|
50
53
|
type PersistedUsageAttempt,
|
|
51
54
|
type PersistedUsageEntry,
|
|
52
55
|
type PersistedClaudeCompatibilityLog,
|
|
56
|
+
type RequestFailureCause,
|
|
57
|
+
type RequestFailureStage,
|
|
53
58
|
type UsageStatus,
|
|
54
59
|
} from "../usage/log";
|
|
55
60
|
import type { RequestExecutionBudget } from "../lib/request-execution-budget";
|
|
61
|
+
import { attributeFinalRequest, attributeSealedAttempt } from "./request-log-failure-attribution";
|
|
62
|
+
import { debugAttemptDeliverySummary } from "../lib/debug";
|
|
56
63
|
import {
|
|
57
64
|
appendUsageDebug,
|
|
58
65
|
isUsageDebugEnabled,
|
|
@@ -67,15 +74,37 @@ import { inferCursorContextWindow } from "../adapters/cursor/discovery";
|
|
|
67
74
|
import { KIRO_MODEL_CONTEXT_WINDOWS, normalizeKiroModelId } from "../providers/kiro-models";
|
|
68
75
|
import { DEVIN_MODEL_CONTEXT_WINDOWS } from "../adapters/devin/live-models";
|
|
69
76
|
import { modelRecordValue } from "../reasoning-effort";
|
|
77
|
+
import type { RequestMetricsRecorder } from "./request-metrics";
|
|
78
|
+
import type {
|
|
79
|
+
CacheDiagnosticDraft,
|
|
80
|
+
CacheDiagnosticFinalFacts,
|
|
81
|
+
PromptCacheKeySource,
|
|
82
|
+
} from "../usage/cache-diagnostic";
|
|
83
|
+
|
|
84
|
+
const CACHE_DIAGNOSTIC_HOOK = Symbol.for("opencodex.cache-diagnostic.v1");
|
|
85
|
+
interface CacheDiagnosticHooks {
|
|
86
|
+
observeInbound(body: unknown, headers: Headers, source: PromptCacheKeySource): CacheDiagnosticDraft;
|
|
87
|
+
rebind(body: unknown, draft: CacheDiagnosticDraft | undefined): void;
|
|
88
|
+
finalize(facts: CacheDiagnosticFinalFacts): void;
|
|
89
|
+
}
|
|
90
|
+
function cacheDiagnosticHooks(): CacheDiagnosticHooks | undefined {
|
|
91
|
+
return (globalThis as Record<symbol, CacheDiagnosticHooks | undefined>)[CACHE_DIAGNOSTIC_HOOK];
|
|
92
|
+
}
|
|
70
93
|
|
|
71
94
|
export interface RequestLogContext {
|
|
72
95
|
model: string;
|
|
73
96
|
provider: string;
|
|
97
|
+
/** Optional process-lifetime aggregate sink, injected by the server composition owner. */
|
|
98
|
+
requestMetricsRecorder?: RequestMetricsRecorder;
|
|
99
|
+
/** Bounded terminal enum observed while inspecting a buffered response body. */
|
|
100
|
+
observedTerminalStatus?: ResponsesTerminalStatus;
|
|
74
101
|
/**
|
|
75
102
|
* Identity of the ONE logical request this context serves (#4546). Set from the execution
|
|
76
103
|
* budget minted at ingress; a retry leg, a repair refetch and a combo child share it.
|
|
77
104
|
*/
|
|
78
105
|
logicalRequestId?: string;
|
|
106
|
+
/** Process-local privacy-bounded cache diagnostic; never persisted with request logs. */
|
|
107
|
+
cacheDiagnosticDraft?: CacheDiagnosticDraft;
|
|
79
108
|
/**
|
|
80
109
|
* Internal live reference to this request's execution budget; omitted from RequestLogEntry and
|
|
81
110
|
* JSONL. Read at final-log time so the row reports the budget's FINAL state rather than a
|
|
@@ -202,6 +231,24 @@ export interface RequestLogContext {
|
|
|
202
231
|
claudeCompatibility?: PersistedClaudeCompatibilityLog;
|
|
203
232
|
}
|
|
204
233
|
|
|
234
|
+
export function observeCacheDiagnosticInbound(
|
|
235
|
+
logCtx: RequestLogContext,
|
|
236
|
+
body: unknown,
|
|
237
|
+
headers: Headers,
|
|
238
|
+
source: PromptCacheKeySource,
|
|
239
|
+
): void {
|
|
240
|
+
const draft = cacheDiagnosticHooks()?.observeInbound(body, headers, source);
|
|
241
|
+
if (draft) logCtx.cacheDiagnosticDraft = draft;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/** Alias a rebuilt form of the request body to the request's diagnostic draft. */
|
|
245
|
+
export function rebindCacheDiagnosticBody(
|
|
246
|
+
body: unknown,
|
|
247
|
+
draft: CacheDiagnosticDraft | undefined,
|
|
248
|
+
): void {
|
|
249
|
+
cacheDiagnosticHooks()?.rebind(body, draft);
|
|
250
|
+
}
|
|
251
|
+
|
|
205
252
|
export interface RequestLogEntry {
|
|
206
253
|
requestId: string;
|
|
207
254
|
/** The logical request this row belongs to (#4546); absent on rows written without a budget. */
|
|
@@ -288,9 +335,17 @@ export interface RequestLogEntry {
|
|
|
288
335
|
routeDecision?: RouteDecisionTraceV1;
|
|
289
336
|
/** Closed Claude protocol codes; no request or header values. */
|
|
290
337
|
claudeCompatibility?: PersistedClaudeCompatibilityLog;
|
|
338
|
+
/**
|
|
339
|
+
* How far this request got and why it failed, in the shared stage and cause vocabulary
|
|
340
|
+
* (#2366). Derived once at the single finalization seam and carried on the row so the
|
|
341
|
+
* dashboard, the durable ledger and the exporter read one answer instead of three.
|
|
342
|
+
*/
|
|
343
|
+
failureStage?: RequestFailureStage;
|
|
344
|
+
failureCause?: RequestFailureCause;
|
|
291
345
|
}
|
|
292
346
|
|
|
293
347
|
const requestLog: RequestLogEntry[] = [];
|
|
348
|
+
const requestLogObserversForTests = new Set<(entry: RequestLogEntry) => void>();
|
|
294
349
|
const MAX_LOG_SIZE = 2000;
|
|
295
350
|
const requestLogEntryBytes = new WeakMap<RequestLogEntry, number>();
|
|
296
351
|
let requestLogBytes = 0;
|
|
@@ -412,6 +467,7 @@ export function requestLogEntryFromPersistedUsage(entry: PersistedUsageEntry): R
|
|
|
412
467
|
...(entry.conversationStateScrub === "account-change"
|
|
413
468
|
? { conversationStateScrub: "account-change" }
|
|
414
469
|
: {}),
|
|
470
|
+
...normalizeRequestFailureAttribution(entry),
|
|
415
471
|
};
|
|
416
472
|
}
|
|
417
473
|
|
|
@@ -472,6 +528,24 @@ export function hydrateRequestLogsFromDisk(
|
|
|
472
528
|
}
|
|
473
529
|
}
|
|
474
530
|
|
|
531
|
+
/**
|
|
532
|
+
* Rebuild the Logs ring after retention deleted rows from the ledger.
|
|
533
|
+
*
|
|
534
|
+
* Without this a compaction is invisible where an operator actually looks: the ring holds up to
|
|
535
|
+
* 2,000 entries independently of the file, so rows deleted from disk keep serving through
|
|
536
|
+
* /api/logs until eviction or a restart -- the dashboard showing history the ledger no longer
|
|
537
|
+
* has. Observers are deliberately not replayed; they exist to watch NEW rows arrive, and
|
|
538
|
+
* replaying a rehydration through them would announce two thousand arrivals that did not happen.
|
|
539
|
+
*/
|
|
540
|
+
export function rehydrateRequestLogsAfterLedgerReplacement(
|
|
541
|
+
reader: () => PersistedUsageEntry[] = () => readRecentUsageEntries(MAX_LOG_SIZE),
|
|
542
|
+
): number {
|
|
543
|
+
requestLog.length = 0;
|
|
544
|
+
requestLogBytes = 0;
|
|
545
|
+
requestLogsHydratedFromDisk = false;
|
|
546
|
+
return hydrateRequestLogsFromDisk(reader);
|
|
547
|
+
}
|
|
548
|
+
|
|
475
549
|
export function addRequestLog(entry: RequestLogEntry) {
|
|
476
550
|
// Sanitize ONCE, at the ingress, and use that one value for both destinations.
|
|
477
551
|
//
|
|
@@ -491,6 +565,9 @@ export function addRequestLog(entry: RequestLogEntry) {
|
|
|
491
565
|
else if (retained !== entry) delete retained.claudeCompatibility;
|
|
492
566
|
entry = retained;
|
|
493
567
|
retainRequestLogEntry(entry);
|
|
568
|
+
for (const observer of requestLogObserversForTests) {
|
|
569
|
+
try { observer(entry); } catch { /* test observation must never fail request logging */ }
|
|
570
|
+
}
|
|
494
571
|
try {
|
|
495
572
|
// Failure diagnostics survive the 200-entry ring buffer by riding the persisted
|
|
496
573
|
// usage entry (devlog/_plan/260716_claudecode_hardening/030). Success rows stay
|
|
@@ -555,6 +632,10 @@ export function addRequestLog(entry: RequestLogEntry) {
|
|
|
555
632
|
...(isKnownTransportPhase(entry.transportPhase) ? { transportPhase: entry.transportPhase } : {}),
|
|
556
633
|
...(isKnownTerminalSource(entry.terminalSource) ? { terminalSource: entry.terminalSource } : {}),
|
|
557
634
|
...failureDiagnostics,
|
|
635
|
+
// Rebuilt explicitly, like every other field here: this function does not spread the
|
|
636
|
+
// entry, so a pair omitted at this line would reach /api/logs and never reach
|
|
637
|
+
// usage.jsonl, which is the surface the derived failure projection reads.
|
|
638
|
+
...normalizeRequestFailureAttribution(entry),
|
|
558
639
|
...(entry.routeDecision ? { routeDecision: entry.routeDecision } : {}),
|
|
559
640
|
...(entry.claudeCompatibility ? { claudeCompatibility: entry.claudeCompatibility } : {}),
|
|
560
641
|
...(entry.conversationStateScrub === "account-change"
|
|
@@ -566,6 +647,12 @@ export function addRequestLog(entry: RequestLogEntry) {
|
|
|
566
647
|
}
|
|
567
648
|
}
|
|
568
649
|
|
|
650
|
+
/** Test-only finalized-row observation without polling the management projection. */
|
|
651
|
+
export function observeRequestLogsForTests(observer: (entry: RequestLogEntry) => void): () => void {
|
|
652
|
+
requestLogObserversForTests.add(observer);
|
|
653
|
+
return () => { requestLogObserversForTests.delete(observer); };
|
|
654
|
+
}
|
|
655
|
+
|
|
569
656
|
export function nextRequestLogId(_timestamp = Date.now()): string {
|
|
570
657
|
return `ocx-${randomBytes(16).toString("hex")}`;
|
|
571
658
|
}
|
|
@@ -714,7 +801,15 @@ export function requestLogErrorCode(
|
|
|
714
801
|
}
|
|
715
802
|
return "permission_denied";
|
|
716
803
|
}
|
|
717
|
-
if (status === 429)
|
|
804
|
+
if (status === 429) {
|
|
805
|
+
// A refused ambiguous reset answers 429 by design (it must not invite a client
|
|
806
|
+
// retry that could duplicate inference); classify it by its message so the log
|
|
807
|
+
// distinguishes a proxy refusal from provider throttling.
|
|
808
|
+
if (upstreamError?.trim() && isUpstreamResetReplayRefusedMessage(upstreamError)) {
|
|
809
|
+
return UPSTREAM_RESET_REPLAY_REFUSED_CODE;
|
|
810
|
+
}
|
|
811
|
+
return "rate_limit_exceeded";
|
|
812
|
+
}
|
|
718
813
|
if (status === 503) return "server_is_overloaded";
|
|
719
814
|
if (status >= 500) return "upstream_server_error";
|
|
720
815
|
return `http_${status}`;
|
|
@@ -766,6 +861,22 @@ export function catalogModelSupportsServiceTier(modelId: string, serviceTier: st
|
|
|
766
861
|
|
|
767
862
|
export function applyResponseLogMetadata(logCtx: RequestLogContext, payload: unknown): void {
|
|
768
863
|
if (!payload || typeof payload !== "object") return;
|
|
864
|
+
if (logCtx.observedTerminalStatus === undefined) {
|
|
865
|
+
const eventType = (payload as { type?: unknown }).type;
|
|
866
|
+
const response = (payload as { response?: unknown }).response;
|
|
867
|
+
const responseStatus = response && typeof response === "object"
|
|
868
|
+
? (response as { status?: unknown }).status
|
|
869
|
+
: (payload as { status?: unknown }).status;
|
|
870
|
+
if (eventType === "response.completed") {
|
|
871
|
+
logCtx.observedTerminalStatus = "completed";
|
|
872
|
+
} else if (eventType === "response.failed") {
|
|
873
|
+
logCtx.observedTerminalStatus = "failed";
|
|
874
|
+
} else if (eventType === "response.incomplete") {
|
|
875
|
+
logCtx.observedTerminalStatus = "incomplete";
|
|
876
|
+
} else if (responseStatus === "completed" || responseStatus === "failed" || responseStatus === "incomplete") {
|
|
877
|
+
logCtx.observedTerminalStatus = responseStatus;
|
|
878
|
+
}
|
|
879
|
+
}
|
|
769
880
|
const source = "response" in payload && typeof (payload as { response?: unknown }).response === "object"
|
|
770
881
|
? (payload as { response?: unknown }).response
|
|
771
882
|
: payload;
|
|
@@ -1293,6 +1404,18 @@ export function addFinalRequestLog(
|
|
|
1293
1404
|
if (errorCode) logCtx.activeAttempt.errorCode = errorCode;
|
|
1294
1405
|
else delete logCtx.activeAttempt.errorCode;
|
|
1295
1406
|
}
|
|
1407
|
+
// Derived and stamped in a sibling module, before the attempt snapshot below. Every input is
|
|
1408
|
+
// a closed value; the open error strings are deliberately not among them.
|
|
1409
|
+
const attribution = attributeFinalRequest({
|
|
1410
|
+
status: effectiveStatus,
|
|
1411
|
+
...(meta?.terminalStatus ? { terminalStatus: meta.terminalStatus } : {}),
|
|
1412
|
+
...(closeReason ? { closeReason } : {}),
|
|
1413
|
+
...(logCtx.transportPhase ? { transportPhase: logCtx.transportPhase } : {}),
|
|
1414
|
+
...(logCtx.terminalSource ? { terminalSource: logCtx.terminalSource } : {}),
|
|
1415
|
+
outputObserved: logCtx.firstOutputMs !== undefined,
|
|
1416
|
+
locallyAnswered: logCtx.localTerminalReason !== undefined,
|
|
1417
|
+
...(logCtx.activeAttempt ? { attempt: logCtx.activeAttempt } : {}),
|
|
1418
|
+
});
|
|
1296
1419
|
// The one seam every request passes exactly once, whatever transport served it and however
|
|
1297
1420
|
// it ended. The terminal usage belongs to the last send that left; the ledger resolves every
|
|
1298
1421
|
// earlier send of this request as unresolved spend rather than handing its tokens back.
|
|
@@ -1310,6 +1433,10 @@ export function addFinalRequestLog(
|
|
|
1310
1433
|
...(attempt.recoveryWithheld?.length ? { recoveryWithheld: [...attempt.recoveryWithheld] } : {}),
|
|
1311
1434
|
...(attempt.usage ? { usage: { ...attempt.usage } } : {}),
|
|
1312
1435
|
...(attempt.tierOutcome ? { tierOutcome: { ...attempt.tierOutcome } } : {}),
|
|
1436
|
+
// Detached, like every mutable field beside it: the live summary keeps counting if the
|
|
1437
|
+
// stream is still draining, and a shared reference would let a finalized row change after
|
|
1438
|
+
// it was written.
|
|
1439
|
+
...(attempt.deliverySummary ? { deliverySummary: { ...attempt.deliverySummary } } : {}),
|
|
1313
1440
|
}));
|
|
1314
1441
|
const isCombo = logCtx.comboId !== undefined && (attempts?.length ?? 0) > 0;
|
|
1315
1442
|
const aggregate = isCombo ? aggregateAttemptUsage(attempts ?? []) : null;
|
|
@@ -1317,10 +1444,40 @@ export function addFinalRequestLog(
|
|
|
1317
1444
|
const usageStatus = aggregate?.status ?? existing.status;
|
|
1318
1445
|
const totalTokens = aggregate?.totalTokens ?? existing.totalTokens;
|
|
1319
1446
|
const spend = requestSpendRecord(logCtx, attempts);
|
|
1447
|
+
const durationMs = Date.now() - start;
|
|
1448
|
+
logCtx.requestMetricsRecorder?.recordFinalRequest({
|
|
1449
|
+
...(logCtx.inboundProtocol ? { protocol: logCtx.inboundProtocol } : {}),
|
|
1450
|
+
status: effectiveStatus,
|
|
1451
|
+
durationMs,
|
|
1452
|
+
...(logCtx.firstOutputMs !== undefined ? { firstOutputMs: logCtx.firstOutputMs } : {}),
|
|
1453
|
+
...(meta?.terminalStatus ? { terminalStatus: meta.terminalStatus } : {}),
|
|
1454
|
+
...(closeReason ? { closeReason } : {}),
|
|
1455
|
+
...(attempts !== undefined ? { attempts } : {}),
|
|
1456
|
+
...(spend ? { spendSends: spend.sends } : {}),
|
|
1457
|
+
...(attribution.failureCause ? { failureCause: attribution.failureCause } : {}),
|
|
1458
|
+
});
|
|
1320
1459
|
const cacheProvenance = classifyCacheTelemetryProvenance(loggedUsage, {
|
|
1321
1460
|
wireParsed: logCtx.usageWireParsed === true,
|
|
1322
1461
|
});
|
|
1323
1462
|
const logicalRequestId = logCtx.logicalRequestId ?? logCtx.executionBudget?.logicalRequestId;
|
|
1463
|
+
const normalizedCacheValue = loggedUsage?.cacheReadInputTokens ?? loggedUsage?.cachedInputTokens;
|
|
1464
|
+
cacheDiagnosticHooks()?.finalize({
|
|
1465
|
+
requestId,
|
|
1466
|
+
...(isLogicalRequestId(logicalRequestId) ? { logicalRequestId } : {}),
|
|
1467
|
+
protocol: logCtx.inboundProtocol ?? "responses",
|
|
1468
|
+
provider: logCtx.provider,
|
|
1469
|
+
model: logCtx.model,
|
|
1470
|
+
...(isCodexUsageAccountLogLabel(logCtx.accountLogLabel) ? { accountLogLabel: logCtx.accountLogLabel } : {}),
|
|
1471
|
+
...(logCtx.affinity ? { affinityMove: logCtx.affinity } : {}),
|
|
1472
|
+
...(logCtx.affinityReason ? { affinityReason: logCtx.affinityReason } : {}),
|
|
1473
|
+
// loggedUsage carries the upstream cache counter by reference all the way from the
|
|
1474
|
+
// adapter extraction for the native Responses route, so an undefined read here is a
|
|
1475
|
+
// genuinely absent counter rather than a defaulted one.
|
|
1476
|
+
...(normalizedCacheValue !== undefined ? { rawCacheCounterValue: normalizedCacheValue } : {}),
|
|
1477
|
+
...(normalizedCacheValue !== undefined ? { normalizedCacheValue } : {}),
|
|
1478
|
+
cacheProvenance,
|
|
1479
|
+
...(logCtx.cacheDiagnosticDraft ? { draft: logCtx.cacheDiagnosticDraft } : {}),
|
|
1480
|
+
});
|
|
1324
1481
|
// Sanitize at the logging layer, not only at the one call site that populates this today.
|
|
1325
1482
|
// The value originates in an upstream-supplied model id, so an unsanitized newline would
|
|
1326
1483
|
// let a single field forge a record boundary in any line-oriented log viewer. Doing it here
|
|
@@ -1364,7 +1521,7 @@ export function addFinalRequestLog(
|
|
|
1364
1521
|
: {}),
|
|
1365
1522
|
...(logCtx.resolvedModel ? { resolvedModel: logCtx.resolvedModel } : {}),
|
|
1366
1523
|
status: effectiveStatus,
|
|
1367
|
-
durationMs
|
|
1524
|
+
durationMs,
|
|
1368
1525
|
...(logCtx.firstOutputMs !== undefined ? { firstOutputMs: logCtx.firstOutputMs } : {}),
|
|
1369
1526
|
...(errorCode ? { errorCode } : {}),
|
|
1370
1527
|
...(meta?.terminalStatus ? { terminalStatus: meta.terminalStatus } : {}),
|
|
@@ -1388,7 +1545,10 @@ export function addFinalRequestLog(
|
|
|
1388
1545
|
...(logCtx.terminalSource ? { terminalSource: logCtx.terminalSource } : {}),
|
|
1389
1546
|
...(logCtx.routeDecision ? { routeDecision: logCtx.routeDecision } : {}),
|
|
1390
1547
|
...(claudeCompatibility ? { claudeCompatibility } : {}),
|
|
1548
|
+
...attribution,
|
|
1391
1549
|
});
|
|
1550
|
+
// Formatted from the finalized snapshot, so the ring shows exactly what the ledger holds.
|
|
1551
|
+
for (const attempt of attempts ?? []) debugAttemptDeliverySummary(requestId, attempt);
|
|
1392
1552
|
if (isUsageDebugEnabled()) {
|
|
1393
1553
|
appendUsageDebug({
|
|
1394
1554
|
ts: Date.now(),
|
|
@@ -1635,8 +1795,14 @@ export function noteProviderAttemptSend(
|
|
|
1635
1795
|
finishRequestAttempt(attempt, attempt.status >= 100 ? attempt.status
|
|
1636
1796
|
: recovery === "key-401" ? 401 : recovery?.includes("429") ? 429 : 502,
|
|
1637
1797
|
Date.now() - (logCtx.activeAttemptStartedAt ?? Date.now()), attempt.usage);
|
|
1798
|
+
// This attempt is being sealed because a NAMED recovery rejected it, so the recovery kind
|
|
1799
|
+
// is direct evidence here rather than an inference from history. Without this the sealed
|
|
1800
|
+
// attempt would reach the ledger with no attribution at all: the finalization seam below
|
|
1801
|
+
// only ever sees the last attempt of the request.
|
|
1802
|
+
attributeSealedAttempt(attempt, recovery);
|
|
1638
1803
|
const completed = { ...attempt, recoveryKinds: [...attempt.recoveryKinds],
|
|
1639
1804
|
...(attempt.usage ? { usage: { ...attempt.usage } } : {}),
|
|
1805
|
+
...(attempt.deliverySummary ? { deliverySummary: { ...attempt.deliverySummary } } : {}),
|
|
1640
1806
|
...(attempt.tierOutcome ? { tierOutcome: { ...attempt.tierOutcome } } : {}) };
|
|
1641
1807
|
const attempts = logCtx.attempts ??= [attempt];
|
|
1642
1808
|
const index = attempts.indexOf(attempt);
|
|
@@ -1809,4 +1975,5 @@ export function clearRequestLogsForTests(): void {
|
|
|
1809
1975
|
requestLog.length = 0;
|
|
1810
1976
|
requestLogBytes = 0;
|
|
1811
1977
|
requestLogsHydratedFromDisk = false;
|
|
1978
|
+
requestLogObserversForTests.clear();
|
|
1812
1979
|
}
|