@bitkyc08/opencodex 2.55.0 → 2.57.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/bin/ocx.mjs +10 -0
- package/gui/dist/assets/{index-BBOZWGB6.css → index-C5-RdDmD.css} +1 -1
- package/gui/dist/assets/{index-VuoiWj9J.js → index-Cz7CLdif.js} +21 -21
- package/gui/dist/index.html +2 -2
- package/package.json +4 -3
- package/src/adapters/base.ts +21 -0
- package/src/adapters/codebuddy/adapter.ts +2 -1
- package/src/adapters/codebuddy/scaffold-guard.ts +248 -0
- package/src/adapters/command-code.ts +1 -1
- package/src/adapters/cursor/envelope-echo.ts +8 -2
- package/src/adapters/cursor/transport-retry.ts +46 -1
- package/src/adapters/cursor.ts +4 -0
- package/src/adapters/google.ts +7 -7
- package/src/adapters/kiro/adapter.ts +42 -1
- package/src/adapters/kiro/payload.ts +17 -3
- package/src/adapters/kiro/reasoning.ts +70 -7
- package/src/adapters/kiro/stream.ts +8 -2
- package/src/adapters/kiro/wire.ts +2 -1
- package/src/adapters/kiro-events.ts +21 -13
- package/src/adapters/kiro-retry.ts +23 -4
- package/src/adapters/openai-chat/errors.ts +116 -0
- package/src/adapters/openai-chat/messages.ts +346 -0
- package/src/adapters/openai-chat/passthrough.ts +146 -0
- package/src/adapters/openai-chat/response-events.ts +117 -0
- package/src/adapters/openai-chat/tool-call-validation.ts +200 -0
- package/src/adapters/openai-chat/tool-name-registry.ts +166 -0
- package/src/adapters/openai-chat/tool-schema.ts +495 -0
- package/src/adapters/openai-chat/wire.ts +50 -0
- package/src/adapters/openai-chat.ts +40 -1452
- package/src/adapters/openai-responses/canonical-forward.ts +202 -0
- package/src/adapters/openai-responses/image-gen.ts +406 -0
- package/src/adapters/openai-responses/internal.ts +3 -0
- package/src/adapters/openai-responses/passthrough.ts +642 -0
- package/src/adapters/openai-responses/prompt-cache.ts +83 -0
- package/src/adapters/openai-responses/reasoning.ts +220 -0
- package/src/adapters/openai-responses/request-strips.ts +185 -0
- package/src/adapters/openai-responses/tool-output-recovery.ts +509 -0
- package/src/adapters/openai-responses/tool-schema.ts +293 -0
- package/src/adapters/openai-responses/web-search.ts +156 -0
- package/src/adapters/openai-responses.ts +4 -2625
- package/src/bridge/errors.ts +58 -0
- package/src/bridge/internal.ts +174 -0
- package/src/bridge/response-json.ts +630 -0
- package/src/bridge/sse.ts +1462 -0
- package/src/bridge.ts +5 -2204
- package/src/chat/inbound.ts +12 -1
- package/src/claude/desktop-profile.ts +66 -9
- package/src/claude/outbound.ts +18 -0
- package/src/cli/account-main.ts +1 -1
- package/src/cli/capabilities.ts +2 -2
- package/src/cli/combo.ts +10 -1
- package/src/cli/index.ts +48 -5
- package/src/cli/registry.ts +2 -1
- package/src/cli/system-command.ts +4 -4
- package/src/clients/config-export.ts +7 -3
- package/src/codex/account-label.ts +14 -3
- package/src/codex/account-lifecycle.ts +3 -0
- package/src/codex/account-store.ts +184 -35
- package/src/codex/account-usability.ts +21 -0
- package/src/codex/auth-api/account-list.ts +507 -0
- package/src/codex/auth-api/http.ts +32 -0
- package/src/codex/auth-api/login-flow.ts +566 -0
- package/src/codex/auth-api/login-state.ts +64 -0
- package/src/codex/auth-api/main-account-probe.ts +331 -0
- package/src/codex/auth-api/pool-mode-gate.ts +274 -0
- package/src/codex/auth-api/pool-quota-probe.ts +512 -0
- package/src/codex/auth-api/reset-credit-service.ts +431 -0
- package/src/codex/auth-api/routes.ts +425 -0
- package/src/codex/auth-api/runtime-config.ts +48 -0
- package/src/codex/auth-api.ts +27 -3118
- package/src/codex/auth-context.ts +252 -35
- package/src/codex/catalog/aggregation.ts +80 -1
- package/src/codex/catalog/auto-review.ts +507 -0
- package/src/codex/catalog/build-entries.ts +981 -0
- package/src/codex/catalog/combo-member.ts +375 -0
- package/src/codex/catalog/derive-entry.ts +229 -0
- package/src/codex/catalog/effort.ts +0 -1
- package/src/codex/catalog/gated-native-warn.ts +63 -0
- package/src/codex/catalog/gather-capture.ts +533 -0
- package/src/codex/catalog/model-hints.ts +691 -0
- package/src/codex/catalog/model-visibility.ts +305 -0
- package/src/codex/catalog/provider-fetch.ts +52 -2942
- package/src/codex/catalog/provider-models.ts +685 -0
- package/src/codex/catalog/remote.ts +30 -0
- package/src/codex/catalog/restore.ts +132 -0
- package/src/codex/catalog/retained-sync.ts +714 -0
- package/src/codex/catalog/routed-gather.ts +895 -0
- package/src/codex/catalog/subagent-roster.ts +176 -0
- package/src/codex/catalog/sync.ts +52 -2698
- package/src/codex/cli-install-provenance.ts +7 -1
- package/src/codex/convergence.ts +7 -2
- package/src/codex/desktop-app/types.ts +11 -2
- package/src/codex/desktop-app/windows.ts +5 -5
- package/src/codex/inject/config-toml.ts +563 -0
- package/src/codex/inject/remove.ts +192 -0
- package/src/codex/inject/restore.ts +567 -0
- package/src/codex/inject/routing-classify.ts +109 -0
- package/src/codex/inject/routing-target.ts +125 -0
- package/src/codex/inject.ts +89 -1444
- package/src/codex/lineage.ts +458 -0
- package/src/codex/model-entitlements.ts +152 -15
- package/src/codex/pool-refresh-backoff.ts +161 -0
- package/src/codex/quota-rejection.ts +104 -15
- package/src/codex/routing/active-account.ts +194 -0
- package/src/codex/routing/cache-affinity.ts +70 -0
- package/src/codex/routing/cooldown-math.ts +285 -0
- package/src/codex/routing/health-store.ts +402 -0
- package/src/codex/routing/probe-lease.ts +358 -0
- package/src/codex/routing/selection.ts +780 -0
- package/src/codex/routing/thread-affinity.ts +586 -0
- package/src/codex/routing/transient-hold-dispatch.ts +141 -0
- package/src/codex/routing.ts +370 -2271
- package/src/codex/shim-fingerprint.ts +223 -0
- package/src/codex/shim-inspect.ts +175 -0
- package/src/codex/shim-probe.ts +367 -0
- package/src/codex/shim-restore-lock.ts +169 -0
- package/src/codex/shim-state-file.ts +151 -0
- package/src/codex/shim-templates.ts +265 -0
- package/src/codex/shim.ts +48 -1268
- package/src/codex/warmup.ts +1 -1
- package/src/combos/failover.ts +85 -0
- package/src/combos/request.ts +17 -10
- package/src/combos/types.ts +23 -2
- package/src/config/diagnostics.ts +705 -0
- package/src/config/feature-flags.ts +55 -0
- package/src/config/live-reconcile.ts +403 -0
- package/src/config/load-degrade.ts +880 -0
- package/src/config/mutation-lock.ts +244 -0
- package/src/config/openai-tier-backup.ts +268 -0
- package/src/config/pending-teardown.ts +31 -0
- package/src/config/persist-unlocked.ts +92 -0
- package/src/config/proxy-env.ts +188 -0
- package/src/config/salvage.ts +244 -0
- package/src/config/schema/config-schema.ts +640 -0
- package/src/config/schema/leaf-validators.ts +855 -0
- package/src/config/warn-memo.ts +28 -0
- package/src/config.ts +234 -4481
- package/src/generated/compatibility-version.json +649 -121
- package/src/images/loop.ts +1 -1
- package/src/lib/errors.ts +17 -0
- package/src/lib/request-execution-budget.ts +198 -23
- package/src/lib/spend-reservation-ledger.ts +958 -0
- package/src/lib/state-store-registrations.ts +6 -2
- package/src/lib/test-home-guard.ts +85 -1
- package/src/lib/upstream-retry.ts +132 -21
- package/src/lib/windows-elevation.ts +76 -14
- package/src/lib/workflow-budget.ts +553 -30
- package/src/oauth/index.ts +2 -2
- package/src/oauth/key-providers.ts +2 -2
- package/src/providers/kiro-models.ts +4 -3
- package/src/providers/label.ts +19 -1
- package/src/providers/model-discovery.ts +16 -0
- package/src/providers/quota/account-cache.ts +441 -0
- package/src/providers/quota/antigravity.ts +295 -0
- package/src/providers/quota/report-cache.ts +320 -0
- package/src/providers/quota/vendor-probes-key.ts +1243 -0
- package/src/providers/quota/vendor-probes-oauth.ts +590 -0
- package/src/providers/quota.ts +324 -3079
- package/src/providers/registry/entries-core.ts +1228 -0
- package/src/providers/registry/entries-extended.ts +1213 -0
- package/src/providers/registry/model-seeds.ts +912 -0
- package/src/providers/registry/types.ts +352 -0
- package/src/providers/registry.ts +24 -3536
- package/src/responses/continuation-ownership.ts +29 -0
- package/src/responses/reasoning-envelope.ts +6 -3
- package/src/responses/state/replay-fingerprint.ts +80 -0
- package/src/responses/state/snapshot-codec.ts +104 -0
- package/src/responses/state/spill-failure.ts +118 -0
- package/src/responses/state/spill-queue.ts +665 -0
- package/src/responses/state/temp-recovery.ts +257 -0
- package/src/responses/state.ts +82 -1143
- package/src/routing/identity-domains.ts +456 -0
- package/src/routing/probe-lease.ts +613 -0
- package/src/server/chat-completions.ts +3 -1
- package/src/server/chat-native.ts +37 -9
- package/src/server/index/bounded-request.ts +88 -0
- package/src/server/index/live-sideband.ts +601 -0
- package/src/server/index/serve-options.ts +1766 -0
- package/src/server/index/startup-warnings.ts +213 -0
- package/src/server/index/websocket-handler.ts +339 -0
- package/src/server/index.ts +45 -2552
- package/src/server/inspection-tee.ts +107 -0
- package/src/server/live.ts +46 -1
- package/src/server/management/combo-routes.ts +10 -1
- package/src/server/management/route-registry.ts +26 -23
- package/src/server/management/shared.ts +8 -5
- package/src/server/management/workflow-budget-routes.ts +133 -0
- package/src/server/management-api.ts +12 -0
- package/src/server/relay-eager.ts +2 -0
- package/src/server/relay.ts +14 -19
- package/src/server/request-log-conversation.ts +9 -7
- package/src/server/request-log.ts +372 -4
- package/src/server/response-log-body.ts +153 -0
- package/src/server/responses/account-change-state.ts +307 -0
- package/src/server/responses/adapter-continuation.ts +540 -0
- package/src/server/responses/adapter-delivery.ts +208 -0
- package/src/server/responses/adapter-dispatch.ts +1042 -0
- package/src/server/responses/codex-ws-wire.ts +5 -0
- package/src/server/responses/collaboration.ts +74 -4
- package/src/server/responses/combo-session-recall.ts +68 -8
- package/src/server/responses/compact.ts +113 -17
- package/src/server/responses/completion-policy.ts +33 -0
- package/src/server/responses/core-auth.ts +529 -0
- package/src/server/responses/core-codex-account.ts +907 -0
- package/src/server/responses/core-combo-failure.ts +210 -0
- package/src/server/responses/core-combo.ts +787 -0
- package/src/server/responses/core-errors.ts +170 -0
- package/src/server/responses/core-lifetime.ts +95 -0
- package/src/server/responses/core-normalize.ts +350 -0
- package/src/server/responses/core-opaque-recovery.ts +380 -0
- package/src/server/responses/core-options.ts +159 -0
- package/src/server/responses/core-replay.ts +298 -0
- package/src/server/responses/core.ts +192 -8893
- package/src/server/responses/encrypted-payload.ts +0 -1
- package/src/server/responses/input-admission.ts +126 -6
- package/src/server/responses/passthrough-delivery.ts +869 -0
- package/src/server/responses/passthrough-dispatch.ts +1494 -0
- package/src/server/responses/passthrough-error.ts +38 -2
- package/src/server/responses/passthrough-execution.ts +54 -0
- package/src/server/responses/request-prepare.ts +1080 -0
- package/src/server/responses/request-send-budget.ts +259 -0
- package/src/server/responses/request-sidecar-auth.ts +149 -0
- package/src/server/responses/request-spend.ts +147 -0
- package/src/server/responses/request-transport.ts +803 -0
- package/src/server/responses/response-effects.ts +157 -0
- package/src/server/responses/run-turn-execution.ts +476 -0
- package/src/server/responses/sidecar-execution.ts +463 -0
- package/src/server/responses/terminal-guard.ts +65 -4
- package/src/server/responses-image-gen-repair.ts +1 -1
- package/src/server/responses-undeclared-tool-guard.ts +9 -5
- package/src/server/workflow-refusal.ts +84 -0
- package/src/service/windows-ops.ts +210 -16
- package/src/service/windows-scheduler.ts +28 -21
- package/src/service.ts +1 -1
- package/src/types/config.ts +34 -1
- package/src/types/request.ts +8 -5
- package/src/types/tools.ts +24 -0
- package/src/types.ts +2 -0
- package/src/update/index.ts +10 -0
- package/src/update/stop-contract.d.mts +1 -0
- package/src/update/stop-contract.mjs +19 -0
- package/src/update/stop-decision.d.mts +1 -1
- package/src/update/stop-decision.mjs +12 -3
- package/src/usage/log.ts +147 -1
- package/src/usage/summary.ts +171 -21
- package/src/vision/anthropic-describe.ts +1 -1
- package/src/vision/describe.ts +5 -5
- package/src/web-search/anthropic-executor.ts +1 -1
- package/src/web-search/exa-executor.ts +1 -1
- package/src/web-search/executor.ts +1 -1
- package/src/web-search/gemini-executor.ts +1 -1
- package/src/web-search/loop.ts +1 -1
- package/src/web-search/ollama-executor.ts +1 -1
- package/src/web-search/parse.ts +67 -14
- package/src/web-search/passthrough-bridge.ts +64 -31
- package/src/web-search/xai-executor.ts +1 -1
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import { existsSync, readFileSync } from "node:fs";
|
|
2
2
|
import { randomBytes } from "node:crypto";
|
|
3
|
+
import { stampApiKeyAccountLabel, usesApiKeyAccount } from "../providers/label";
|
|
4
|
+
import { KEY_ACCOUNT_LOG_LABEL_RE } from "../codex/account-label";
|
|
5
|
+
import { readBoundedResponseBody } from "../lib/bounded-body";
|
|
3
6
|
import type { ResponsesTerminalStatus } from "../bridge";
|
|
4
7
|
import {
|
|
5
8
|
classifyError,
|
|
@@ -17,28 +20,38 @@ import { readCodexCatalogPath } from "../codex/catalog";
|
|
|
17
20
|
import type { AttemptTierOutcome, OcxProviderConfig, OcxUsage } from "../types";
|
|
18
21
|
import { normalizeRouteDecisionTrace, type RouteDecisionTraceV1 } from "../routing/trace";
|
|
19
22
|
import type { AdapterRequest } from "../adapters/base";
|
|
23
|
+
import type { RequestSpendSettlement } from "./responses/request-spend";
|
|
20
24
|
import type { AdapterTierMetadata } from "../providers/fastwire";
|
|
21
25
|
import { redactSecretString, sanitizeLogMetadataString } from "../lib/redact";
|
|
22
26
|
import {
|
|
23
27
|
appendUsageEntry,
|
|
28
|
+
classifyCacheTelemetryProvenance,
|
|
24
29
|
isKnownAdmissionKind,
|
|
30
|
+
isKnownAffinityMove,
|
|
31
|
+
isKnownAffinityReason,
|
|
32
|
+
isKnownCacheTelemetryProvenance,
|
|
25
33
|
isKnownInboundProtocol,
|
|
26
34
|
isKnownTerminalSource,
|
|
27
35
|
isKnownTransportPhase,
|
|
28
36
|
isKnownUsageSurface,
|
|
29
37
|
isCodexUsageAccountLogLabel,
|
|
38
|
+
isLogicalRequestId,
|
|
30
39
|
isValidReasoningWireValue,
|
|
31
40
|
normalizeClaudeCompatibilityUsageLog,
|
|
41
|
+
normalizeRequestSpend,
|
|
32
42
|
readRecentUsageEntries,
|
|
33
43
|
usageForFinalLog,
|
|
34
44
|
usageStatusForFinalLog,
|
|
35
45
|
usageTotalTokens,
|
|
36
46
|
type AttemptRecoveryKind,
|
|
47
|
+
type CacheTelemetryProvenance,
|
|
48
|
+
type PersistedRequestSpend,
|
|
37
49
|
type PersistedUsageAttempt,
|
|
38
50
|
type PersistedUsageEntry,
|
|
39
51
|
type PersistedClaudeCompatibilityLog,
|
|
40
52
|
type UsageStatus,
|
|
41
53
|
} from "../usage/log";
|
|
54
|
+
import type { RequestExecutionBudget } from "../lib/request-execution-budget";
|
|
42
55
|
import {
|
|
43
56
|
appendUsageDebug,
|
|
44
57
|
isUsageDebugEnabled,
|
|
@@ -57,6 +70,29 @@ import { modelRecordValue } from "../reasoning-effort";
|
|
|
57
70
|
export interface RequestLogContext {
|
|
58
71
|
model: string;
|
|
59
72
|
provider: string;
|
|
73
|
+
/**
|
|
74
|
+
* Identity of the ONE logical request this context serves (#4546). Set from the execution
|
|
75
|
+
* budget minted at ingress; a retry leg, a repair refetch and a combo child share it.
|
|
76
|
+
*/
|
|
77
|
+
logicalRequestId?: string;
|
|
78
|
+
/**
|
|
79
|
+
* Internal live reference to this request's execution budget; omitted from RequestLogEntry and
|
|
80
|
+
* JSONL. Read at final-log time so the row reports the budget's FINAL state rather than a
|
|
81
|
+
* snapshot taken before the recovery legs that the row is meant to explain.
|
|
82
|
+
*/
|
|
83
|
+
executionBudget?: RequestExecutionBudget;
|
|
84
|
+
/**
|
|
85
|
+
* True once usage counts were taken from a response wire rather than reported raw by the
|
|
86
|
+
* adapter. It decides cache provenance: the normalizer writes zero-default token-detail
|
|
87
|
+
* objects, so an all-zero cache detail from a parsed wire is not a measured cache miss.
|
|
88
|
+
*/
|
|
89
|
+
usageWireParsed?: boolean;
|
|
90
|
+
/**
|
|
91
|
+
* Every affinity reason recorded for this request, in order. `affinityReason` keeps the final
|
|
92
|
+
* one for the existing row shape; a request that moved twice has two causes and losing the
|
|
93
|
+
* first one loses the more expensive half of the story.
|
|
94
|
+
*/
|
|
95
|
+
affinityMoveReasons?: CodexAffinityReason[];
|
|
60
96
|
/** TTFT: ms from request start to the first non-empty model output delta (WP4, devlog 040). */
|
|
61
97
|
firstOutputMs?: number;
|
|
62
98
|
/** Best-effort chat/session correlation for Logs grouping (#330). Opaque; omit when unknown. */
|
|
@@ -106,6 +142,15 @@ export interface RequestLogContext {
|
|
|
106
142
|
preserveResolvedModelFromRoute?: boolean;
|
|
107
143
|
usage?: OcxUsage;
|
|
108
144
|
usageLogInputTokens?: number;
|
|
145
|
+
/**
|
|
146
|
+
* The output ceiling this request may actually spend, for the durable spend reservation
|
|
147
|
+
* (#4707). Captured from the caller's `max_output_tokens`; absent when the caller omitted it
|
|
148
|
+
* and the adapter's own provider/model default decides, in which case only the input estimate
|
|
149
|
+
* is reserved up front and settlement corrects it.
|
|
150
|
+
*/
|
|
151
|
+
spendOutputCeilingTokens?: number;
|
|
152
|
+
/** Settles this request's durable spend entries from `addFinalRequestLog`. */
|
|
153
|
+
spendTracker?: RequestSpendSettlement;
|
|
109
154
|
attempts?: PersistedUsageAttempt[];
|
|
110
155
|
/** Internal mutable final attempt; omitted from RequestLogEntry/JSONL. */
|
|
111
156
|
activeAttempt?: PersistedUsageAttempt;
|
|
@@ -143,6 +188,11 @@ export interface RequestLogContext {
|
|
|
143
188
|
affinity?: CodexAffinityMove;
|
|
144
189
|
/** Why the binding was kept, moved, or released (#4546). */
|
|
145
190
|
affinityReason?: CodexAffinityReason;
|
|
191
|
+
/**
|
|
192
|
+
* Set when this request dropped account-bound continuation because the serving
|
|
193
|
+
* Codex pool account was not the issuer. Never an account identifier.
|
|
194
|
+
*/
|
|
195
|
+
conversationStateScrub?: "account-change";
|
|
146
196
|
transportPhase?: "pre_headers" | "mid_stream" | "terminal_sse";
|
|
147
197
|
terminalSource?: "upstream" | "synthetic";
|
|
148
198
|
/** Bounded route-decision trace (RI-01); never contains secrets. */
|
|
@@ -153,6 +203,8 @@ export interface RequestLogContext {
|
|
|
153
203
|
|
|
154
204
|
export interface RequestLogEntry {
|
|
155
205
|
requestId: string;
|
|
206
|
+
/** The logical request this row belongs to (#4546); absent on rows written without a budget. */
|
|
207
|
+
logicalRequestId?: string;
|
|
156
208
|
timestamp: number;
|
|
157
209
|
model: string;
|
|
158
210
|
provider: string;
|
|
@@ -206,13 +258,30 @@ export interface RequestLogEntry {
|
|
|
206
258
|
usage?: OcxUsage;
|
|
207
259
|
totalTokens?: number;
|
|
208
260
|
attempts?: PersistedUsageAttempt[];
|
|
261
|
+
/**
|
|
262
|
+
* Upstream spend for the whole logical request: sends aggregated across attempts and combo
|
|
263
|
+
* children, split into settled and unresolved, with the budget state and move reasons that
|
|
264
|
+
* explain them. Per-attempt `sendCount` stays the accounting source; this is the total.
|
|
265
|
+
*/
|
|
266
|
+
spend?: PersistedRequestSpend;
|
|
267
|
+
/** Whether this row's cache detail was observed, synthesized for the wire, or absent. */
|
|
268
|
+
cacheProvenance?: CacheTelemetryProvenance;
|
|
209
269
|
/** Codex pool affinity decision for this request (diagnostics for #186). */
|
|
210
270
|
affinity?: CodexAffinityMove;
|
|
211
271
|
/** Why that decision was made (#4546): a move is the expensive event, so it names its cause. */
|
|
212
272
|
affinityReason?: CodexAffinityReason;
|
|
273
|
+
/**
|
|
274
|
+
* Set when this request dropped account-bound continuation after a Codex pool
|
|
275
|
+
* account change. Never an account identifier.
|
|
276
|
+
*/
|
|
277
|
+
conversationStateScrub?: "account-change";
|
|
213
278
|
/** Where the upstream terminal/failure was observed. */
|
|
214
279
|
transportPhase?: "pre_headers" | "mid_stream" | "terminal_sse";
|
|
215
|
-
/**
|
|
280
|
+
/**
|
|
281
|
+
* Whether the HTTP status and message originated upstream or were synthesized by this
|
|
282
|
+
* proxy. Covers SSE tails and pre-stream JSON refusals. Management surfaces this so a
|
|
283
|
+
* local refusal cannot be presented as an upstream reason.
|
|
284
|
+
*/
|
|
216
285
|
terminalSource?: "upstream" | "synthetic";
|
|
217
286
|
/** Bounded route-decision trace (RI-01); never contains secrets. */
|
|
218
287
|
routeDecision?: RouteDecisionTraceV1;
|
|
@@ -287,8 +356,10 @@ export function requestLogEntryFromPersistedUsage(entry: PersistedUsageEntry): R
|
|
|
287
356
|
const closeReason = asCloseReason(entry.closeReason);
|
|
288
357
|
const routeDecision = normalizeRouteDecisionTraceForLog(entry.routeDecision);
|
|
289
358
|
const claudeCompatibility = normalizeClaudeCompatibilityUsageLog(entry.claudeCompatibility);
|
|
359
|
+
const spend = normalizeRequestSpend(entry.spend);
|
|
290
360
|
return {
|
|
291
361
|
requestId: entry.requestId,
|
|
362
|
+
...(isLogicalRequestId(entry.logicalRequestId) ? { logicalRequestId: entry.logicalRequestId } : {}),
|
|
292
363
|
timestamp: entry.timestamp,
|
|
293
364
|
model: entry.model,
|
|
294
365
|
provider: entry.provider,
|
|
@@ -328,10 +399,33 @@ export function requestLogEntryFromPersistedUsage(entry: PersistedUsageEntry): R
|
|
|
328
399
|
...(entry.usage ? { usage: entry.usage } : {}),
|
|
329
400
|
...(entry.totalTokens !== undefined ? { totalTokens: entry.totalTokens } : {}),
|
|
330
401
|
...(entry.attempts !== undefined ? { attempts: entry.attempts } : {}),
|
|
402
|
+
...(spend ? { spend } : {}),
|
|
403
|
+
...(isKnownCacheTelemetryProvenance(entry.cacheProvenance)
|
|
404
|
+
? { cacheProvenance: entry.cacheProvenance }
|
|
405
|
+
: {}),
|
|
406
|
+
...persistedAffinityFields(entry),
|
|
331
407
|
...(isKnownTransportPhase(entry.transportPhase) ? { transportPhase: entry.transportPhase } : {}),
|
|
332
408
|
...(isKnownTerminalSource(entry.terminalSource) ? { terminalSource: entry.terminalSource } : {}),
|
|
333
409
|
...(routeDecision ? { routeDecision } : {}),
|
|
334
410
|
...(claudeCompatibility ? { claudeCompatibility } : {}),
|
|
411
|
+
...(entry.conversationStateScrub === "account-change"
|
|
412
|
+
? { conversationStateScrub: "account-change" }
|
|
413
|
+
: {}),
|
|
414
|
+
};
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
/**
|
|
418
|
+
* Affinity survived only in memory before this: `addFinalRequestLog` set it on the row and the
|
|
419
|
+
* field-by-field disk projection never named it, so the move that discarded a warm prefix was
|
|
420
|
+
* gone at the next restart — the same whitelist trap #4592 hit one layer up.
|
|
421
|
+
*/
|
|
422
|
+
function persistedAffinityFields(
|
|
423
|
+
entry: Pick<RequestLogEntry, "affinity" | "affinityReason">,
|
|
424
|
+
): Pick<PersistedUsageEntry, "affinity" | "affinityReason"> {
|
|
425
|
+
if (!isKnownAffinityMove(entry.affinity)) return {};
|
|
426
|
+
return {
|
|
427
|
+
affinity: entry.affinity,
|
|
428
|
+
...(isKnownAffinityReason(entry.affinityReason) ? { affinityReason: entry.affinityReason } : {}),
|
|
335
429
|
};
|
|
336
430
|
}
|
|
337
431
|
|
|
@@ -410,6 +504,7 @@ export function addRequestLog(entry: RequestLogEntry) {
|
|
|
410
504
|
: {};
|
|
411
505
|
appendUsageEntry({
|
|
412
506
|
requestId: entry.requestId,
|
|
507
|
+
...(isLogicalRequestId(entry.logicalRequestId) ? { logicalRequestId: entry.logicalRequestId } : {}),
|
|
413
508
|
timestamp: entry.timestamp,
|
|
414
509
|
provider: entry.provider,
|
|
415
510
|
model: entry.model,
|
|
@@ -451,11 +546,19 @@ export function addRequestLog(entry: RequestLogEntry) {
|
|
|
451
546
|
...(entry.usage ? { usage: entry.usage } : {}),
|
|
452
547
|
...(entry.totalTokens !== undefined ? { totalTokens: entry.totalTokens } : {}),
|
|
453
548
|
...(entry.attempts !== undefined ? { attempts: entry.attempts } : {}),
|
|
549
|
+
...(entry.spend ? { spend: entry.spend } : {}),
|
|
550
|
+
...(isKnownCacheTelemetryProvenance(entry.cacheProvenance)
|
|
551
|
+
? { cacheProvenance: entry.cacheProvenance }
|
|
552
|
+
: {}),
|
|
553
|
+
...persistedAffinityFields(entry),
|
|
454
554
|
...(isKnownTransportPhase(entry.transportPhase) ? { transportPhase: entry.transportPhase } : {}),
|
|
455
555
|
...(isKnownTerminalSource(entry.terminalSource) ? { terminalSource: entry.terminalSource } : {}),
|
|
456
556
|
...failureDiagnostics,
|
|
457
557
|
...(entry.routeDecision ? { routeDecision: entry.routeDecision } : {}),
|
|
458
558
|
...(entry.claudeCompatibility ? { claudeCompatibility: entry.claudeCompatibility } : {}),
|
|
559
|
+
...(entry.conversationStateScrub === "account-change"
|
|
560
|
+
? { conversationStateScrub: "account-change" }
|
|
561
|
+
: {}),
|
|
459
562
|
});
|
|
460
563
|
} catch {
|
|
461
564
|
/* request logging must never fail a user request */
|
|
@@ -682,8 +785,14 @@ export function applyResponseLogMetadata(logCtx: RequestLogContext, payload: unk
|
|
|
682
785
|
}
|
|
683
786
|
const usage = usageFromResponsesPayload((source as { usage?: unknown }).usage);
|
|
684
787
|
if (usage && !logCtx.usageFromBridge) {
|
|
685
|
-
logCtx
|
|
686
|
-
|
|
788
|
+
if (!recordKeyWireAttemptUsage(logCtx, usage)) {
|
|
789
|
+
logCtx.usage = usage;
|
|
790
|
+
if (logCtx.activeAttempt) logCtx.activeAttempt.usage = usage;
|
|
791
|
+
}
|
|
792
|
+
// Counts taken off a wire, not reported raw. The zero-default token-detail objects strict
|
|
793
|
+
// clients require are indistinguishable here from a measured zero, so the cache detail these
|
|
794
|
+
// counts carry is recorded as synthesized rather than as an observed miss.
|
|
795
|
+
logCtx.usageWireParsed = true;
|
|
687
796
|
}
|
|
688
797
|
}
|
|
689
798
|
|
|
@@ -741,6 +850,15 @@ export function usageFromResponsesPayload(usage: unknown): OcxUsage | undefined
|
|
|
741
850
|
return undefined;
|
|
742
851
|
}
|
|
743
852
|
|
|
853
|
+
/**
|
|
854
|
+
* Mark a refusal this proxy synthesized locally. Sets origin to `synthetic` and a
|
|
855
|
+
* distinct local reason so the request log cannot be read as an upstream overload.
|
|
856
|
+
*/
|
|
857
|
+
export function markLocalRequestLogRefusal(logCtx: RequestLogContext, reason: string): void {
|
|
858
|
+
logCtx.localTerminalReason = reason;
|
|
859
|
+
logCtx.terminalSource = "synthetic";
|
|
860
|
+
}
|
|
861
|
+
|
|
744
862
|
export function inspectResponseLogJson(logCtx: RequestLogContext, text: string): void {
|
|
745
863
|
try {
|
|
746
864
|
applyResponseLogMetadata(logCtx, JSON.parse(text));
|
|
@@ -977,6 +1095,158 @@ export function httpStatusForRequestLogTerminal(
|
|
|
977
1095
|
return httpStatusForTerminalStatus(status);
|
|
978
1096
|
}
|
|
979
1097
|
|
|
1098
|
+
/**
|
|
1099
|
+
* Aggregate one logical request's upstream spend from the rows that recorded it.
|
|
1100
|
+
*
|
|
1101
|
+
* Attempts are the accounting source and combo children are attempts of the same context, so a
|
|
1102
|
+
* sum over `logCtx.attempts` is the send count for one user turn — the number the amplification
|
|
1103
|
+
* in #4546 is measured in. A terminal status is what makes a send explainable, so the split is
|
|
1104
|
+
* drawn there rather than at success: a 502 is settled spend, an attempt abandoned in flight is
|
|
1105
|
+
* not. The budget's own counter is folded in as `reserved` because a leg that re-sent without
|
|
1106
|
+
* opening an attempt row is charged and unobserved, and that difference belongs in
|
|
1107
|
+
* `unresolved` rather than quietly inflating `settled`.
|
|
1108
|
+
*/
|
|
1109
|
+
export function requestSpendRecord(
|
|
1110
|
+
logCtx: Pick<RequestLogContext, "executionBudget" | "affinityMoveReasons" | "affinityReason">,
|
|
1111
|
+
attempts: readonly PersistedUsageAttempt[] | undefined,
|
|
1112
|
+
): PersistedRequestSpend | undefined {
|
|
1113
|
+
const rows = attempts ?? [];
|
|
1114
|
+
const budget = logCtx.executionBudget;
|
|
1115
|
+
const reasons = [...new Set(
|
|
1116
|
+
(logCtx.affinityMoveReasons ?? (logCtx.affinityReason ? [logCtx.affinityReason] : []))
|
|
1117
|
+
.filter(isKnownAffinityReason),
|
|
1118
|
+
)];
|
|
1119
|
+
if (rows.length === 0 && !budget && reasons.length === 0) return undefined;
|
|
1120
|
+
const sends = rows.reduce((total, attempt) => total + attempt.sendCount, 0);
|
|
1121
|
+
const settled = rows.reduce(
|
|
1122
|
+
(total, attempt) => attempt.status >= 100 ? total + attempt.sendCount : total,
|
|
1123
|
+
0,
|
|
1124
|
+
);
|
|
1125
|
+
const charged = Math.max(sends, budget?.used ?? 0);
|
|
1126
|
+
return {
|
|
1127
|
+
sends,
|
|
1128
|
+
settled,
|
|
1129
|
+
unresolved: Math.max(0, charged - settled),
|
|
1130
|
+
...(budget ? { reserved: budget.used, policyVersion: budget.policyVersion } : {}),
|
|
1131
|
+
...(reasons.length > 0 ? { moveReasons: reasons } : {}),
|
|
1132
|
+
};
|
|
1133
|
+
}
|
|
1134
|
+
|
|
1135
|
+
/**
|
|
1136
|
+
* Record an affinity decision so both the row's final answer and the sequence survive. A request
|
|
1137
|
+
* that moved for `quota_refusal` and then again for `transient` paid for two discarded prefixes,
|
|
1138
|
+
* and the single-valued field can only report the second.
|
|
1139
|
+
*/
|
|
1140
|
+
export function noteAffinityMove(
|
|
1141
|
+
logCtx: RequestLogContext,
|
|
1142
|
+
move: CodexAffinityMove,
|
|
1143
|
+
reason: CodexAffinityReason,
|
|
1144
|
+
): void {
|
|
1145
|
+
logCtx.affinity = move;
|
|
1146
|
+
logCtx.affinityReason = reason;
|
|
1147
|
+
(logCtx.affinityMoveReasons ??= []).push(reason);
|
|
1148
|
+
}
|
|
1149
|
+
|
|
1150
|
+
/**
|
|
1151
|
+
* The affinity scope a released binding belonged to: one thread, one model lane.
|
|
1152
|
+
*
|
|
1153
|
+
* Both halves are part of the key. A thread holds a separate binding per model lane, so a
|
|
1154
|
+
* quota refusal on one lane and a transient streak on another are two releases; keyed by thread
|
|
1155
|
+
* alone the second overwrites the first and one of the two rows reports a cause that never
|
|
1156
|
+
* happened on it.
|
|
1157
|
+
*/
|
|
1158
|
+
export interface AffinityModelLane {
|
|
1159
|
+
model: string;
|
|
1160
|
+
/** Thread/conversation that owns the binding; omitted when the caller has no thread identity. */
|
|
1161
|
+
conversationId?: string;
|
|
1162
|
+
}
|
|
1163
|
+
|
|
1164
|
+
/**
|
|
1165
|
+
* Release reasons waiting for the request that can report them (#4546, #4598).
|
|
1166
|
+
*
|
|
1167
|
+
* Bounded like the routing-side map it mirrors: this is a diagnostic, and an unbounded map keyed
|
|
1168
|
+
* by conversation is a leak.
|
|
1169
|
+
*/
|
|
1170
|
+
const pendingNoAccountReasons = new Map<string, CodexAffinityReason>();
|
|
1171
|
+
const MAX_PENDING_NO_ACCOUNT_REASONS = 1024;
|
|
1172
|
+
|
|
1173
|
+
function affinityLaneKey(lane: AffinityModelLane): string {
|
|
1174
|
+
return `${lane.conversationId ?? ""}\u0000${lane.model}`;
|
|
1175
|
+
}
|
|
1176
|
+
|
|
1177
|
+
export function noteNoAccountAffinityReason(lane: AffinityModelLane, reason: CodexAffinityReason): void {
|
|
1178
|
+
if (!isKnownAffinityReason(reason)) return;
|
|
1179
|
+
const key = affinityLaneKey(lane);
|
|
1180
|
+
if (!pendingNoAccountReasons.has(key) && pendingNoAccountReasons.size >= MAX_PENDING_NO_ACCOUNT_REASONS) {
|
|
1181
|
+
const oldest = pendingNoAccountReasons.keys().next();
|
|
1182
|
+
if (!oldest.done) pendingNoAccountReasons.delete(oldest.value);
|
|
1183
|
+
}
|
|
1184
|
+
pendingNoAccountReasons.set(key, reason);
|
|
1185
|
+
}
|
|
1186
|
+
|
|
1187
|
+
/** Read and forget one lane's reason. Other lanes on the same thread keep theirs. */
|
|
1188
|
+
export function takeNoAccountAffinityReason(lane: AffinityModelLane): CodexAffinityReason | undefined {
|
|
1189
|
+
const key = affinityLaneKey(lane);
|
|
1190
|
+
const reason = pendingNoAccountReasons.get(key);
|
|
1191
|
+
if (reason !== undefined) pendingNoAccountReasons.delete(key);
|
|
1192
|
+
return reason;
|
|
1193
|
+
}
|
|
1194
|
+
|
|
1195
|
+
/** Test-only process-state reset for isolated harnesses. */
|
|
1196
|
+
export function clearNoAccountAffinityReasonsForTests(): void {
|
|
1197
|
+
pendingNoAccountReasons.clear();
|
|
1198
|
+
}
|
|
1199
|
+
|
|
1200
|
+
/**
|
|
1201
|
+
* Report a selection that produced no account, on the request that failed because of it.
|
|
1202
|
+
*
|
|
1203
|
+
* A no-account resolve reaches no auth context, so until now its cause was handed to whichever
|
|
1204
|
+
* later resolve happened to succeed — and a pool that stays exhausted never produces one, leaving
|
|
1205
|
+
* the failure permanently unexplained. Attaching the reason to THIS request's own record is what
|
|
1206
|
+
* makes the failure self-describing: the row is written, persisted and hydrated like any other,
|
|
1207
|
+
* and it survives a restart.
|
|
1208
|
+
*
|
|
1209
|
+
* Deliberately not a separate synthetic row. `/api/usage` counts one row as one request, so an
|
|
1210
|
+
* extra event row would report a request that never existed and skew the very cost totals this
|
|
1211
|
+
* work exists to make trustworthy.
|
|
1212
|
+
*/
|
|
1213
|
+
export function recordNoAccountAffinityFailure(
|
|
1214
|
+
logCtx: RequestLogContext,
|
|
1215
|
+
lane: AffinityModelLane,
|
|
1216
|
+
reason?: CodexAffinityReason,
|
|
1217
|
+
): CodexAffinityReason | undefined {
|
|
1218
|
+
const resolved = isKnownAffinityReason(reason) ? reason : takeNoAccountAffinityReason(lane);
|
|
1219
|
+
if (resolved === undefined) return undefined;
|
|
1220
|
+
noteAffinityMove(logCtx, "cleared", resolved);
|
|
1221
|
+
logCtx.errorCode ??= "codex_no_account";
|
|
1222
|
+
return resolved;
|
|
1223
|
+
}
|
|
1224
|
+
// Attempt identity can change in place while a combo parent retains an older context copy.
|
|
1225
|
+
// These objects own their usage even after a rotation to an unknown key identity.
|
|
1226
|
+
const keyUsageOwners = new WeakSet<PersistedUsageAttempt>();
|
|
1227
|
+
const keyWireUsageBaselines = new WeakMap<PersistedUsageAttempt, OcxUsage | undefined>();
|
|
1228
|
+
|
|
1229
|
+
function cloneKeyUsage(usage: OcxUsage | undefined): OcxUsage | undefined {
|
|
1230
|
+
return usage ? { ...usage } : undefined;
|
|
1231
|
+
}
|
|
1232
|
+
|
|
1233
|
+
/** Replace this physical send's wire snapshot against the pre-send baseline; repeats do not sum. */
|
|
1234
|
+
export function recordKeyWireAttemptUsage(logCtx: RequestLogContext, usage: OcxUsage | undefined): boolean {
|
|
1235
|
+
if (!usage) return false;
|
|
1236
|
+
const attempt = logCtx.activeAttempt;
|
|
1237
|
+
if (!attempt || !keyUsageOwners.has(attempt) || !keyWireUsageBaselines.has(attempt)) return false;
|
|
1238
|
+
const baseline = keyWireUsageBaselines.get(attempt);
|
|
1239
|
+
const current = { ...usage };
|
|
1240
|
+
attempt.usage = baseline
|
|
1241
|
+
? aggregateAttemptUsage([
|
|
1242
|
+
{ ...attempt, usage: baseline, usageStatus: baseline.estimated ? "estimated" : "reported" },
|
|
1243
|
+
{ ...attempt, usage: current, usageStatus: current.estimated ? "estimated" : "reported" },
|
|
1244
|
+
]).usage
|
|
1245
|
+
: current;
|
|
1246
|
+
logCtx.usage = attempt.usage;
|
|
1247
|
+
return true;
|
|
1248
|
+
}
|
|
1249
|
+
|
|
980
1250
|
export function addFinalRequestLog(
|
|
981
1251
|
requestId: string,
|
|
982
1252
|
start: number,
|
|
@@ -1007,13 +1277,19 @@ export function addFinalRequestLog(
|
|
|
1007
1277
|
logCtx.activeAttempt,
|
|
1008
1278
|
effectiveStatus,
|
|
1009
1279
|
Date.now() - (logCtx.activeAttemptStartedAt ?? start),
|
|
1010
|
-
logCtx.
|
|
1280
|
+
keyUsageOwners.has(logCtx.activeAttempt)
|
|
1281
|
+
? logCtx.activeAttempt.usage
|
|
1282
|
+
: logCtx.usage,
|
|
1011
1283
|
);
|
|
1012
1284
|
// The final row and its active physical attempt describe the same terminal. Preserve the
|
|
1013
1285
|
// semantic code on both so detailed attempt telemetry cannot regress to a generic status code.
|
|
1014
1286
|
if (errorCode) logCtx.activeAttempt.errorCode = errorCode;
|
|
1015
1287
|
else delete logCtx.activeAttempt.errorCode;
|
|
1016
1288
|
}
|
|
1289
|
+
// The one seam every request passes exactly once, whatever transport served it and however
|
|
1290
|
+
// it ended. The terminal usage belongs to the last send that left; the ledger resolves every
|
|
1291
|
+
// earlier send of this request as unresolved spend rather than handing its tokens back.
|
|
1292
|
+
logCtx.spendTracker?.settle(logCtx.usage);
|
|
1017
1293
|
const existing = finalizedUsage(
|
|
1018
1294
|
logCtx.providerAdapter ?? logCtx.provider,
|
|
1019
1295
|
logCtx.usage,
|
|
@@ -1032,6 +1308,11 @@ export function addFinalRequestLog(
|
|
|
1032
1308
|
const loggedUsage = aggregate?.usage ?? existing.usage;
|
|
1033
1309
|
const usageStatus = aggregate?.status ?? existing.status;
|
|
1034
1310
|
const totalTokens = aggregate?.totalTokens ?? existing.totalTokens;
|
|
1311
|
+
const spend = requestSpendRecord(logCtx, attempts);
|
|
1312
|
+
const cacheProvenance = classifyCacheTelemetryProvenance(loggedUsage, {
|
|
1313
|
+
wireParsed: logCtx.usageWireParsed === true,
|
|
1314
|
+
});
|
|
1315
|
+
const logicalRequestId = logCtx.logicalRequestId ?? logCtx.executionBudget?.logicalRequestId;
|
|
1035
1316
|
// Sanitize at the logging layer, not only at the one call site that populates this today.
|
|
1036
1317
|
// The value originates in an upstream-supplied model id, so an unsanitized newline would
|
|
1037
1318
|
// let a single field forge a record boundary in any line-oriented log viewer. Doing it here
|
|
@@ -1041,6 +1322,7 @@ export function addFinalRequestLog(
|
|
|
1041
1322
|
const claudeCompatibility = normalizeClaudeCompatibilityUsageLog(logCtx.claudeCompatibility);
|
|
1042
1323
|
addLog({
|
|
1043
1324
|
requestId,
|
|
1325
|
+
...(isLogicalRequestId(logicalRequestId) ? { logicalRequestId } : {}),
|
|
1044
1326
|
timestamp: start,
|
|
1045
1327
|
model: isCombo ? logCtx.requestedModel! : logCtx.model,
|
|
1046
1328
|
provider: isCombo ? "combo" : logCtx.provider,
|
|
@@ -1084,8 +1366,16 @@ export function addFinalRequestLog(
|
|
|
1084
1366
|
...(loggedUsage ? { usage: loggedUsage } : {}),
|
|
1085
1367
|
...(totalTokens !== undefined ? { totalTokens } : {}),
|
|
1086
1368
|
...(attempts !== undefined ? { attempts } : {}),
|
|
1369
|
+
...(spend ? { spend } : {}),
|
|
1370
|
+
// "unknown" is recorded rather than omitted whenever usage exists: a row that reported tokens
|
|
1371
|
+
// with no cache detail at all is a different fact from a row with no usage, and the summary
|
|
1372
|
+
// has to refuse both as a hit-rate denominator.
|
|
1373
|
+
...(loggedUsage || cacheProvenance !== "unknown" ? { cacheProvenance } : {}),
|
|
1087
1374
|
...(logCtx.affinity ? { affinity: logCtx.affinity } : {}),
|
|
1088
1375
|
...(logCtx.affinityReason ? { affinityReason: logCtx.affinityReason } : {}),
|
|
1376
|
+
...(logCtx.conversationStateScrub === "account-change"
|
|
1377
|
+
? { conversationStateScrub: "account-change" }
|
|
1378
|
+
: {}),
|
|
1089
1379
|
...(logCtx.transportPhase ? { transportPhase: logCtx.transportPhase } : {}),
|
|
1090
1380
|
...(logCtx.terminalSource ? { terminalSource: logCtx.terminalSource } : {}),
|
|
1091
1381
|
...(logCtx.routeDecision ? { routeDecision: logCtx.routeDecision } : {}),
|
|
@@ -1284,6 +1574,84 @@ export function sealRequestAttemptIdentity(
|
|
|
1284
1574
|
attempt.provider = provider;
|
|
1285
1575
|
attempt.adapter = adapter;
|
|
1286
1576
|
if (isCodexUsageAccountLogLabel(accountLogLabel)) attempt.accountLogLabel = accountLogLabel;
|
|
1577
|
+
else delete attempt.accountLogLabel;
|
|
1578
|
+
}
|
|
1579
|
+
|
|
1580
|
+
/** Preserve metered JSON failures before key recovery consumes/cancels their body. */
|
|
1581
|
+
export async function recordKeyAttemptFailure(logCtx: RequestLogContext, response: Response, signal?: AbortSignal): Promise<void> {
|
|
1582
|
+
const attempt = logCtx.activeAttempt;
|
|
1583
|
+
if (!attempt || !KEY_ACCOUNT_LOG_LABEL_RE.test(attempt.accountLogLabel ?? "")) return;
|
|
1584
|
+
attempt.status = response.status;
|
|
1585
|
+
const cancelOriginal = (): void => { try { void response.body?.cancel().catch(() => {}); } catch { /* closed */ } };
|
|
1586
|
+
signal?.addEventListener("abort", cancelOriginal, { once: true });
|
|
1587
|
+
try {
|
|
1588
|
+
if (signal?.aborted) { cancelOriginal(); return; }
|
|
1589
|
+
const body = await readBoundedResponseBody(response.clone(), { signal, totalTimeoutMs: 1000, inactivityTimeoutMs: 1000 });
|
|
1590
|
+
if (body.truncated || body.oversized) return;
|
|
1591
|
+
const value = JSON.parse(body.text);
|
|
1592
|
+
const usage = usageFromResponsesPayload(value?.usage ?? value?.response?.usage);
|
|
1593
|
+
if (usage) recordKeyWireAttemptUsage(logCtx, usage);
|
|
1594
|
+
} catch { /* Absent/malformed usage remains unknown; recovery still owns the response. */ }
|
|
1595
|
+
finally { signal?.removeEventListener("abort", cancelOriginal); }
|
|
1596
|
+
}
|
|
1597
|
+
|
|
1598
|
+
/** Add raw per-response usage before a bridge combines multiple rounds for the client. */
|
|
1599
|
+
export function recordKeyAttemptUsage(logCtx: RequestLogContext, usage: OcxUsage | undefined): void {
|
|
1600
|
+
const attempt = logCtx.activeAttempt;
|
|
1601
|
+
if (!attempt || !usage) return;
|
|
1602
|
+
attempt.usage = attempt.usage
|
|
1603
|
+
? aggregateAttemptUsage([{ ...attempt, usageStatus: attempt.usage.estimated ? "estimated" : "reported" },
|
|
1604
|
+
{ ...attempt, usage, usageStatus: usage.estimated ? "estimated" : "reported" }]).usage
|
|
1605
|
+
: { ...usage };
|
|
1606
|
+
logCtx.usage = attempt.usage;
|
|
1607
|
+
}
|
|
1608
|
+
|
|
1609
|
+
/** A stable active object lets combo/stream callbacks keep pointing at the final attempt.
|
|
1610
|
+
* Earlier key segments are immutable, flat snapshots inserted before that active object. */
|
|
1611
|
+
export function noteProviderAttemptSend(
|
|
1612
|
+
logCtx: RequestLogContext,
|
|
1613
|
+
providerName: string,
|
|
1614
|
+
provider: OcxProviderConfig,
|
|
1615
|
+
inputTokenEstimate: number | undefined,
|
|
1616
|
+
recovery?: AttemptRecoveryKind,
|
|
1617
|
+
): void {
|
|
1618
|
+
const attempt = logCtx.activeAttempt;
|
|
1619
|
+
const previous = attempt?.accountLogLabel;
|
|
1620
|
+
stampApiKeyAccountLabel(logCtx, providerName, provider);
|
|
1621
|
+
const next = logCtx.accountLogLabel;
|
|
1622
|
+
if (attempt && usesApiKeyAccount(provider)) keyUsageOwners.add(attempt);
|
|
1623
|
+
if (attempt && attempt.sendCount > 0 && previous !== next
|
|
1624
|
+
&& (KEY_ACCOUNT_LOG_LABEL_RE.test(previous ?? "") || KEY_ACCOUNT_LOG_LABEL_RE.test(next ?? ""))) {
|
|
1625
|
+
// An input estimate is not evidence that a failed send used that many tokens.
|
|
1626
|
+
delete attempt.inputTokenEstimate;
|
|
1627
|
+
finishRequestAttempt(attempt, attempt.status >= 100 ? attempt.status
|
|
1628
|
+
: recovery === "key-401" ? 401 : recovery?.includes("429") ? 429 : 502,
|
|
1629
|
+
Date.now() - (logCtx.activeAttemptStartedAt ?? Date.now()), attempt.usage);
|
|
1630
|
+
const completed = { ...attempt, recoveryKinds: [...attempt.recoveryKinds],
|
|
1631
|
+
...(attempt.usage ? { usage: { ...attempt.usage } } : {}),
|
|
1632
|
+
...(attempt.tierOutcome ? { tierOutcome: { ...attempt.tierOutcome } } : {}) };
|
|
1633
|
+
const attempts = logCtx.attempts ??= [attempt];
|
|
1634
|
+
const index = attempts.indexOf(attempt);
|
|
1635
|
+
if (index >= 0) attempts.splice(index, 0, completed);
|
|
1636
|
+
else attempts.push(completed, attempt);
|
|
1637
|
+
const fresh = beginRequestAttempt(completed.ordinal + 1, providerName, completed.model, completed.adapter);
|
|
1638
|
+
// Effort/tier metadata describes the request and is captured before the physical send.
|
|
1639
|
+
for (const key of ["requestedEffort", "effectiveEffort", "reasoningWireField", "reasoningWireValue", "tierOutcome"] as const) {
|
|
1640
|
+
if (completed[key] !== undefined) Object.assign(fresh, { [key]: completed[key] });
|
|
1641
|
+
}
|
|
1642
|
+
for (const key of Object.keys(attempt)) delete (attempt as unknown as Record<string, unknown>)[key];
|
|
1643
|
+
Object.assign(attempt, fresh);
|
|
1644
|
+
delete logCtx.usage;
|
|
1645
|
+
logCtx.activeAttemptStartedAt = Date.now();
|
|
1646
|
+
}
|
|
1647
|
+
if (attempt) {
|
|
1648
|
+
sealRequestAttemptIdentity(attempt, logCtx.provider, attempt.adapter, next);
|
|
1649
|
+
recordAttemptCredentialSource(attempt, providerName, provider, attempt.adapter);
|
|
1650
|
+
}
|
|
1651
|
+
noteAttemptSend(attempt, inputTokenEstimate, recovery);
|
|
1652
|
+
if (attempt && keyUsageOwners.has(attempt)) {
|
|
1653
|
+
keyWireUsageBaselines.set(attempt, cloneKeyUsage(attempt.usage));
|
|
1654
|
+
}
|
|
1287
1655
|
}
|
|
1288
1656
|
|
|
1289
1657
|
/** Capture only the resolved upstream route; inbound auth and today's config cannot label old usage. */
|