@bitkyc08/opencodex 2.58.0 → 2.60.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +28 -10
- package/gui/dist/assets/index-BTuCbqQd.css +1 -0
- package/gui/dist/assets/index-DoBVdPHP.js +134 -0
- package/gui/dist/index.html +2 -2
- package/gui/dist/provider-icons/crusoe.svg +1 -0
- package/gui/dist/provider-icons/opper.svg +3 -0
- package/package.json +4 -1
- package/src/adapters/anthropic-image-codec.ts +16 -2
- package/src/adapters/anthropic-image-normalize.ts +49 -2
- package/src/adapters/anthropic.ts +4 -1
- package/src/adapters/base.ts +34 -1
- package/src/adapters/coding-agent/turn.ts +22 -2
- package/src/adapters/command-code.ts +50 -3
- package/src/adapters/cursor/catalog.ts +11 -0
- package/src/adapters/cursor/checkpoint-store.ts +3 -0
- package/src/adapters/cursor/discovery.ts +11 -8
- package/src/adapters/cursor/effort-map.ts +16 -2
- package/src/adapters/cursor/envelope-echo.ts +55 -2
- package/src/adapters/cursor/live-transport.ts +26 -9
- package/src/adapters/cursor/message-mapper.ts +3 -2
- package/src/adapters/cursor/protobuf-request.ts +8 -5
- package/src/adapters/cursor/request-builder.ts +21 -4
- package/src/adapters/cursor/thread-continuity.ts +105 -31
- package/src/adapters/cursor/tool-guidance.ts +5 -4
- package/src/adapters/cursor/transport.ts +19 -0
- package/src/adapters/cursor.ts +45 -2
- package/src/adapters/devin/cloud-direct/chat.ts +14 -3
- package/src/adapters/devin/cloud-direct/index.ts +7 -0
- package/src/adapters/devin/cloud-direct/stated-reset-retry.ts +140 -0
- package/src/adapters/devin.ts +125 -26
- package/src/adapters/google-antigravity-replay.ts +1 -1
- package/src/adapters/google-antigravity-wire.ts +55 -4
- package/src/adapters/google-http.ts +57 -11
- 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 +60 -10
- package/src/adapters/openai-chat/response-events.ts +61 -0
- package/src/adapters/openai-chat-images.ts +3 -1
- package/src/adapters/openai-chat.ts +10 -11
- package/src/adapters/openai-responses/image-gen.ts +8 -6
- package/src/adapters/openai-responses/passthrough.ts +25 -4
- package/src/adapters/openai-responses/reasoning.ts +7 -0
- package/src/adapters/openai-responses/tool-output-recovery.ts +75 -0
- package/src/adapters/openai-responses/tool-schema.ts +19 -7
- package/src/adapters/opencode-go-additional-tools.ts +12 -2
- package/src/adapters/responses-tool-schema.ts +76 -46
- package/src/adapters/run-turn-queue.ts +17 -4
- package/src/bridge/response-json.ts +1 -1
- package/src/bridge/sse.ts +28 -35
- package/src/claude/context-windows.ts +22 -0
- package/src/claude/outbound.ts +35 -4
- package/src/cli/account-api.ts +4 -3
- package/src/cli/account-extended.ts +26 -6
- package/src/cli/account-orca-import.ts +63 -0
- package/src/cli/account.ts +32 -4
- package/src/cli/capabilities.ts +40 -0
- package/src/cli/claude.ts +29 -1
- package/src/cli/codex-cli-update.ts +97 -2
- package/src/cli/dispatch.ts +57 -3
- package/src/cli/doctor.ts +218 -4
- package/src/cli/help.ts +4 -1
- package/src/cli/hub.ts +3 -2
- package/src/cli/index.ts +95 -22
- package/src/cli/models-runtime.ts +33 -4
- package/src/cli/opencode.ts +2 -2
- package/src/cli/provider.ts +13 -1
- package/src/cli/registry.ts +11 -1
- package/src/cli/runtime-api.ts +44 -0
- package/src/cli/start-args.ts +94 -0
- package/src/cli/system-command.ts +2 -0
- package/src/client/machine-api.ts +6 -5
- package/src/client/machine-listener.ts +16 -3
- package/src/client/runtime.ts +26 -2
- package/src/clients/config-export/constants.ts +2 -3
- package/src/clients/config-export.ts +5 -5
- package/src/codex/account-store.ts +146 -5
- package/src/codex/auth-api/account-list.ts +19 -11
- package/src/codex/auth-api/pool-quota-probe.ts +44 -10
- package/src/codex/auth-api/routes.ts +17 -2
- package/src/codex/auth-context.ts +16 -12
- package/src/codex/catalog/build-entries.ts +25 -4
- package/src/codex/catalog/derive-entry.ts +8 -1
- package/src/codex/catalog/effort.ts +10 -6
- package/src/codex/catalog/gather-capture.ts +22 -2
- package/src/codex/catalog/model-hints.ts +66 -33
- package/src/codex/catalog/parsing.ts +90 -5
- package/src/codex/catalog/provider-models.ts +19 -2
- package/src/codex/catalog/reserve-warn.ts +96 -0
- package/src/codex/catalog/retained-sync.ts +41 -26
- package/src/codex/catalog/routed-gather.ts +61 -3
- package/src/codex/cli-installation-identity.ts +210 -0
- package/src/codex/cli-installation-targets.ts +158 -0
- package/src/codex/context-compat.ts +5 -2
- package/src/codex/convergence.ts +5 -0
- package/src/codex/desired-state.ts +4 -1
- package/src/codex/history-job.ts +6 -6
- package/src/codex/history-provider.ts +24 -167
- package/src/codex/history-rollout-read.ts +174 -0
- package/src/codex/history-state-open.ts +105 -0
- package/src/codex/inject/config-toml.ts +44 -2
- package/src/codex/inject.ts +3 -2
- package/src/codex/internal/catalog-writer.ts +33 -1
- package/src/codex/lineage.ts +83 -32
- package/src/codex/loopback-target.ts +31 -0
- package/src/codex/main-account-hard-lock.ts +2 -1
- package/src/codex/main-account.ts +10 -3
- package/src/codex/main-device-reauth.ts +17 -9
- package/src/codex/model-cache.ts +47 -0
- package/src/codex/model-entitlements.ts +80 -2
- package/src/codex/observed-model-denials.ts +230 -0
- package/src/codex/orca-auth-source.ts +94 -0
- package/src/codex/orca-import.ts +219 -0
- package/src/codex/prompt-text-probe.ts +289 -16
- package/src/codex/quota-401-recovery.ts +12 -0
- package/src/codex/quota-types.ts +65 -0
- package/src/codex/quota.ts +24 -19
- package/src/codex/routing/cooldown-math.ts +8 -47
- package/src/codex/routing/pin-drain.ts +57 -0
- package/src/codex/routing.ts +20 -16
- package/src/codex/shim.ts +1 -1
- package/src/codex/subagent-model-fallback.ts +114 -2
- package/src/codex/windows-installation-files.ts +224 -0
- package/src/combos/failover.ts +125 -5
- package/src/config/admitted-identity.ts +222 -0
- package/src/config/diagnostics.ts +43 -1
- package/src/config/feature-flags.ts +5 -0
- package/src/config/load-degrade.ts +33 -0
- package/src/config/pending-teardown.ts +8 -0
- package/src/config/process-state.ts +36 -3
- package/src/config/provider-relative-send-path.ts +16 -0
- package/src/config/proxy-env.ts +31 -7
- package/src/config/schema/compaction-triggers.ts +11 -0
- package/src/config/schema/config-schema.ts +25 -0
- package/src/config/schema/leaf-validators.ts +76 -17
- package/src/config.ts +2 -2
- package/src/generated/compatibility-version.json +410 -258
- package/src/generated/model-metadata.ts +1 -1
- 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/mutation-plan.ts +815 -0
- package/src/integrations/writer.ts +85 -99
- 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/bounded-body.ts +4 -2
- package/src/lib/debug-log-buffer.ts +6 -1
- package/src/lib/debug.ts +23 -0
- package/src/lib/destination-policy.ts +48 -6
- package/src/lib/errors.ts +82 -15
- package/src/lib/http-response-semantics.ts +57 -0
- package/src/lib/lab-live-pinned-sender.ts +26 -12
- package/src/lib/local-destinations.ts +32 -5
- 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-outbound.ts +53 -5
- package/src/lib/proxy-env.ts +70 -3
- package/src/lib/request-execution-budget.ts +11 -3
- package/src/lib/response-body-inactivity.ts +193 -0
- package/src/lib/retry-delay.ts +69 -0
- package/src/lib/socks5-fetch.ts +741 -0
- package/src/lib/spend-ledger-owner.ts +364 -0
- package/src/lib/spend-reservation-ledger.ts +332 -35
- package/src/lib/windows-system-proxy.ts +16 -11
- package/src/lib/workflow-budget.ts +145 -8
- package/src/oauth/account-quota-rank.ts +72 -15
- package/src/oauth/callback-server.ts +4 -3
- package/src/oauth/generic-account-failover.ts +41 -27
- package/src/oauth/health.ts +12 -1
- package/src/oauth/index.ts +3 -107
- package/src/oauth/login-flow-state.ts +127 -0
- package/src/oauth/orcarouter.ts +15 -2
- package/src/oauth/store.ts +8 -0
- package/src/providers/codex-capacity.ts +9 -0
- package/src/providers/derive.ts +34 -17
- package/src/providers/devin-cli-authmode-migration.ts +14 -10
- package/src/providers/devin-provider-merge-migration.ts +33 -12
- package/src/providers/free-directory.ts +20 -2
- package/src/providers/key-failover.ts +327 -25
- package/src/providers/model-rename-migration.ts +56 -1
- package/src/providers/model-rename-startup.ts +7 -5
- package/src/providers/openai-sidecar.ts +4 -0
- package/src/providers/openai-virtual-models.ts +42 -2
- package/src/providers/opencode-go-transport.ts +14 -5
- package/src/providers/quota/antigravity.ts +22 -2
- package/src/providers/quota/report-cache.ts +3 -0
- package/src/providers/quota/vendor-probes-key.ts +1 -1
- package/src/providers/registry/entries-core.ts +39 -17
- package/src/providers/registry/entries-extended.ts +131 -1
- package/src/providers/registry/model-ids.ts +168 -0
- package/src/providers/registry/model-seeds.ts +87 -21
- 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/responses/apply-patch-envelope.ts +32 -11
- package/src/responses/bridge-search-replay-cache.ts +152 -0
- package/src/responses/code-mode-helper-compat.ts +26 -16
- package/src/responses/custom-tool-compat.ts +1 -1
- package/src/responses/freeform-wrapper-scan.ts +279 -0
- package/src/responses/hosted-tool-policy.ts +85 -2
- package/src/responses/legacy-dotted-tool-name-repair.ts +134 -0
- package/src/responses/progressive-freeform-input.ts +130 -0
- package/src/responses/reasoning-envelope.ts +30 -0
- package/src/responses/schema.ts +9 -2
- package/src/responses/state.ts +5 -12
- package/src/responses/tool-name-aliases.ts +15 -1
- package/src/router.ts +91 -115
- 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/auth-cors.ts +29 -0
- package/src/server/chat-completions.ts +13 -5
- package/src/server/chat-native-sse.ts +26 -9
- package/src/server/chat-native.ts +10 -4
- package/src/server/claude-messages.ts +30 -5
- package/src/server/effort-row.ts +11 -3
- package/src/server/grok-responses-control-frame.ts +160 -1
- package/src/server/gui-static.ts +36 -2
- package/src/server/inbound-body-admission.ts +187 -0
- package/src/server/index/serve-options.ts +86 -29
- package/src/server/index/spend-ledger-lifecycle.ts +66 -0
- package/src/server/index/websocket-handler.ts +6 -1
- package/src/server/index.ts +24 -25
- package/src/server/management/api-access.ts +3 -4
- package/src/server/management/aside-profile-routes.ts +266 -7
- package/src/server/management/config-routes.ts +48 -7
- package/src/server/management/context.ts +3 -0
- package/src/server/management/integration-routes.ts +287 -5
- package/src/server/management/metrics-routes.ts +20 -0
- package/src/server/management/model-rows.ts +224 -12
- package/src/server/management/provider-capability-config.ts +35 -7
- package/src/server/management/provider-routes.ts +70 -18
- package/src/server/management/route-registry.ts +14 -0
- package/src/server/management/shared.ts +10 -3
- 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-api.ts +2 -0
- package/src/server/management-auth.ts +15 -1
- package/src/server/proxy-liveness.ts +97 -2
- package/src/server/readiness.ts +29 -10
- package/src/server/relay-eager.ts +24 -2
- package/src/server/relay.ts +136 -34
- package/src/server/request-log.ts +80 -3
- package/src/server/request-metrics.ts +236 -0
- package/src/server/responses/adapter-continuation.ts +74 -30
- package/src/server/responses/adapter-delivery.ts +39 -8
- package/src/server/responses/adapter-dispatch.ts +63 -30
- package/src/server/responses/compact.ts +89 -18
- package/src/server/responses/compaction-routing.ts +111 -0
- package/src/server/responses/core-codex-account.ts +90 -24
- package/src/server/responses/core-combo.ts +7 -7
- package/src/server/responses/core-normalize.ts +16 -15
- package/src/server/responses/core-opaque-recovery.ts +1 -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 +68 -2
- package/src/server/responses/passthrough-delivery.ts +41 -15
- package/src/server/responses/passthrough-dispatch.ts +145 -53
- package/src/server/responses/passthrough-execution.ts +11 -1
- package/src/server/responses/policy-fallback.ts +5 -13
- package/src/server/responses/request-prepare.ts +96 -17
- package/src/server/responses/request-send-budget.ts +89 -8
- package/src/server/responses/request-sidecar-auth.ts +17 -9
- package/src/server/responses/request-spend.ts +38 -9
- package/src/server/responses/request-transport.ts +15 -12
- package/src/server/responses/run-turn-execution.ts +45 -8
- package/src/server/responses/sidecar-execution.ts +19 -2
- package/src/server/responses/ws-upstream.ts +16 -28
- package/src/server/responses-custom-tool-repair.ts +29 -56
- package/src/server/responses-undeclared-tool-guard.ts +31 -1
- package/src/server/sse-frame-buffer.ts +12 -10
- package/src/server/sse-payload-rewrite.ts +37 -10
- package/src/server/system-env-shell.ts +5 -1
- package/src/server/system-env.ts +7 -1
- package/src/server/workflow-refusal.ts +56 -2
- package/src/service/cli.ts +16 -6
- package/src/service/guards.ts +10 -0
- package/src/service/health.ts +43 -0
- package/src/service/state.ts +7 -2
- package/src/tray/windows-tray.ps1 +155 -3
- package/src/types/accounts.ts +4 -0
- package/src/types/config.ts +111 -6
- package/src/types/provider.ts +37 -0
- package/src/types/request.ts +9 -1
- package/src/types/tools.ts +14 -0
- package/src/types/wire.ts +9 -1
- package/src/types.ts +1 -0
- package/src/usage/expected-prices.ts +28 -0
- package/src/usage/log.ts +87 -4
- package/src/vision/eligibility.ts +88 -9
- package/src/vision/plan.ts +34 -10
- package/src/web-search/executor.ts +41 -2
- package/src/web-search/loop.ts +6 -1
- package/src/web-search/passthrough-bridge.ts +39 -5
- package/gui/dist/assets/index-BbrHOIY0.js +0 -128
- package/gui/dist/assets/index-C5-RdDmD.css +0 -1
|
@@ -30,6 +30,7 @@ import {
|
|
|
30
30
|
createSseTerminalOutputBoundary,
|
|
31
31
|
doneFrame,
|
|
32
32
|
failedTailFrame,
|
|
33
|
+
refusalFailedTailFrame,
|
|
33
34
|
upstreamErrorTailFrame,
|
|
34
35
|
} from "./relay";
|
|
35
36
|
import {
|
|
@@ -127,6 +128,23 @@ export function relaySseEagerBounded(
|
|
|
127
128
|
return null;
|
|
128
129
|
}
|
|
129
130
|
};
|
|
131
|
+
/**
|
|
132
|
+
* Same tail, except that a refusal the boundary already captured outranks the
|
|
133
|
+
* read failure that followed it — the upstream ended the turn before the
|
|
134
|
+
* socket did (#5176).
|
|
135
|
+
*/
|
|
136
|
+
const encodeTerminalTail = (error: unknown): Uint8Array | null => {
|
|
137
|
+
const refusalCode = terminalBoundary.upstreamRefusalCode();
|
|
138
|
+
const refusalMessage = terminalBoundary.upstreamError();
|
|
139
|
+
if (refusalCode === undefined || refusalMessage === undefined) {
|
|
140
|
+
return encodeFailedTail(error);
|
|
141
|
+
}
|
|
142
|
+
try {
|
|
143
|
+
return refusalFailedTailFrame(terminalEncoder, refusalMessage, refusalCode);
|
|
144
|
+
} catch {
|
|
145
|
+
return null;
|
|
146
|
+
}
|
|
147
|
+
};
|
|
130
148
|
const rewriteDecoder = activeRewrite ? new TextDecoder() : null;
|
|
131
149
|
const rewriteEncoder = activeRewrite ? new TextEncoder() : null;
|
|
132
150
|
const rewriteBudget = opts?.rewriteBudget;
|
|
@@ -313,7 +331,11 @@ export function relaySseEagerBounded(
|
|
|
313
331
|
const upstreamError = terminalBoundary.upstreamError() ?? opts?.upstreamError;
|
|
314
332
|
const upstreamErrorFrame = upstreamError === undefined
|
|
315
333
|
? adapterEofFrame
|
|
316
|
-
: upstreamErrorTailFrame(
|
|
334
|
+
: upstreamErrorTailFrame(
|
|
335
|
+
terminalEncoder,
|
|
336
|
+
upstreamError,
|
|
337
|
+
terminalBoundary.upstreamRefusalCode(),
|
|
338
|
+
);
|
|
317
339
|
queuedBytes += upstreamErrorFrame.byteLength + terminalSentinel.byteLength;
|
|
318
340
|
try {
|
|
319
341
|
controllerRef?.enqueue(upstreamErrorFrame);
|
|
@@ -439,7 +461,7 @@ export function relaySseEagerBounded(
|
|
|
439
461
|
// getters, toString) that re-entrantly cancel the client or abort the
|
|
440
462
|
// upstream. Build the tail FIRST, then re-check eligibility before
|
|
441
463
|
// committing to the synthetic terminal (adversarial review blocker).
|
|
442
|
-
const tail =
|
|
464
|
+
const tail = encodeTerminalTail(err);
|
|
443
465
|
if (tail && canDeliver()) {
|
|
444
466
|
// Inspection and client framing have separate bounded parsers. If
|
|
445
467
|
// inspection resynchronized after an oversized frame and observed a
|
package/src/server/relay.ts
CHANGED
|
@@ -5,6 +5,9 @@ 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";
|
|
@@ -22,10 +25,11 @@ import {
|
|
|
22
25
|
} from "./request-log";
|
|
23
26
|
import {
|
|
24
27
|
BoundedSseFrameBuffer,
|
|
28
|
+
EMPTY_BYTES,
|
|
25
29
|
joinSseFrameBytes,
|
|
26
30
|
MAX_CLIENT_SSE_FRAME_BYTES,
|
|
27
31
|
} from "./sse-frame-buffer";
|
|
28
|
-
import { replaceSseDataPayload } from "./sse-payload-rewrite";
|
|
32
|
+
import { replaceSseDataPayload, sseDataPayload } from "./sse-payload-rewrite";
|
|
29
33
|
import { createBoundedResponseLogBody } from "./response-log-body";
|
|
30
34
|
|
|
31
35
|
const nativePassthroughSseResponses = new WeakSet<Response>();
|
|
@@ -149,16 +153,61 @@ export function failedTailFrame(encoder: TextEncoder, err: unknown): Uint8Array
|
|
|
149
153
|
return encoder.encode(`\n\nevent: response.failed\ndata: ${payload}\n\n${DONE_SSE_FRAME_TEXT}`);
|
|
150
154
|
}
|
|
151
155
|
|
|
152
|
-
|
|
156
|
+
/**
|
|
157
|
+
* Close a turn the upstream ended without a Responses terminal.
|
|
158
|
+
*
|
|
159
|
+
* `refusalCode` carries the upstream's own verdict when it gave one. Codex
|
|
160
|
+
* classifies this terminal by `error.code` alone and retries everything outside
|
|
161
|
+
* its fatal set (codex-rs/codex-api/src/sse/responses.rs:423-450), so stamping
|
|
162
|
+
* `upstream_server_error` on a refusal delivered it as a retryable disconnect
|
|
163
|
+
* and drove the reconnect loop in #5176. Without a refusal code the terminal is
|
|
164
|
+
* unchanged: a genuine transport failure stays retryable, which is what it is.
|
|
165
|
+
*/
|
|
166
|
+
export function upstreamErrorTailFrame(
|
|
167
|
+
encoder: TextEncoder,
|
|
168
|
+
message: string,
|
|
169
|
+
refusalCode?: string,
|
|
170
|
+
): Uint8Array {
|
|
171
|
+
return encoder.encode(
|
|
172
|
+
`event: response.failed\ndata: ${upstreamErrorFailedPayload(message, refusalCode)}\n\n`,
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function upstreamErrorFailedPayload(message: string, refusalCode?: string): string {
|
|
153
177
|
const error = {
|
|
154
|
-
type: "upstream_error",
|
|
155
|
-
code: "upstream_server_error",
|
|
178
|
+
type: refusalCode === undefined ? "upstream_error" : "invalid_request_error",
|
|
179
|
+
code: refusalCode ?? "upstream_server_error",
|
|
156
180
|
message: redactSecretString(message).slice(0, MAX_TAIL_ERROR_MESSAGE_CHARS),
|
|
157
181
|
};
|
|
158
|
-
return
|
|
182
|
+
return JSON.stringify({
|
|
159
183
|
type: "response.failed",
|
|
160
|
-
response: {
|
|
161
|
-
|
|
184
|
+
response: {
|
|
185
|
+
status: "failed",
|
|
186
|
+
error,
|
|
187
|
+
last_error: error,
|
|
188
|
+
...(refusalCode === undefined ? {} : { retryable: false }),
|
|
189
|
+
},
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Terminal for a read that failed after the upstream had already refused.
|
|
195
|
+
*
|
|
196
|
+
* Framed exactly like {@link failedTailFrame} — leading blank line to close a
|
|
197
|
+
* partial block, then the sentinel — but carrying the refusal instead of the
|
|
198
|
+
* generic reset. The refusal is the real outcome of the turn and the socket
|
|
199
|
+
* teardown that followed it is not, so reporting `upstream_reset` here would
|
|
200
|
+
* restart a turn the upstream has already ended (#5176).
|
|
201
|
+
*/
|
|
202
|
+
export function refusalFailedTailFrame(
|
|
203
|
+
encoder: TextEncoder,
|
|
204
|
+
message: string,
|
|
205
|
+
refusalCode: string,
|
|
206
|
+
): Uint8Array {
|
|
207
|
+
const payload = upstreamErrorFailedPayload(message, refusalCode);
|
|
208
|
+
return encoder.encode(
|
|
209
|
+
`\n\nevent: response.failed\ndata: ${payload}\n\n${DONE_SSE_FRAME_TEXT}`,
|
|
210
|
+
);
|
|
162
211
|
}
|
|
163
212
|
|
|
164
213
|
function boundedBareUpstreamErrorMessage(payload: unknown): string | undefined {
|
|
@@ -168,12 +217,57 @@ function boundedBareUpstreamErrorMessage(payload: unknown): string | undefined {
|
|
|
168
217
|
return message ? redactSecretString(message).slice(0, MAX_TAIL_ERROR_MESSAGE_CHARS) : undefined;
|
|
169
218
|
}
|
|
170
219
|
|
|
220
|
+
/**
|
|
221
|
+
* A bare upstream `error` event reduced to what the synthesized terminal needs.
|
|
222
|
+
*
|
|
223
|
+
* The structured code is authoritative whenever the upstream sent one: a code
|
|
224
|
+
* that is not a refusal means the upstream did not refuse, whatever its
|
|
225
|
+
* diagnostic text happens to quote. Recognized refusal copy is read only when
|
|
226
|
+
* no code was carried anywhere on the event, which is the shape #5176 reports.
|
|
227
|
+
* A refusal code with no message still yields a terminal, because Codex accepts
|
|
228
|
+
* that shape and supplies its own copy for it.
|
|
229
|
+
*
|
|
230
|
+
* The candidate topology mirrors {@link upstreamErrorMessageFromPayload}: code
|
|
231
|
+
* and message must be read from the same places, or an event whose message is
|
|
232
|
+
* nested under `response.error` would contribute text while its verdict went
|
|
233
|
+
* unseen.
|
|
234
|
+
*/
|
|
235
|
+
function boundedBareUpstreamError(payload: unknown): {
|
|
236
|
+
message: string;
|
|
237
|
+
refusalCode: string | undefined;
|
|
238
|
+
} | undefined {
|
|
239
|
+
const root = asJsonRecord(payload);
|
|
240
|
+
if (!root || root.type !== "error") return undefined;
|
|
241
|
+
const message = boundedBareUpstreamErrorMessage(payload);
|
|
242
|
+
const response = asJsonRecord(root.response);
|
|
243
|
+
// Precedence is {@link upstreamErrorMessageFromPayload}'s, so the envelope
|
|
244
|
+
// that supplied the message also supplies the verdict. Taking the FIRST code
|
|
245
|
+
// rather than searching for a refusal is what stops a refusal nested below a
|
|
246
|
+
// transient one from overruling it.
|
|
247
|
+
const code = [
|
|
248
|
+
asJsonRecord(root.error),
|
|
249
|
+
asJsonRecord(root.last_error),
|
|
250
|
+
asJsonRecord(response?.error),
|
|
251
|
+
asJsonRecord(response?.incomplete_details),
|
|
252
|
+
root,
|
|
253
|
+
]
|
|
254
|
+
.map(candidate => stringField(candidate, "code"))
|
|
255
|
+
.find(candidate => candidate !== undefined);
|
|
256
|
+
const refusalCode = code !== undefined
|
|
257
|
+
? (isTerminalRefusalCode(code) ? code : undefined)
|
|
258
|
+
: message === undefined ? undefined : safetyRefusalCodeFromMessage(message);
|
|
259
|
+
if (message !== undefined) return { message, refusalCode };
|
|
260
|
+
if (refusalCode === undefined) return undefined;
|
|
261
|
+
return { message: terminalRefusalFallbackMessage(refusalCode), refusalCode };
|
|
262
|
+
}
|
|
263
|
+
|
|
171
264
|
export type SseTerminalOutputBoundary = {
|
|
172
265
|
feed(chunk: Uint8Array): Uint8Array;
|
|
173
266
|
finish(): Uint8Array;
|
|
174
267
|
terminalSeen(): boolean;
|
|
175
268
|
doneSeen(): boolean;
|
|
176
269
|
upstreamError(): string | undefined;
|
|
270
|
+
upstreamRefusalCode(): string | undefined;
|
|
177
271
|
dispose(): void;
|
|
178
272
|
};
|
|
179
273
|
|
|
@@ -196,11 +290,12 @@ export function createSseTerminalOutputBoundary(
|
|
|
196
290
|
let pendingDone: { block: Uint8Array; delimiter: Uint8Array } | null = null;
|
|
197
291
|
let disposed = false;
|
|
198
292
|
let upstreamError: string | undefined;
|
|
293
|
+
let upstreamRefusalCode: string | undefined;
|
|
199
294
|
|
|
200
295
|
const processFrames = (
|
|
201
296
|
frames: ReturnType<BoundedSseFrameBuffer["feed"]>,
|
|
202
297
|
): Uint8Array => {
|
|
203
|
-
if (disposed || terminal || frames.length === 0) return
|
|
298
|
+
if (disposed || terminal || frames.length === 0) return EMPTY_BYTES;
|
|
204
299
|
const output: Uint8Array[] = [];
|
|
205
300
|
let responsesTerminal = false;
|
|
206
301
|
for (const frame of frames) {
|
|
@@ -209,8 +304,11 @@ export function createSseTerminalOutputBoundary(
|
|
|
209
304
|
const parsed = payload === null ? undefined : parseSsePayload(payload);
|
|
210
305
|
// Observe on the client reader itself: a tee inspection branch may lag
|
|
211
306
|
// behind EOF, so its log context cannot determine the outgoing terminal.
|
|
212
|
-
const
|
|
213
|
-
if (
|
|
307
|
+
const bare = boundedBareUpstreamError(parsed);
|
|
308
|
+
if (bare !== undefined) {
|
|
309
|
+
upstreamError = bare.message;
|
|
310
|
+
upstreamRefusalCode = bare.refusalCode;
|
|
311
|
+
}
|
|
214
312
|
const safetyBuffering = dropSafetyBuffering && parsed !== undefined
|
|
215
313
|
? codexSafetyBufferingBlockAction(parsed) : "keep";
|
|
216
314
|
if (safetyBuffering === "drop") continue;
|
|
@@ -259,13 +357,13 @@ export function createSseTerminalOutputBoundary(
|
|
|
259
357
|
|
|
260
358
|
return {
|
|
261
359
|
feed(chunk) {
|
|
262
|
-
if (disposed || terminal) return
|
|
360
|
+
if (disposed || terminal) return EMPTY_BYTES;
|
|
263
361
|
return processFrames(framer.feed(chunk));
|
|
264
362
|
},
|
|
265
363
|
finish() {
|
|
266
|
-
if (disposed || terminal) return
|
|
364
|
+
if (disposed || terminal) return EMPTY_BYTES;
|
|
267
365
|
const tail = framer.finish();
|
|
268
|
-
if (tail.byteLength === 0) return
|
|
366
|
+
if (tail.byteLength === 0) return EMPTY_BYTES;
|
|
269
367
|
// EOF may cut off the final SSE block before its blank-line delimiter.
|
|
270
368
|
// Feed it through the exact same parser/rewrite/terminal path as a
|
|
271
369
|
// complete frame, using a synthetic delimiter so the client receives a
|
|
@@ -277,6 +375,7 @@ export function createSseTerminalOutputBoundary(
|
|
|
277
375
|
terminalSeen: () => terminal,
|
|
278
376
|
doneSeen: () => done,
|
|
279
377
|
upstreamError: () => upstreamError,
|
|
378
|
+
upstreamRefusalCode: () => upstreamRefusalCode,
|
|
280
379
|
dispose() {
|
|
281
380
|
if (disposed) return;
|
|
282
381
|
disposed = true;
|
|
@@ -348,7 +447,11 @@ export function relaySseWithFailedTail(
|
|
|
348
447
|
const upstreamError = terminalBoundary.upstreamError() ?? opts?.upstreamError;
|
|
349
448
|
controller.enqueue(upstreamError === undefined
|
|
350
449
|
? adapterEofIncompleteFrame(encoder)
|
|
351
|
-
: upstreamErrorTailFrame(
|
|
450
|
+
: upstreamErrorTailFrame(
|
|
451
|
+
encoder,
|
|
452
|
+
upstreamError,
|
|
453
|
+
terminalBoundary.upstreamRefusalCode(),
|
|
454
|
+
));
|
|
352
455
|
controller.enqueue(doneFrame(encoder));
|
|
353
456
|
}
|
|
354
457
|
terminalBoundary.dispose();
|
|
@@ -359,7 +462,7 @@ export function relaySseWithFailedTail(
|
|
|
359
462
|
if (result !== "buffered") return;
|
|
360
463
|
}
|
|
361
464
|
} catch (err) {
|
|
362
|
-
let partial: Uint8Array =
|
|
465
|
+
let partial: Uint8Array = EMPTY_BYTES;
|
|
363
466
|
let tailTerminal = false;
|
|
364
467
|
try {
|
|
365
468
|
partial = terminalBoundary.finish();
|
|
@@ -377,7 +480,11 @@ export function relaySseWithFailedTail(
|
|
|
377
480
|
if (!terminalBoundary.doneSeen()) controller.enqueue(doneFrame(encoder));
|
|
378
481
|
} else {
|
|
379
482
|
// Leading blank line terminates a partial SSE block so the failed frame parses cleanly.
|
|
380
|
-
|
|
483
|
+
const refusalCode = terminalBoundary.upstreamRefusalCode();
|
|
484
|
+
const refusalMessage = terminalBoundary.upstreamError();
|
|
485
|
+
controller.enqueue(refusalCode !== undefined && refusalMessage !== undefined
|
|
486
|
+
? refusalFailedTailFrame(encoder, refusalMessage, refusalCode)
|
|
487
|
+
: failedTailFrame(encoder, err));
|
|
381
488
|
}
|
|
382
489
|
controller.close();
|
|
383
490
|
} catch { /* client already torn down */ }
|
|
@@ -403,15 +510,7 @@ export function nextSseBlock(buffer: string): { block: string; delimiter: string
|
|
|
403
510
|
};
|
|
404
511
|
}
|
|
405
512
|
|
|
406
|
-
export
|
|
407
|
-
const data: string[] = [];
|
|
408
|
-
for (const line of block.split(/\r?\n/)) {
|
|
409
|
-
if (!line.startsWith("data:")) continue;
|
|
410
|
-
const value = line.slice(5);
|
|
411
|
-
data.push(value.startsWith(" ") ? value.slice(1) : value);
|
|
412
|
-
}
|
|
413
|
-
return data.length > 0 ? data.join("\n") : null;
|
|
414
|
-
}
|
|
513
|
+
export { sseDataPayload } from "./sse-payload-rewrite";
|
|
415
514
|
|
|
416
515
|
type JsonRecord = Record<string, unknown>;
|
|
417
516
|
|
|
@@ -727,6 +826,9 @@ export function responseWithDeferredRequestLog(
|
|
|
727
826
|
// convention for a client cancellation or upstream read failure.
|
|
728
827
|
const status = reason === "cancel" ? 499 : reason === "read_error" ? 502 : response.status;
|
|
729
828
|
addFinalRequestLog(requestId, start, logCtx, status, {
|
|
829
|
+
...(reason === "eof" && logCtx.observedTerminalStatus
|
|
830
|
+
? { terminalStatus: logCtx.observedTerminalStatus }
|
|
831
|
+
: {}),
|
|
730
832
|
closeReason: reason === "cancel" ? "client_cancel" : "non_stream",
|
|
731
833
|
}, addLog);
|
|
732
834
|
},
|
|
@@ -943,8 +1045,8 @@ export function createSseInspector(handlers: SseInspectorHandlers): SseInspector
|
|
|
943
1045
|
let reported = false;
|
|
944
1046
|
let sawTerminal = false;
|
|
945
1047
|
let disposed = false;
|
|
946
|
-
let delimiterTail: Uint8Array =
|
|
947
|
-
let candidate: Uint8Array =
|
|
1048
|
+
let delimiterTail: Uint8Array = EMPTY_BYTES;
|
|
1049
|
+
let candidate: Uint8Array = EMPTY_BYTES;
|
|
948
1050
|
let candidateBytes = 0;
|
|
949
1051
|
let discardingOversizedFrame = false;
|
|
950
1052
|
const reportFirstOutput = createFirstOutputReporter(handlers.onFirstOutput);
|
|
@@ -957,8 +1059,8 @@ export function createSseInspector(handlers: SseInspectorHandlers): SseInspector
|
|
|
957
1059
|
let firstResponseId: string | undefined;
|
|
958
1060
|
|
|
959
1061
|
const clearFrameState = (): void => {
|
|
960
|
-
delimiterTail =
|
|
961
|
-
candidate =
|
|
1062
|
+
delimiterTail = EMPTY_BYTES;
|
|
1063
|
+
candidate = EMPTY_BYTES;
|
|
962
1064
|
candidateBytes = 0;
|
|
963
1065
|
discardingOversizedFrame = false;
|
|
964
1066
|
};
|
|
@@ -995,9 +1097,9 @@ export function createSseInspector(handlers: SseInspectorHandlers): SseInspector
|
|
|
995
1097
|
};
|
|
996
1098
|
|
|
997
1099
|
const takeCandidate = (): Uint8Array => {
|
|
998
|
-
if (candidateBytes === 0) return
|
|
1100
|
+
if (candidateBytes === 0) return EMPTY_BYTES;
|
|
999
1101
|
const frame = candidate.slice(0, candidateBytes);
|
|
1000
|
-
candidate =
|
|
1102
|
+
candidate = EMPTY_BYTES;
|
|
1001
1103
|
candidateBytes = 0;
|
|
1002
1104
|
return frame;
|
|
1003
1105
|
};
|
|
@@ -1010,7 +1112,7 @@ export function createSseInspector(handlers: SseInspectorHandlers): SseInspector
|
|
|
1010
1112
|
Math.min(nextBytes, MAX_INSPECTION_SSE_FRAME_BYTES),
|
|
1011
1113
|
);
|
|
1012
1114
|
if (nextBytes > MAX_INSPECTION_SSE_FRAME_BYTES) {
|
|
1013
|
-
candidate =
|
|
1115
|
+
candidate = EMPTY_BYTES;
|
|
1014
1116
|
candidateBytes = 0;
|
|
1015
1117
|
discardingOversizedFrame = true;
|
|
1016
1118
|
inspectionCounters.frameCapOverflows += 1;
|
|
@@ -1173,7 +1275,7 @@ export function createSseInspector(handlers: SseInspectorHandlers): SseInspector
|
|
|
1173
1275
|
|
|
1174
1276
|
const scanChunk = (chunk: Uint8Array): void => {
|
|
1175
1277
|
const previousTail = delimiterTail;
|
|
1176
|
-
delimiterTail =
|
|
1278
|
+
delimiterTail = EMPTY_BYTES;
|
|
1177
1279
|
const tailLength = previousTail.byteLength;
|
|
1178
1280
|
const totalLength = tailLength + chunk.byteLength;
|
|
1179
1281
|
const byteAt = (index: number): number => index < tailLength
|
|
@@ -1219,7 +1321,7 @@ export function createSseInspector(handlers: SseInspectorHandlers): SseInspector
|
|
|
1219
1321
|
if (disposed) return;
|
|
1220
1322
|
try {
|
|
1221
1323
|
retainCandidateSlice(delimiterTail);
|
|
1222
|
-
delimiterTail =
|
|
1324
|
+
delimiterTail = EMPTY_BYTES;
|
|
1223
1325
|
if (!discardingOversizedFrame && candidateBytes > 0 && !reported) {
|
|
1224
1326
|
const sourceBytes = candidateBytes;
|
|
1225
1327
|
const decoded = decoder!.decode(takeCandidate());
|
|
@@ -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,
|
|
@@ -44,6 +46,7 @@ import {
|
|
|
44
46
|
usageStatusForFinalLog,
|
|
45
47
|
usageTotalTokens,
|
|
46
48
|
type AttemptRecoveryKind,
|
|
49
|
+
type AttemptRecoveryWithheld,
|
|
47
50
|
type CacheTelemetryProvenance,
|
|
48
51
|
type PersistedRequestSpend,
|
|
49
52
|
type PersistedUsageAttempt,
|
|
@@ -66,10 +69,15 @@ import { inferCursorContextWindow } from "../adapters/cursor/discovery";
|
|
|
66
69
|
import { KIRO_MODEL_CONTEXT_WINDOWS, normalizeKiroModelId } from "../providers/kiro-models";
|
|
67
70
|
import { DEVIN_MODEL_CONTEXT_WINDOWS } from "../adapters/devin/live-models";
|
|
68
71
|
import { modelRecordValue } from "../reasoning-effort";
|
|
72
|
+
import type { RequestMetricsRecorder } from "./request-metrics";
|
|
69
73
|
|
|
70
74
|
export interface RequestLogContext {
|
|
71
75
|
model: string;
|
|
72
76
|
provider: string;
|
|
77
|
+
/** Optional process-lifetime aggregate sink, injected by the server composition owner. */
|
|
78
|
+
requestMetricsRecorder?: RequestMetricsRecorder;
|
|
79
|
+
/** Bounded terminal enum observed while inspecting a buffered response body. */
|
|
80
|
+
observedTerminalStatus?: ResponsesTerminalStatus;
|
|
73
81
|
/**
|
|
74
82
|
* Identity of the ONE logical request this context serves (#4546). Set from the execution
|
|
75
83
|
* budget minted at ingress; a retry leg, a repair refetch and a combo child share it.
|
|
@@ -290,6 +298,7 @@ export interface RequestLogEntry {
|
|
|
290
298
|
}
|
|
291
299
|
|
|
292
300
|
const requestLog: RequestLogEntry[] = [];
|
|
301
|
+
const requestLogObserversForTests = new Set<(entry: RequestLogEntry) => void>();
|
|
293
302
|
const MAX_LOG_SIZE = 2000;
|
|
294
303
|
const requestLogEntryBytes = new WeakMap<RequestLogEntry, number>();
|
|
295
304
|
let requestLogBytes = 0;
|
|
@@ -490,6 +499,9 @@ export function addRequestLog(entry: RequestLogEntry) {
|
|
|
490
499
|
else if (retained !== entry) delete retained.claudeCompatibility;
|
|
491
500
|
entry = retained;
|
|
492
501
|
retainRequestLogEntry(entry);
|
|
502
|
+
for (const observer of requestLogObserversForTests) {
|
|
503
|
+
try { observer(entry); } catch { /* test observation must never fail request logging */ }
|
|
504
|
+
}
|
|
493
505
|
try {
|
|
494
506
|
// Failure diagnostics survive the 200-entry ring buffer by riding the persisted
|
|
495
507
|
// usage entry (devlog/_plan/260716_claudecode_hardening/030). Success rows stay
|
|
@@ -565,6 +577,12 @@ export function addRequestLog(entry: RequestLogEntry) {
|
|
|
565
577
|
}
|
|
566
578
|
}
|
|
567
579
|
|
|
580
|
+
/** Test-only finalized-row observation without polling the management projection. */
|
|
581
|
+
export function observeRequestLogsForTests(observer: (entry: RequestLogEntry) => void): () => void {
|
|
582
|
+
requestLogObserversForTests.add(observer);
|
|
583
|
+
return () => { requestLogObserversForTests.delete(observer); };
|
|
584
|
+
}
|
|
585
|
+
|
|
568
586
|
export function nextRequestLogId(_timestamp = Date.now()): string {
|
|
569
587
|
return `ocx-${randomBytes(16).toString("hex")}`;
|
|
570
588
|
}
|
|
@@ -713,7 +731,15 @@ export function requestLogErrorCode(
|
|
|
713
731
|
}
|
|
714
732
|
return "permission_denied";
|
|
715
733
|
}
|
|
716
|
-
if (status === 429)
|
|
734
|
+
if (status === 429) {
|
|
735
|
+
// A refused ambiguous reset answers 429 by design (it must not invite a client
|
|
736
|
+
// retry that could duplicate inference); classify it by its message so the log
|
|
737
|
+
// distinguishes a proxy refusal from provider throttling.
|
|
738
|
+
if (upstreamError?.trim() && isUpstreamResetReplayRefusedMessage(upstreamError)) {
|
|
739
|
+
return UPSTREAM_RESET_REPLAY_REFUSED_CODE;
|
|
740
|
+
}
|
|
741
|
+
return "rate_limit_exceeded";
|
|
742
|
+
}
|
|
717
743
|
if (status === 503) return "server_is_overloaded";
|
|
718
744
|
if (status >= 500) return "upstream_server_error";
|
|
719
745
|
return `http_${status}`;
|
|
@@ -765,6 +791,22 @@ export function catalogModelSupportsServiceTier(modelId: string, serviceTier: st
|
|
|
765
791
|
|
|
766
792
|
export function applyResponseLogMetadata(logCtx: RequestLogContext, payload: unknown): void {
|
|
767
793
|
if (!payload || typeof payload !== "object") return;
|
|
794
|
+
if (logCtx.observedTerminalStatus === undefined) {
|
|
795
|
+
const eventType = (payload as { type?: unknown }).type;
|
|
796
|
+
const response = (payload as { response?: unknown }).response;
|
|
797
|
+
const responseStatus = response && typeof response === "object"
|
|
798
|
+
? (response as { status?: unknown }).status
|
|
799
|
+
: (payload as { status?: unknown }).status;
|
|
800
|
+
if (eventType === "response.completed") {
|
|
801
|
+
logCtx.observedTerminalStatus = "completed";
|
|
802
|
+
} else if (eventType === "response.failed") {
|
|
803
|
+
logCtx.observedTerminalStatus = "failed";
|
|
804
|
+
} else if (eventType === "response.incomplete") {
|
|
805
|
+
logCtx.observedTerminalStatus = "incomplete";
|
|
806
|
+
} else if (responseStatus === "completed" || responseStatus === "failed" || responseStatus === "incomplete") {
|
|
807
|
+
logCtx.observedTerminalStatus = responseStatus;
|
|
808
|
+
}
|
|
809
|
+
}
|
|
768
810
|
const source = "response" in payload && typeof (payload as { response?: unknown }).response === "object"
|
|
769
811
|
? (payload as { response?: unknown }).response
|
|
770
812
|
: payload;
|
|
@@ -854,7 +896,13 @@ export function usageFromResponsesPayload(usage: unknown): OcxUsage | undefined
|
|
|
854
896
|
* Mark a refusal this proxy synthesized locally. Sets origin to `synthetic` and a
|
|
855
897
|
* distinct local reason so the request log cannot be read as an upstream overload.
|
|
856
898
|
*/
|
|
857
|
-
|
|
899
|
+
// Typed by the two fields it writes rather than by the whole context: the durable-spend tracker
|
|
900
|
+
// has to mark a row from a narrow view of it, and widening that view to the full context there
|
|
901
|
+
// would pull the entire log shape into a module that touches two of its fields.
|
|
902
|
+
export function markLocalRequestLogRefusal(
|
|
903
|
+
logCtx: Pick<RequestLogContext, "localTerminalReason" | "terminalSource">,
|
|
904
|
+
reason: string,
|
|
905
|
+
): void {
|
|
858
906
|
logCtx.localTerminalReason = reason;
|
|
859
907
|
logCtx.terminalSource = "synthetic";
|
|
860
908
|
}
|
|
@@ -1300,6 +1348,7 @@ export function addFinalRequestLog(
|
|
|
1300
1348
|
const attempts = logCtx.attempts?.map(attempt => ({
|
|
1301
1349
|
...attempt,
|
|
1302
1350
|
recoveryKinds: [...attempt.recoveryKinds],
|
|
1351
|
+
...(attempt.recoveryWithheld?.length ? { recoveryWithheld: [...attempt.recoveryWithheld] } : {}),
|
|
1303
1352
|
...(attempt.usage ? { usage: { ...attempt.usage } } : {}),
|
|
1304
1353
|
...(attempt.tierOutcome ? { tierOutcome: { ...attempt.tierOutcome } } : {}),
|
|
1305
1354
|
}));
|
|
@@ -1309,6 +1358,17 @@ export function addFinalRequestLog(
|
|
|
1309
1358
|
const usageStatus = aggregate?.status ?? existing.status;
|
|
1310
1359
|
const totalTokens = aggregate?.totalTokens ?? existing.totalTokens;
|
|
1311
1360
|
const spend = requestSpendRecord(logCtx, attempts);
|
|
1361
|
+
const durationMs = Date.now() - start;
|
|
1362
|
+
logCtx.requestMetricsRecorder?.recordFinalRequest({
|
|
1363
|
+
...(logCtx.inboundProtocol ? { protocol: logCtx.inboundProtocol } : {}),
|
|
1364
|
+
status: effectiveStatus,
|
|
1365
|
+
durationMs,
|
|
1366
|
+
...(logCtx.firstOutputMs !== undefined ? { firstOutputMs: logCtx.firstOutputMs } : {}),
|
|
1367
|
+
...(meta?.terminalStatus ? { terminalStatus: meta.terminalStatus } : {}),
|
|
1368
|
+
...(closeReason ? { closeReason } : {}),
|
|
1369
|
+
...(attempts !== undefined ? { attempts } : {}),
|
|
1370
|
+
...(spend ? { spendSends: spend.sends } : {}),
|
|
1371
|
+
});
|
|
1312
1372
|
const cacheProvenance = classifyCacheTelemetryProvenance(loggedUsage, {
|
|
1313
1373
|
wireParsed: logCtx.usageWireParsed === true,
|
|
1314
1374
|
});
|
|
@@ -1356,7 +1416,7 @@ export function addFinalRequestLog(
|
|
|
1356
1416
|
: {}),
|
|
1357
1417
|
...(logCtx.resolvedModel ? { resolvedModel: logCtx.resolvedModel } : {}),
|
|
1358
1418
|
status: effectiveStatus,
|
|
1359
|
-
durationMs
|
|
1419
|
+
durationMs,
|
|
1360
1420
|
...(logCtx.firstOutputMs !== undefined ? { firstOutputMs: logCtx.firstOutputMs } : {}),
|
|
1361
1421
|
...(errorCode ? { errorCode } : {}),
|
|
1362
1422
|
...(meta?.terminalStatus ? { terminalStatus: meta.terminalStatus } : {}),
|
|
@@ -1703,6 +1763,22 @@ export function noteAttemptSend(
|
|
|
1703
1763
|
}
|
|
1704
1764
|
}
|
|
1705
1765
|
|
|
1766
|
+
/**
|
|
1767
|
+
* Record that a recovery this attempt was eligible for did not happen.
|
|
1768
|
+
*
|
|
1769
|
+
* Deliberately NOT `noteAttemptSend`: nothing was sent, so `sendCount` must not move. The two
|
|
1770
|
+
* together are what make a one-send log readable — no kind and no withheld reason means nothing
|
|
1771
|
+
* was eligible, a withheld reason means something was and the budget refused it (#5044).
|
|
1772
|
+
*/
|
|
1773
|
+
export function noteAttemptRecoveryWithheld(
|
|
1774
|
+
attempt: PersistedUsageAttempt | undefined,
|
|
1775
|
+
reason: AttemptRecoveryWithheld,
|
|
1776
|
+
): void {
|
|
1777
|
+
if (!attempt) return;
|
|
1778
|
+
if (!attempt.recoveryWithheld) attempt.recoveryWithheld = [];
|
|
1779
|
+
if (!attempt.recoveryWithheld.includes(reason)) attempt.recoveryWithheld.push(reason);
|
|
1780
|
+
}
|
|
1781
|
+
|
|
1706
1782
|
export function finishRequestAttempt(
|
|
1707
1783
|
attempt: PersistedUsageAttempt,
|
|
1708
1784
|
status: number,
|
|
@@ -1785,4 +1861,5 @@ export function clearRequestLogsForTests(): void {
|
|
|
1785
1861
|
requestLog.length = 0;
|
|
1786
1862
|
requestLogBytes = 0;
|
|
1787
1863
|
requestLogsHydratedFromDisk = false;
|
|
1864
|
+
requestLogObserversForTests.clear();
|
|
1788
1865
|
}
|