@bitkyc08/opencodex 2.56.0 → 2.58.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-D4zuyIxQ.js → index-BbrHOIY0.js} +21 -21
- package/gui/dist/assets/{index-BBOZWGB6.css → index-C5-RdDmD.css} +1 -1
- package/gui/dist/index.html +2 -2
- package/package.json +4 -4
- package/src/adapters/codebuddy/adapter.ts +2 -1
- package/src/adapters/codebuddy/scaffold-guard.ts +249 -0
- package/src/adapters/command-code.ts +12 -3
- package/src/adapters/cursor/cursor-errors.ts +15 -0
- package/src/adapters/cursor/discovery.ts +65 -1
- package/src/adapters/cursor/envelope-echo.ts +8 -2
- package/src/adapters/cursor/live-transport.ts +5 -1
- package/src/adapters/cursor/protobuf-events.ts +110 -11
- package/src/adapters/cursor/protobuf-request.ts +19 -1
- package/src/adapters/cursor/text-toolcall.ts +230 -0
- package/src/adapters/cursor/thread-continuity.ts +67 -0
- package/src/adapters/cursor/types.ts +5 -0
- package/src/adapters/cursor.ts +55 -5
- package/src/adapters/google-http.ts +38 -13
- package/src/adapters/google.ts +7 -7
- 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/mimo-free.ts +32 -17
- package/src/adapters/ollama-native.ts +42 -8
- package/src/adapters/openai-chat/tool-name-registry.ts +166 -0
- package/src/adapters/openai-chat/tool-schema.ts +25 -7
- package/src/adapters/openai-chat.ts +8 -8
- package/src/adapters/openai-responses/passthrough.ts +62 -5
- package/src/adapters/openai-responses/request-strips.ts +43 -0
- package/src/adapters/physical-send.ts +50 -0
- package/src/bridge/errors.ts +26 -2
- package/src/bridge/response-json.ts +8 -2
- package/src/bridge/sse.ts +20 -2
- package/src/claude/desktop-profile.ts +66 -9
- package/src/claude/outbound.ts +32 -4
- 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/config-command.ts +35 -18
- package/src/cli/dispatch.ts +17 -4
- package/src/cli/index.ts +92 -7
- package/src/cli/registry.ts +2 -1
- package/src/cli/system-command.ts +74 -5
- package/src/cli/uninstall-client-state.ts +12 -0
- package/src/clients/config-export.ts +7 -3
- package/src/codex/account-label.ts +14 -3
- package/src/codex/account-store.ts +113 -26
- package/src/codex/account-usability.ts +21 -0
- package/src/codex/auth-api/login-flow.ts +14 -2
- package/src/codex/auth-api/reset-credit-service.ts +11 -2
- package/src/codex/auth-context.ts +199 -15
- package/src/codex/catalog/aggregation.ts +80 -1
- package/src/codex/catalog/model-visibility.ts +1 -0
- package/src/codex/catalog/remote.ts +30 -0
- package/src/codex/catalog/retained-sync.ts +9 -1
- package/src/codex/catalog/routed-gather.ts +38 -1
- 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/desktop-switches.ts +145 -0
- package/src/codex/history-job.ts +5 -1
- package/src/codex/history-provider.ts +33 -4
- package/src/codex/history-worker.ts +14 -1
- package/src/codex/inject/remove.ts +145 -7
- package/src/codex/inject/restore.ts +231 -32
- package/src/codex/inject.ts +12 -16
- package/src/codex/loopback-target.ts +9 -0
- package/src/codex/model-entitlements.ts +152 -15
- package/src/codex/native-profile-startup.ts +64 -20
- package/src/codex/pool-refresh-backoff.ts +12 -3
- package/src/codex/quota-rejection.ts +104 -15
- package/src/codex/routing/cache-affinity.ts +70 -0
- package/src/codex/routing/cooldown-math.ts +10 -0
- package/src/codex/routing/selection.ts +79 -2
- package/src/codex/routing/thread-affinity.ts +50 -2
- package/src/codex/routing/transient-hold-dispatch.ts +141 -0
- package/src/codex/routing.ts +29 -49
- 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/atomic-write.ts +83 -8
- package/src/config/pending-teardown.ts +31 -0
- package/src/config/schema/config-schema.ts +2 -0
- package/src/config/schema/leaf-validators.ts +1 -0
- package/src/generated/compatibility-version.json +272 -180
- package/src/images/loop.ts +1 -1
- package/src/lib/bounded-subprocess.ts +62 -10
- package/src/lib/errors.ts +17 -0
- package/src/lib/request-execution-budget.ts +147 -21
- package/src/lib/spend-reservation-ledger.ts +18 -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 +77 -10
- package/src/lib/windows-elevation.ts +76 -14
- package/src/lib/windows-secret-acl.ts +151 -15
- package/src/lib/windows-user-principal.ts +5 -1
- package/src/oauth/index.ts +2 -2
- package/src/oauth/key-providers.ts +2 -2
- package/src/providers/derive.ts +6 -0
- package/src/providers/kiro-models.ts +4 -3
- package/src/providers/label.ts +19 -1
- package/src/providers/model-discovery.ts +35 -7
- package/src/providers/registry/entries-core.ts +18 -0
- package/src/providers/registry/entries-extended.ts +59 -28
- package/src/providers/registry/model-seeds.ts +71 -17
- package/src/providers/registry/types.ts +9 -0
- package/src/responses/reasoning-envelope.ts +6 -3
- package/src/responses/spill-store.ts +17 -0
- package/src/responses/state/body-policy.ts +25 -0
- package/src/responses/state/spill-queue.ts +8 -6
- package/src/responses/state.ts +3 -22
- package/src/router.ts +4 -0
- package/src/routing/identity-domains.ts +21 -14
- package/src/routing/probe-lease.ts +103 -1
- package/src/server/auth-cors.ts +1 -0
- package/src/server/chat-completions.ts +3 -1
- package/src/server/chat-native.ts +37 -9
- package/src/server/index/live-sideband.ts +37 -1
- package/src/server/index/websocket-handler.ts +54 -3
- package/src/server/index.ts +5 -5
- 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/config-routes.ts +27 -5
- package/src/server/models-capabilities.ts +24 -3
- package/src/server/relay-eager.ts +2 -0
- package/src/server/relay.ts +14 -19
- package/src/server/request-log.ts +127 -3
- package/src/server/response-log-body.ts +153 -0
- package/src/server/responses/account-change-state.ts +74 -0
- package/src/server/responses/adapter-continuation.ts +33 -7
- package/src/server/responses/adapter-delivery.ts +5 -11
- package/src/server/responses/adapter-dispatch.ts +84 -13
- package/src/server/responses/codex-ws-exchange.ts +65 -4
- 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/combo-stream-preflight.ts +68 -5
- package/src/server/responses/compact.ts +54 -13
- package/src/server/responses/core-auth.ts +2 -0
- package/src/server/responses/core-codex-account.ts +51 -3
- package/src/server/responses/core-combo.ts +129 -23
- package/src/server/responses/core-errors.ts +18 -0
- package/src/server/responses/core-options.ts +3 -0
- package/src/server/responses/core-replay.ts +105 -32
- package/src/server/responses/core.ts +3 -3
- package/src/server/responses/encrypted-payload.ts +0 -1
- package/src/server/responses/fetch-helpers.ts +4 -1
- package/src/server/responses/input-admission.ts +126 -6
- package/src/server/responses/native-injection-protocol.ts +42 -0
- package/src/server/responses/native-injection-replay.ts +105 -0
- package/src/server/responses/native-injection.ts +242 -0
- package/src/server/responses/native-response-control.ts +56 -0
- package/src/server/responses/native-response-json.ts +14 -0
- package/src/server/responses/native-response-output.ts +37 -0
- package/src/server/responses/native-steering-log.ts +44 -0
- package/src/server/responses/native-steering-policy.ts +49 -0
- package/src/server/responses/native-steering-replay.ts +126 -0
- package/src/server/responses/native-steering-settings.ts +76 -0
- package/src/server/responses/native-steering.ts +400 -0
- package/src/server/responses/native-tool-results.ts +130 -0
- package/src/server/responses/passthrough-delivery.ts +30 -6
- package/src/server/responses/passthrough-dispatch.ts +61 -11
- package/src/server/responses/passthrough-error.ts +38 -2
- package/src/server/responses/request-prepare.ts +173 -22
- package/src/server/responses/request-send-budget.ts +97 -2
- package/src/server/responses/request-spend.ts +147 -0
- package/src/server/responses/request-transport.ts +62 -3
- package/src/server/responses/run-turn-execution.ts +59 -31
- package/src/server/responses/sidecar-execution.ts +7 -13
- package/src/server/responses/terminal-guard.ts +65 -4
- package/src/server/responses/ws-upstream.ts +21 -1
- package/src/server/responses-undeclared-tool-guard.ts +9 -5
- package/src/server/stop-teardown.ts +8 -1
- package/src/server/ws-bridge.ts +16 -1
- package/src/service/cli.ts +13 -1
- 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 +8 -1
- package/src/types/provider.ts +13 -0
- 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 +1 -1
- 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
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/** Read-ahead allowance, not a lifetime limit on SSE inspection or delivery. */
|
|
2
|
+
export const MAX_INSPECTION_READ_AHEAD_BYTES = 32 * 1024 * 1024;
|
|
3
|
+
|
|
4
|
+
export interface InspectionTeeOptions {
|
|
5
|
+
/** Release pacing so the inspection owner's existing bounded drain can run. */
|
|
6
|
+
clientGoneSignal?: AbortSignal;
|
|
7
|
+
/** Internal test seam; callers must not take this value from upstream data. */
|
|
8
|
+
maxReadAheadBytes?: number;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Keep native tee cancellation semantics while pacing the inspection branch.
|
|
13
|
+
*
|
|
14
|
+
* Inspection may lead raw client consumption by the allowance plus one source
|
|
15
|
+
* chunk (and native tee prefetch). It never stops merely because the whole turn
|
|
16
|
+
* crossed that size: terminal, usage and continuation observers retain ownership.
|
|
17
|
+
* Count raw client bytes BEFORE rewrites, which may shrink, drop or expand them.
|
|
18
|
+
* Push sources still need their own producer-side bound; this is not an RSS cap.
|
|
19
|
+
*/
|
|
20
|
+
export function teeWithBoundedInspection(
|
|
21
|
+
source: ReadableStream<Uint8Array>,
|
|
22
|
+
options: InspectionTeeOptions = {},
|
|
23
|
+
): [ReadableStream<Uint8Array>, ReadableStream<Uint8Array>] {
|
|
24
|
+
const limit = options.maxReadAheadBytes ?? MAX_INSPECTION_READ_AHEAD_BYTES;
|
|
25
|
+
if (!Number.isSafeInteger(limit) || limit <= 0) {
|
|
26
|
+
throw new RangeError("Inspection read-ahead limit must be a positive safe integer");
|
|
27
|
+
}
|
|
28
|
+
const [client, inspection] = source.tee();
|
|
29
|
+
let leadBytes = 0;
|
|
30
|
+
let pacingReleased = false;
|
|
31
|
+
let credit: Promise<void> | undefined;
|
|
32
|
+
let wake: (() => void) | undefined;
|
|
33
|
+
|
|
34
|
+
const wakeReader = () => {
|
|
35
|
+
const resolve = wake;
|
|
36
|
+
wake = undefined;
|
|
37
|
+
credit = undefined;
|
|
38
|
+
resolve?.();
|
|
39
|
+
};
|
|
40
|
+
const releasePacing = () => {
|
|
41
|
+
pacingReleased = true;
|
|
42
|
+
wakeReader();
|
|
43
|
+
};
|
|
44
|
+
const signal = options.clientGoneSignal;
|
|
45
|
+
if (signal?.aborted) releasePacing();
|
|
46
|
+
else signal?.addEventListener("abort", releasePacing, { once: true });
|
|
47
|
+
|
|
48
|
+
const wrap = (
|
|
49
|
+
body: ReadableStream<Uint8Array>,
|
|
50
|
+
isClient: boolean,
|
|
51
|
+
): ReadableStream<Uint8Array> => {
|
|
52
|
+
const reader = body.getReader();
|
|
53
|
+
let ended = false;
|
|
54
|
+
// An upstream error must wake an inspector waiting for credit, not remain
|
|
55
|
+
// hidden until the client happens to issue another read. One observer per
|
|
56
|
+
// reader, not a permanent Promise.race reaction attached on every chunk.
|
|
57
|
+
void reader.closed.catch(releasePacing);
|
|
58
|
+
const releaseLock = () => {
|
|
59
|
+
try { reader.releaseLock(); } catch { /* a pending read owns the lock */ }
|
|
60
|
+
};
|
|
61
|
+
const finish = () => {
|
|
62
|
+
ended = true;
|
|
63
|
+
releasePacing();
|
|
64
|
+
if (!isClient) signal?.removeEventListener("abort", releasePacing);
|
|
65
|
+
};
|
|
66
|
+
return new ReadableStream<Uint8Array>({
|
|
67
|
+
async pull(controller) {
|
|
68
|
+
if (ended) return;
|
|
69
|
+
if (!isClient && !pacingReleased && leadBytes >= limit) {
|
|
70
|
+
credit ??= new Promise<void>(resolve => { wake = resolve; });
|
|
71
|
+
await credit;
|
|
72
|
+
if (ended) return;
|
|
73
|
+
}
|
|
74
|
+
try {
|
|
75
|
+
const result = await reader.read();
|
|
76
|
+
if (ended) return;
|
|
77
|
+
if (result.done) {
|
|
78
|
+
finish();
|
|
79
|
+
releaseLock();
|
|
80
|
+
controller.close();
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
if (isClient) {
|
|
84
|
+
leadBytes -= result.value.byteLength;
|
|
85
|
+
if (leadBytes < limit) wakeReader();
|
|
86
|
+
} else {
|
|
87
|
+
leadBytes += result.value.byteLength;
|
|
88
|
+
}
|
|
89
|
+
controller.enqueue(result.value);
|
|
90
|
+
} catch (error) {
|
|
91
|
+
if (ended) return;
|
|
92
|
+
finish();
|
|
93
|
+
releaseLock();
|
|
94
|
+
controller.error(error);
|
|
95
|
+
}
|
|
96
|
+
},
|
|
97
|
+
cancel(reason) {
|
|
98
|
+
finish();
|
|
99
|
+
// Awaiting one tee branch's cancellation waits for the sibling. The
|
|
100
|
+
// owner must be free to finish cleanup and cancel/read that sibling.
|
|
101
|
+
void reader.cancel(reason).catch(() => undefined);
|
|
102
|
+
releaseLock();
|
|
103
|
+
},
|
|
104
|
+
}, { highWaterMark: 0 });
|
|
105
|
+
};
|
|
106
|
+
return [wrap(client, true), wrap(inspection, false)];
|
|
107
|
+
}
|
package/src/server/live.ts
CHANGED
|
@@ -72,7 +72,17 @@ export const LIVE_SIDEBAND_API_ROOT = "https://api.openai.com/v1";
|
|
|
72
72
|
* Client protocol headers relayed verbatim to the upstream on call-create and sideband upgrade.
|
|
73
73
|
* `openai-alpha: quicksilver=v2` carries the Frameless protocol negotiation — without it the
|
|
74
74
|
* ChatGPT backend validates the type-less Frameless session as v1 quicksilver and 400s
|
|
75
|
-
* (openai/codex `realtime_request_headers`, core/src/realtime_conversation.rs).
|
|
75
|
+
* (openai/codex `realtime_request_headers`, core/src/realtime_conversation.rs).
|
|
76
|
+
*
|
|
77
|
+
* `x-codex-turn-metadata` is on the same list upstream builds for the sideband upgrade and was
|
|
78
|
+
* missing here, so every realtime turn reached the model with metadata the client had attached
|
|
79
|
+
* and this proxy silently dropped. The Responses passthrough already forwards it
|
|
80
|
+
* (`src/adapters/openai-responses/passthrough.ts`); the sideband goes to the same realtime
|
|
81
|
+
* upstream the caller was addressing, so there is nothing to scope it away from. That is not
|
|
82
|
+
* true of the images sidecar, which strips it deliberately and keeps doing so.
|
|
83
|
+
*
|
|
84
|
+
* Every name here is relayed only when the caller sent it. Nothing on this list is invented,
|
|
85
|
+
* which is what keeps a caller that omits one byte-identical upstream. Auth headers
|
|
76
86
|
* (`authorization`, `chatgpt-account-id`) stay proxy-owned and are never taken from this list.
|
|
77
87
|
*/
|
|
78
88
|
export const LIVE_CLIENT_PROTOCOL_HEADERS = [
|
|
@@ -82,6 +92,7 @@ export const LIVE_CLIENT_PROTOCOL_HEADERS = [
|
|
|
82
92
|
"thread-id",
|
|
83
93
|
"originator",
|
|
84
94
|
"x-oai-attestation",
|
|
95
|
+
"x-codex-turn-metadata",
|
|
85
96
|
] as const;
|
|
86
97
|
|
|
87
98
|
/**
|
|
@@ -128,6 +139,40 @@ export function logLiveSidebandFrame(dir: "c2u" | "u2c", data: unknown): void {
|
|
|
128
139
|
}
|
|
129
140
|
}
|
|
130
141
|
|
|
142
|
+
/**
|
|
143
|
+
* Sideband lifecycle stages, recorded in the same JSONL as the frame records.
|
|
144
|
+
*
|
|
145
|
+
* Frame forensics alone cannot separate the three realtime-voice failures reported in #4721.
|
|
146
|
+
* A join that never reached this proxy, a join whose upstream handshake was refused, and a
|
|
147
|
+
* relay that opened and then carried nothing all leave the same empty file, which is why the
|
|
148
|
+
* original report could only say "no frame log". One record per stage makes them distinct:
|
|
149
|
+
* no record at all means the client never dialed the proxy, `upstream-failed` carries the
|
|
150
|
+
* status the client was handed, and `relay-attached` with no following frame record means the
|
|
151
|
+
* transport is live and the silence is upstream of it.
|
|
152
|
+
*/
|
|
153
|
+
export type LiveSidebandStage = "upstream-open" | "upstream-failed" | "relay-attached" | "relay-closed";
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Append one lifecycle record. Same privacy rule as the frame records and for the same reason:
|
|
157
|
+
* no URL, no call id, no header, no frame content — only the stage and, on failure, the status
|
|
158
|
+
* and error code this proxy synthesized itself.
|
|
159
|
+
*/
|
|
160
|
+
export function logLiveSidebandStage(
|
|
161
|
+
stage: LiveSidebandStage,
|
|
162
|
+
detail?: { status?: number; code?: string },
|
|
163
|
+
): void {
|
|
164
|
+
const logPath = process.env[LIVE_FRAME_LOG_ENV];
|
|
165
|
+
if (!logPath) return;
|
|
166
|
+
try {
|
|
167
|
+
const record: Record<string, unknown> = { ts: new Date().toISOString(), stage };
|
|
168
|
+
if (detail?.status !== undefined) record.status = detail.status;
|
|
169
|
+
if (detail?.code !== undefined) record.code = detail.code;
|
|
170
|
+
appendFileSync(logPath, JSON.stringify(record) + "\n");
|
|
171
|
+
} catch {
|
|
172
|
+
// Diagnostics must never break the relay.
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
131
176
|
function clientProtocolHeaders(reqHeaders: Headers): Record<string, string> {
|
|
132
177
|
const out: Record<string, string> = {};
|
|
133
178
|
for (const name of LIVE_CLIENT_PROTOCOL_HEADERS) {
|
|
@@ -80,17 +80,20 @@ function sparseComboConfig<T extends {
|
|
|
80
80
|
waitForCooldownMs?: number;
|
|
81
81
|
imageInput?: "auto" | "disabled";
|
|
82
82
|
reasoningEffortMode?: "strict" | "adaptive";
|
|
83
|
-
|
|
83
|
+
defaultEffortMode?: "fallback" | "force";
|
|
84
|
+
}>(combo: T): Omit<T, "cooldownMs" | "waitForCooldownMs" | "imageInput" | "reasoningEffortMode" | "defaultEffortMode"> & {
|
|
84
85
|
cooldownMs?: number;
|
|
85
86
|
waitForCooldownMs?: number;
|
|
86
87
|
imageInput?: "disabled";
|
|
87
88
|
reasoningEffortMode?: "adaptive";
|
|
89
|
+
defaultEffortMode?: "force";
|
|
88
90
|
} {
|
|
89
91
|
const {
|
|
90
92
|
cooldownMs,
|
|
91
93
|
waitForCooldownMs,
|
|
92
94
|
imageInput,
|
|
93
95
|
reasoningEffortMode,
|
|
96
|
+
defaultEffortMode,
|
|
94
97
|
...rest
|
|
95
98
|
} = combo;
|
|
96
99
|
return {
|
|
@@ -101,6 +104,7 @@ function sparseComboConfig<T extends {
|
|
|
101
104
|
: {}),
|
|
102
105
|
...(imageInput === "disabled" ? { imageInput: "disabled" as const } : {}),
|
|
103
106
|
...(reasoningEffortMode === "adaptive" ? { reasoningEffortMode: "adaptive" as const } : {}),
|
|
107
|
+
...(defaultEffortMode === "force" ? { defaultEffortMode: "force" as const } : {}),
|
|
104
108
|
};
|
|
105
109
|
}
|
|
106
110
|
|
|
@@ -170,6 +174,11 @@ export async function handleComboRoutes(ctx: ManagementContext): Promise<Respons
|
|
|
170
174
|
...(!Object.hasOwn(requestedCombo, "waitForCooldownMs") && previous?.waitForCooldownMs !== undefined
|
|
171
175
|
? { waitForCooldownMs: previous.waitForCooldownMs }
|
|
172
176
|
: {}),
|
|
177
|
+
// The dashboard does not expose this advanced CLI/API policy. Preserve it when
|
|
178
|
+
// a GUI round-trip omits the field instead of silently downgrading to fallback.
|
|
179
|
+
...(!Object.hasOwn(requestedCombo, "defaultEffortMode") && previous?.defaultEffortMode !== undefined
|
|
180
|
+
? { defaultEffortMode: previous.defaultEffortMode }
|
|
181
|
+
: {}),
|
|
173
182
|
};
|
|
174
183
|
const error = comboConfigError(id, effectiveCombo, config.providers, {
|
|
175
184
|
requireEnabledTarget: true,
|
|
@@ -3,6 +3,11 @@ import { randomUUID } from "node:crypto";
|
|
|
3
3
|
import { readFileSync } from "node:fs";
|
|
4
4
|
import type { CatalogModel } from "../../codex/catalog";
|
|
5
5
|
import { catalogModelSlug, invalidateCodexModelsCache, nativeContextLimits, nativeModelRows, uniqueCatalogModelsForPublicList } from "../../codex/catalog";
|
|
6
|
+
import {
|
|
7
|
+
applyCodexDesktopSwitches,
|
|
8
|
+
describeCodexDesktopSwitches,
|
|
9
|
+
type CodexDesktopSwitchApply,
|
|
10
|
+
} from "../../codex/desktop-switches";
|
|
6
11
|
import {
|
|
7
12
|
DEFAULT_SUBAGENT_MODELS,
|
|
8
13
|
codexAutoStartEnabled,
|
|
@@ -328,6 +333,11 @@ export async function handleConfigRoutes(ctx: ManagementContext): Promise<Respon
|
|
|
328
333
|
codexDesktopAuthless: config.codexDesktopAuthless === true,
|
|
329
334
|
// Absent keeps Design B remote compaction; true selects the dedicated provider identity.
|
|
330
335
|
codexClientCompaction: config.codexClientCompaction === true,
|
|
336
|
+
codexDesktopSwitches: describeCodexDesktopSwitches(config, {
|
|
337
|
+
applied: false,
|
|
338
|
+
reason: "not_requested",
|
|
339
|
+
retryable: false,
|
|
340
|
+
}),
|
|
331
341
|
startupHealth: await readStartupHealth(config),
|
|
332
342
|
codexRuntime: {
|
|
333
343
|
path: displayCodexRuntimePath(resolved.runtime.command),
|
|
@@ -597,15 +607,26 @@ export async function handleConfigRoutes(ctx: ManagementContext): Promise<Respon
|
|
|
597
607
|
configureAppOwnedMemoryBudget(resolveAppOwnedMemoryBudgetBytes(body.appOwnedMemoryBudgetMb));
|
|
598
608
|
enforceAppOwnedMemoryBudget();
|
|
599
609
|
}
|
|
600
|
-
// Both Desktop compatibility switches change the injected config.toml shape, so converge now
|
|
601
|
-
// rather than waiting for the next start; the injector re-reads config and rewrites the form.
|
|
602
610
|
const authlessIsEnabled = config.codexDesktopAuthless === true;
|
|
603
611
|
const clientCompactionIsEnabled = config.codexClientCompaction === true;
|
|
604
|
-
const
|
|
605
|
-
||
|
|
606
|
-
|
|
612
|
+
const desktopSwitchesChanged = authlessWasEnabled !== authlessIsEnabled
|
|
613
|
+
|| clientCompactionWasEnabled !== clientCompactionIsEnabled;
|
|
614
|
+
// Catalog convergence is not config injection, and the comment that used to sit here said
|
|
615
|
+
// it was. `convergeCodexCatalog` rejects any scope but `catalog` and never reaches the
|
|
616
|
+
// injector, which is why flipping either switch left `config.toml` in its old shape until
|
|
617
|
+
// a separate `ocx sync` (#4809). Both halves are needed when a Desktop switch changes; a
|
|
618
|
+
// picker-only update still refreshes just the catalog.
|
|
619
|
+
const catalogRefresh = pickerWasEnabled !== pickerIsEnabled || desktopSwitchesChanged
|
|
607
620
|
? await convergeCodexCatalog()
|
|
608
621
|
: undefined;
|
|
622
|
+
// Injection second, matching `syncModelsToCodex`: the injected `model_catalog_json` should
|
|
623
|
+
// point at a catalog that has already settled. And it runs here rather than inside the save
|
|
624
|
+
// because coordinated Codex writes acquire the Codex write lock N before the config mutation
|
|
625
|
+
// lock C — awaiting N while still holding C would invert that order.
|
|
626
|
+
const desktopSwitchApply: CodexDesktopSwitchApply = desktopSwitchesChanged
|
|
627
|
+
? await applyCodexDesktopSwitches(config)
|
|
628
|
+
: { applied: false, reason: "not_requested", retryable: false };
|
|
629
|
+
const codexDesktopSwitches = describeCodexDesktopSwitches(config, desktopSwitchApply);
|
|
609
630
|
const catalogRefreshPending = catalogRefresh
|
|
610
631
|
? catalogRefreshIsPending(catalogRefresh)
|
|
611
632
|
: false;
|
|
@@ -622,6 +643,7 @@ export async function handleConfigRoutes(ctx: ManagementContext): Promise<Respon
|
|
|
622
643
|
catalogRefreshPending,
|
|
623
644
|
codexDesktopAuthless: authlessIsEnabled,
|
|
624
645
|
codexClientCompaction: clientCompactionIsEnabled,
|
|
646
|
+
codexDesktopSwitches,
|
|
625
647
|
codexMainAccountHardLock: config.codexMainAccountHardLock === true,
|
|
626
648
|
mainAccountHardLock: getMainAccountHardLockStatus(config),
|
|
627
649
|
startupHealth: await readStartupHealth(config),
|
|
@@ -10,6 +10,11 @@ import type { CursorEffortTable } from "../integrations/cursor-effort-table";
|
|
|
10
10
|
* Completions, Responses and Anthropic Messages, streams, and accepts tool calls, so those are
|
|
11
11
|
* constants; context length and vision come from catalog data when known and are omitted
|
|
12
12
|
* otherwise, matching Cursor's optional-field schema.
|
|
13
|
+
*
|
|
14
|
+
* Top-level capacity metrics (`context_window`, `context_length`, `max_output_tokens`) are
|
|
15
|
+
* mirrored directly on each model row for external client discovery (e.g. pi-ai, DSH,
|
|
16
|
+
* LibreChat) that inspects flat properties rather than Cursor's nested `capabilities.*` shape.
|
|
17
|
+
* A row that gains a nested capacity value must gain the top-level mirror in the same change.
|
|
13
18
|
*/
|
|
14
19
|
|
|
15
20
|
/**
|
|
@@ -132,6 +137,19 @@ export interface ModelCapabilityFields {
|
|
|
132
137
|
supports_vision?: boolean;
|
|
133
138
|
reasoning_effort?: string[];
|
|
134
139
|
};
|
|
140
|
+
/**
|
|
141
|
+
* Mirrored top-level context window for external/legacy client discovery (e.g. pi-ai, DSH)
|
|
142
|
+
* that reads top-level context_window / context_length instead of nested capabilities.
|
|
143
|
+
*/
|
|
144
|
+
context_window?: number;
|
|
145
|
+
/**
|
|
146
|
+
* Top-level context length alias matching capabilities.context_length for clients expecting context_length.
|
|
147
|
+
*/
|
|
148
|
+
context_length?: number;
|
|
149
|
+
/**
|
|
150
|
+
* Mirrored top-level max output token limit for external/legacy client discovery.
|
|
151
|
+
*/
|
|
152
|
+
max_output_tokens?: number;
|
|
135
153
|
/**
|
|
136
154
|
* Cursor reads the long-context threshold from `pricing.overrides[].min_prompt_tokens`. That
|
|
137
155
|
* key sits outside its validated capability schema, so it is the one place a threshold can
|
|
@@ -153,6 +171,7 @@ export function modelCapabilityFields(input: ModelCapabilityInput): ModelCapabil
|
|
|
153
171
|
const longContextLength = positiveInt(input.longContextWindow);
|
|
154
172
|
const maxOutputTokens = positiveInt(input.maxOutputTokens);
|
|
155
173
|
const hasLongTier = contextLength !== undefined && longContextLength !== undefined && longContextLength > contextLength;
|
|
174
|
+
const effectiveContextLength = hasLongTier ? longContextLength : contextLength;
|
|
156
175
|
const modalities = Array.isArray(input.inputModalities)
|
|
157
176
|
? input.inputModalities.filter(modality => typeof modality === "string" && modality.length > 0)
|
|
158
177
|
: undefined;
|
|
@@ -160,9 +179,7 @@ export function modelCapabilityFields(input: ModelCapabilityInput): ModelCapabil
|
|
|
160
179
|
return {
|
|
161
180
|
api_types: [...OPENCODEX_MODEL_API_TYPES],
|
|
162
181
|
capabilities: {
|
|
163
|
-
...(
|
|
164
|
-
? { context_length: longContextLength }
|
|
165
|
-
: contextLength !== undefined ? { context_length: contextLength } : {}),
|
|
182
|
+
...(effectiveContextLength !== undefined ? { context_length: effectiveContextLength } : {}),
|
|
166
183
|
...(maxOutputTokens !== undefined ? { max_output_tokens: maxOutputTokens } : {}),
|
|
167
184
|
// Once a gateway advertises api_types, Cursor keeps only rows whose output_modalities
|
|
168
185
|
// include "text"; omitting the key drops the row from the extended catalog.
|
|
@@ -174,6 +191,10 @@ export function modelCapabilityFields(input: ModelCapabilityInput): ModelCapabil
|
|
|
174
191
|
...(supportsVision !== undefined ? { supports_vision: supportsVision } : {}),
|
|
175
192
|
...(efforts.length > 0 ? { reasoning_effort: [...efforts] } : {}),
|
|
176
193
|
},
|
|
194
|
+
...(effectiveContextLength !== undefined
|
|
195
|
+
? { context_window: effectiveContextLength, context_length: effectiveContextLength }
|
|
196
|
+
: {}),
|
|
197
|
+
...(maxOutputTokens !== undefined ? { max_output_tokens: maxOutputTokens } : {}),
|
|
177
198
|
...(hasLongTier ? { pricing: { overrides: [{ min_prompt_tokens: contextLength }] } } : {}),
|
|
178
199
|
};
|
|
179
200
|
}
|
|
@@ -466,6 +466,8 @@ export function relaySseEagerBounded(
|
|
|
466
466
|
else hooks.onSynthetic(syntheticKind, syntheticReason);
|
|
467
467
|
}
|
|
468
468
|
if (cancelled && !hooks.sawTerminal()) {
|
|
469
|
+
// Finalize transport telemetry before the cancellation hook persists its usage row.
|
|
470
|
+
upstream.abort();
|
|
469
471
|
hooks.onClientCancel();
|
|
470
472
|
}
|
|
471
473
|
if (cancelled || upstream.signal.aborted || syntheticKind === "failed" || deliveryFallbackSent) {
|
package/src/server/relay.ts
CHANGED
|
@@ -26,6 +26,7 @@ import {
|
|
|
26
26
|
MAX_CLIENT_SSE_FRAME_BYTES,
|
|
27
27
|
} from "./sse-frame-buffer";
|
|
28
28
|
import { replaceSseDataPayload } from "./sse-payload-rewrite";
|
|
29
|
+
import { createBoundedResponseLogBody } from "./response-log-body";
|
|
29
30
|
|
|
30
31
|
const nativePassthroughSseResponses = new WeakSet<Response>();
|
|
31
32
|
const eagerRelaySseResponses = new WeakSet<Response>();
|
|
@@ -718,25 +719,16 @@ export function responseWithDeferredRequestLog(
|
|
|
718
719
|
}
|
|
719
720
|
if (!response.body || !contentType.includes("text/event-stream")) {
|
|
720
721
|
if (response.body && (contentType.includes("application/json") || response.status >= 400)) {
|
|
721
|
-
const
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
const body = new ReadableStream<Uint8Array>({
|
|
732
|
-
async start(controller) {
|
|
733
|
-
try {
|
|
734
|
-
controller.enqueue(new TextEncoder().encode(await finalizeJsonLog()));
|
|
735
|
-
controller.close();
|
|
736
|
-
} catch (err) {
|
|
737
|
-
addFinalRequestLog(requestId, start, logCtx, 502, { closeReason: "non_stream" }, addLog);
|
|
738
|
-
try { controller.error(err); } catch { /* already torn down */ }
|
|
739
|
-
}
|
|
722
|
+
const body = createBoundedResponseLogBody(response.body, {
|
|
723
|
+
json: contentType.includes("application/json"),
|
|
724
|
+
inspect: text => inspectResponseLogJson(logCtx, text),
|
|
725
|
+
finalize: reason => {
|
|
726
|
+
// Preserve wire status; request history follows the adjacent SSE
|
|
727
|
+
// convention for a client cancellation or upstream read failure.
|
|
728
|
+
const status = reason === "cancel" ? 499 : reason === "read_error" ? 502 : response.status;
|
|
729
|
+
addFinalRequestLog(requestId, start, logCtx, status, {
|
|
730
|
+
closeReason: reason === "cancel" ? "client_cancel" : "non_stream",
|
|
731
|
+
}, addLog);
|
|
740
732
|
},
|
|
741
733
|
});
|
|
742
734
|
return new Response(body, {
|
|
@@ -1338,6 +1330,9 @@ function startBoundedInspectionPump(options: InspectionPumpOptions): void {
|
|
|
1338
1330
|
try {
|
|
1339
1331
|
for (;;) {
|
|
1340
1332
|
const { done, value } = await reader.read();
|
|
1333
|
+
// Hard cancellation settles a pending read as EOF. Do not flush a
|
|
1334
|
+
// partial terminal after the owner already finalized cancellation.
|
|
1335
|
+
if (cancelled) break;
|
|
1341
1336
|
if (clientGoneSignal?.aborted) markClientGone();
|
|
1342
1337
|
if (drainStopped) {
|
|
1343
1338
|
// stopDrain() cancelled the reader; the settled read is the wake-up.
|
|
@@ -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,6 +20,7 @@ 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 {
|
|
@@ -138,6 +142,15 @@ export interface RequestLogContext {
|
|
|
138
142
|
preserveResolvedModelFromRoute?: boolean;
|
|
139
143
|
usage?: OcxUsage;
|
|
140
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;
|
|
141
154
|
attempts?: PersistedUsageAttempt[];
|
|
142
155
|
/** Internal mutable final attempt; omitted from RequestLogEntry/JSONL. */
|
|
143
156
|
activeAttempt?: PersistedUsageAttempt;
|
|
@@ -772,8 +785,10 @@ export function applyResponseLogMetadata(logCtx: RequestLogContext, payload: unk
|
|
|
772
785
|
}
|
|
773
786
|
const usage = usageFromResponsesPayload((source as { usage?: unknown }).usage);
|
|
774
787
|
if (usage && !logCtx.usageFromBridge) {
|
|
775
|
-
logCtx
|
|
776
|
-
|
|
788
|
+
if (!recordKeyWireAttemptUsage(logCtx, usage)) {
|
|
789
|
+
logCtx.usage = usage;
|
|
790
|
+
if (logCtx.activeAttempt) logCtx.activeAttempt.usage = usage;
|
|
791
|
+
}
|
|
777
792
|
// Counts taken off a wire, not reported raw. The zero-default token-detail objects strict
|
|
778
793
|
// clients require are indistinguishable here from a measured zero, so the cache detail these
|
|
779
794
|
// counts carry is recorded as synthesized rather than as an observed miss.
|
|
@@ -1206,6 +1221,31 @@ export function recordNoAccountAffinityFailure(
|
|
|
1206
1221
|
logCtx.errorCode ??= "codex_no_account";
|
|
1207
1222
|
return resolved;
|
|
1208
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
|
+
}
|
|
1209
1249
|
|
|
1210
1250
|
export function addFinalRequestLog(
|
|
1211
1251
|
requestId: string,
|
|
@@ -1237,13 +1277,19 @@ export function addFinalRequestLog(
|
|
|
1237
1277
|
logCtx.activeAttempt,
|
|
1238
1278
|
effectiveStatus,
|
|
1239
1279
|
Date.now() - (logCtx.activeAttemptStartedAt ?? start),
|
|
1240
|
-
logCtx.
|
|
1280
|
+
keyUsageOwners.has(logCtx.activeAttempt)
|
|
1281
|
+
? logCtx.activeAttempt.usage
|
|
1282
|
+
: logCtx.usage,
|
|
1241
1283
|
);
|
|
1242
1284
|
// The final row and its active physical attempt describe the same terminal. Preserve the
|
|
1243
1285
|
// semantic code on both so detailed attempt telemetry cannot regress to a generic status code.
|
|
1244
1286
|
if (errorCode) logCtx.activeAttempt.errorCode = errorCode;
|
|
1245
1287
|
else delete logCtx.activeAttempt.errorCode;
|
|
1246
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);
|
|
1247
1293
|
const existing = finalizedUsage(
|
|
1248
1294
|
logCtx.providerAdapter ?? logCtx.provider,
|
|
1249
1295
|
logCtx.usage,
|
|
@@ -1528,6 +1574,84 @@ export function sealRequestAttemptIdentity(
|
|
|
1528
1574
|
attempt.provider = provider;
|
|
1529
1575
|
attempt.adapter = adapter;
|
|
1530
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
|
+
}
|
|
1531
1655
|
}
|
|
1532
1656
|
|
|
1533
1657
|
/** Capture only the resolved upstream route; inbound auth and today's config cannot label old usage. */
|