@yansigit/opencodex 2.33.0 → 2.35.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 +3 -3
- package/gui/dist/assets/index-BjCaHxdz.js +112 -0
- package/gui/dist/assets/index-DLkXOXLC.css +1 -0
- package/gui/dist/index.html +2 -2
- package/package.json +1 -1
- package/src/adapters/anthropic.ts +79 -2
- package/src/adapters/command-code.ts +141 -23
- package/src/adapters/cursor/call-id.ts +44 -0
- package/src/adapters/cursor/checkpoint-store.ts +15 -10
- package/src/adapters/cursor/discovery.ts +60 -2
- package/src/adapters/cursor/effort-map.ts +79 -1
- package/src/adapters/cursor/envelope-echo.ts +162 -0
- package/src/adapters/cursor/live-models.ts +7 -2
- package/src/adapters/cursor/live-transport.ts +17 -1
- package/src/adapters/cursor/message-mapper.ts +4 -1
- package/src/adapters/cursor/native-exec-fs.ts +13 -12
- package/src/adapters/cursor/native-exec-network.ts +3 -5
- package/src/adapters/cursor/native-exec-policy.ts +47 -0
- package/src/adapters/cursor/native-exec-shell.ts +116 -31
- package/src/adapters/cursor/native-exec.ts +38 -10
- package/src/adapters/cursor/protobuf-events.ts +28 -2
- package/src/adapters/cursor/protobuf-request.ts +93 -41
- package/src/adapters/cursor/request-builder.ts +39 -10
- package/src/adapters/cursor/tool-definitions.ts +27 -3
- package/src/adapters/cursor/tool-result-normalize.ts +51 -6
- package/src/adapters/cursor/types.ts +23 -4
- package/src/adapters/cursor.ts +170 -29
- package/src/adapters/google-aistudio-parser.ts +49 -0
- package/src/adapters/google-antigravity-replay.ts +105 -25
- package/src/adapters/google-antigravity-wire.ts +5 -0
- package/src/adapters/google-errors.ts +41 -12
- package/src/adapters/google-http.ts +12 -11
- package/src/adapters/google.ts +219 -36
- package/src/adapters/image.ts +1 -1
- package/src/adapters/kiro-constants.ts +15 -0
- package/src/adapters/kiro-tools.ts +43 -15
- package/src/adapters/kiro.ts +54 -9
- package/src/adapters/openai-chat.ts +286 -242
- package/src/adapters/openai-responses.ts +335 -24
- package/src/adapters/run-turn-queue.ts +36 -1
- package/src/adapters/tool-catalog-nudge.ts +2 -2
- package/src/adapters/xai-tool-schema.ts +436 -0
- package/src/bridge.ts +67 -26
- package/src/chat/inbound.ts +29 -1
- package/src/chat/outbound.ts +15 -7
- package/src/claude/agents-inject.ts +8 -1
- package/src/claude/outbound.ts +10 -8
- package/src/cli/account-api.ts +27 -7
- package/src/cli/account-extended.ts +10 -3
- package/src/cli/account.ts +29 -5
- package/src/cli/alias.ts +66 -0
- package/src/cli/claude.ts +26 -1
- package/src/cli/dispatch.ts +13 -1
- package/src/cli/help.ts +1 -0
- package/src/cli/index.ts +6 -1
- package/src/cli/init.ts +1 -0
- package/src/cli/models-runtime.ts +95 -0
- package/src/cli/models.ts +13 -7
- package/src/cli/provider-runtime.ts +16 -2
- package/src/cli/registry.ts +6 -1
- package/src/cli/telemetry-commands.ts +25 -0
- package/src/cli/v2.ts +34 -10
- package/src/codex/account-pause.ts +2 -1
- package/src/codex/account-priority.ts +3 -2
- package/src/codex/app-server-processes.ts +80 -6
- package/src/codex/auth-api.ts +48 -8
- package/src/codex/auth-context.ts +21 -18
- package/src/codex/catalog/aggregation.ts +6 -0
- package/src/codex/catalog/model-metadata.ts +13 -1
- package/src/codex/catalog/native-models.ts +5 -2
- package/src/codex/catalog/parsing.ts +16 -0
- package/src/codex/catalog/provider-fetch.ts +20 -3
- package/src/codex/catalog/sync.ts +127 -2
- package/src/codex/catalog.ts +1 -1
- package/src/codex/codex-write-lock.ts +3 -1
- package/src/codex/convergence-types.ts +1 -1
- package/src/codex/convergence.ts +22 -2
- package/src/codex/desired-state.ts +2 -2
- package/src/codex/desktop-app-restart.ts +18 -5
- package/src/codex/inject-coordination.ts +83 -0
- package/src/codex/inject.ts +14 -1
- package/src/codex/log-guard/inspect.ts +22 -4
- package/src/codex/model-entitlements.ts +9 -2
- package/src/codex/prompt-layers.ts +371 -25
- package/src/codex/prompt-text-probe.ts +238 -0
- package/src/codex/quota.ts +123 -18
- package/src/codex/routing.ts +9 -0
- package/src/codex/subagent-model-fallback.ts +198 -27
- package/src/codex/transition-state.ts +107 -8
- package/src/combos/types.ts +10 -0
- package/src/compatibility/openai-responses.ts +33 -1
- package/src/config/autonomous-remediation.ts +21 -0
- package/src/config/provider-validation.ts +14 -0
- package/src/config/rebase-provenance.ts +68 -0
- package/src/config.ts +191 -17
- package/src/generated/compatibility-version.json +279 -159
- package/src/generated/model-metadata.ts +3 -0
- package/src/images/loop.ts +5 -4
- package/src/lab/conformance/fixtures/protocol-v1-cases.json +1 -1
- package/src/lab/fabric/producer-child.ts +1 -1
- package/src/lib/config-ownership.ts +20 -0
- package/src/lib/errors.ts +11 -2
- package/src/lib/package-tree-integrity.ts +101 -0
- package/src/oauth/aistudio-credentials.ts +65 -0
- package/src/oauth/aistudio-native-daemon.ts +116 -0
- package/src/oauth/aistudio-session-sync.ts +95 -0
- package/src/oauth/generic-account-failover.ts +231 -0
- package/src/oauth/google-aistudio-auth.ts +98 -0
- package/src/oauth/index.ts +57 -5
- package/src/oauth/key-providers.ts +18 -1
- package/src/oauth/kiro.ts +45 -0
- package/src/oauth/login-cli.ts +65 -1
- package/src/oauth/types.ts +15 -0
- package/src/providers/codex-capacity.ts +5 -2
- package/src/providers/command-code-efforts.ts +38 -6
- package/src/providers/context-cap.ts +4 -3
- package/src/providers/default-aliases.ts +65 -0
- package/src/providers/derive.ts +29 -1
- package/src/providers/fastwire.ts +7 -1
- package/src/providers/model-presets.ts +119 -0
- package/src/providers/new-model-policy.ts +146 -0
- package/src/providers/provider-id-rewrite.ts +2 -1
- package/src/providers/quota.ts +157 -46
- package/src/providers/registry.ts +184 -71
- package/src/providers/slug-codec.ts +52 -0
- package/src/responses/code-mode-helper-compat.ts +50 -0
- package/src/responses/custom-tool-compat.ts +34 -10
- package/src/responses/parser.ts +4 -0
- package/src/responses/schema.ts +5 -1
- package/src/responses/thought-signature-replay.ts +17 -0
- package/src/router.ts +43 -2
- package/src/routing/account-pool/cooldown.ts +8 -0
- package/src/routing/account-pool/index.ts +1 -0
- package/src/routing/analytics.ts +1 -0
- package/src/routing/quota.ts +10 -0
- package/src/server/auth-cors.ts +24 -0
- package/src/server/chat-completions.ts +26 -16
- package/src/server/chat-native-sse.ts +3 -3
- package/src/server/chat-native.ts +30 -11
- package/src/server/claude-messages.ts +1 -1
- package/src/server/effort-policy.ts +16 -0
- package/src/server/index.ts +180 -14
- package/src/server/lifecycle.ts +52 -1
- package/src/server/management/agent-settings-routes.ts +31 -15
- package/src/server/management/codex-prompt-routes.ts +570 -0
- package/src/server/management/combo-routes.ts +2 -1
- package/src/server/management/config-routes.ts +27 -9
- package/src/server/management/context.ts +9 -0
- package/src/server/management/logs-usage-routes.ts +11 -5
- package/src/server/management/model-routes.ts +266 -0
- package/src/server/management/oauth-account-routes.ts +13 -3
- package/src/server/management/provider-routes.ts +137 -3
- package/src/server/management/routing-profile-routes.ts +2 -2
- package/src/server/management-api.ts +2 -0
- package/src/server/port-reclaim.ts +19 -1
- package/src/server/relay-eager.ts +147 -20
- package/src/server/relay.ts +251 -19
- package/src/server/request-log-conversation.ts +33 -0
- package/src/server/request-log.ts +48 -21
- package/src/server/responses/collaboration.ts +42 -5
- package/src/server/responses/combo-stream-preflight.ts +10 -3
- package/src/server/responses/core.ts +575 -140
- package/src/server/responses/empty-completion-guard.ts +35 -0
- package/src/server/responses/fetch-helpers.ts +14 -6
- package/src/server/responses/input-admission.ts +3 -1
- package/src/server/responses/passthrough-error.ts +33 -9
- package/src/server/responses/policy-fallback.ts +1 -1
- package/src/server/responses/responses-field-backfill.ts +105 -13
- package/src/server/responses/ws-upstream.ts +35 -5
- package/src/server/responses-custom-tool-repair.ts +52 -7
- package/src/server/responses-terminal-repair.ts +25 -4
- package/src/server/sse-frame-buffer.ts +31 -4
- package/src/server/ws-bridge.ts +14 -2
- package/src/smoke/fingerprint-cache.ts +133 -0
- package/src/smoke/live-scenarios.ts +33 -0
- package/src/smoke/runner.ts +119 -0
- package/src/telemetry/dispatcher.ts +44 -0
- package/src/telemetry/fingerprint.ts +24 -0
- package/src/telemetry/hook.ts +43 -0
- package/src/telemetry/ledger.ts +54 -0
- package/src/telemetry/types.ts +23 -0
- package/src/types/config.ts +66 -14
- package/src/types/provider.ts +79 -1
- package/src/types/request.ts +18 -10
- package/src/types/tools.ts +30 -11
- package/src/types.ts +1 -0
- package/src/usage/command-code-manifest.ts +116 -0
- package/src/usage/cost.ts +2 -2
- package/src/usage/expected-prices.ts +126 -24
- package/src/usage/log.ts +18 -8
- package/src/usage/summary.ts +34 -12
- package/src/web-search/exa-executor.ts +40 -9
- package/src/web-search/index.ts +16 -8
- package/src/web-search/loop.ts +5 -4
- package/gui/dist/assets/index-DKLr4LTE.js +0 -102
- package/gui/dist/assets/index-DrSQdTRd.css +0 -1
package/src/server/relay.ts
CHANGED
|
@@ -1,4 +1,12 @@
|
|
|
1
1
|
import type { ResponsesTerminalStatus } from "../bridge";
|
|
2
|
+
import {
|
|
3
|
+
cyberPolicyErrorType,
|
|
4
|
+
CYBER_POLICY_ERROR_CODE,
|
|
5
|
+
CYBER_POLICY_FALLBACK_MESSAGE,
|
|
6
|
+
isCyberPolicyCode,
|
|
7
|
+
isCyberPolicyMessage,
|
|
8
|
+
} from "../lib/errors";
|
|
9
|
+
import { redactSecretString } from "../lib/redact";
|
|
2
10
|
import { isTranslatorBudgetExceededError } from "../lib/translator-budget";
|
|
3
11
|
import { isUsageDebugEnabled } from "../usage/debug";
|
|
4
12
|
import {
|
|
@@ -16,6 +24,7 @@ import {
|
|
|
16
24
|
joinSseFrameBytes,
|
|
17
25
|
MAX_CLIENT_SSE_FRAME_BYTES,
|
|
18
26
|
} from "./sse-frame-buffer";
|
|
27
|
+
import { replaceSseDataPayload } from "./sse-payload-rewrite";
|
|
19
28
|
|
|
20
29
|
const nativePassthroughSseResponses = new WeakSet<Response>();
|
|
21
30
|
const eagerRelaySseResponses = new WeakSet<Response>();
|
|
@@ -24,6 +33,30 @@ export const MAX_INSPECTION_SSE_FRAME_BYTES = MAX_CLIENT_SSE_FRAME_BYTES;
|
|
|
24
33
|
export const MAX_COMPLETED_OUTPUT_ITEMS = 256;
|
|
25
34
|
export const MAX_COMPLETED_OUTPUT_ITEM_SOURCE_BYTES = 8 * 1024 * 1024;
|
|
26
35
|
export const MAX_TAIL_ERROR_MESSAGE_CHARS = 512;
|
|
36
|
+
const ADAPTER_EOF_INCOMPLETE_PAYLOAD = JSON.stringify({
|
|
37
|
+
type: "response.incomplete",
|
|
38
|
+
response: {
|
|
39
|
+
status: "incomplete",
|
|
40
|
+
incomplete_details: { reason: "adapter_eof" },
|
|
41
|
+
},
|
|
42
|
+
});
|
|
43
|
+
const DONE_SSE_FRAME_TEXT = "data: [DONE]\n\n";
|
|
44
|
+
const FAILED_TAIL_FALLBACK_PAYLOAD = JSON.stringify({
|
|
45
|
+
type: "response.failed",
|
|
46
|
+
response: {
|
|
47
|
+
status: "failed",
|
|
48
|
+
error: {
|
|
49
|
+
type: "upstream_error",
|
|
50
|
+
code: "upstream_reset",
|
|
51
|
+
message: "Upstream stream terminated unexpectedly",
|
|
52
|
+
},
|
|
53
|
+
last_error: {
|
|
54
|
+
type: "upstream_error",
|
|
55
|
+
code: "upstream_reset",
|
|
56
|
+
message: "Upstream stream terminated unexpectedly",
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
});
|
|
27
60
|
|
|
28
61
|
export type InspectionCounters = {
|
|
29
62
|
frameBufferHighWaterBytes: number;
|
|
@@ -99,6 +132,21 @@ export function buildFailedTailPayload(err: unknown): string {
|
|
|
99
132
|
});
|
|
100
133
|
}
|
|
101
134
|
|
|
135
|
+
function buildFailedTailPayloadOrFallback(err: unknown): string {
|
|
136
|
+
try {
|
|
137
|
+
return buildFailedTailPayload(err);
|
|
138
|
+
} catch {
|
|
139
|
+
// Error.message and String(error) may execute hostile accessors. Preserve a
|
|
140
|
+
// bounded protocol terminal even when diagnostic serialization is unsafe.
|
|
141
|
+
return FAILED_TAIL_FALLBACK_PAYLOAD;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export function failedTailFrame(encoder: TextEncoder, err: unknown): Uint8Array {
|
|
146
|
+
const payload = buildFailedTailPayloadOrFallback(err);
|
|
147
|
+
return encoder.encode(`\n\nevent: response.failed\ndata: ${payload}\n\n${DONE_SSE_FRAME_TEXT}`);
|
|
148
|
+
}
|
|
149
|
+
|
|
102
150
|
export type SseTerminalOutputBoundary = {
|
|
103
151
|
feed(chunk: Uint8Array): Uint8Array;
|
|
104
152
|
finish(): Uint8Array;
|
|
@@ -111,13 +159,16 @@ export type SseTerminalOutputBoundary = {
|
|
|
111
159
|
* Frame-aware client output boundary shared by both native Responses relays.
|
|
112
160
|
* It buffers only the current incomplete SSE block under the same hard byte
|
|
113
161
|
* cap as inspection, forwards complete blocks through the first Responses
|
|
114
|
-
* terminal, and drops every later block/byte.
|
|
162
|
+
* terminal, and drops every later block/byte. A premature [DONE] is held until
|
|
163
|
+
* a terminal arrives so clean EOF can synthesize one terminal and one sentinel.
|
|
115
164
|
*/
|
|
116
165
|
export function createSseTerminalOutputBoundary(): SseTerminalOutputBoundary {
|
|
117
166
|
const decoder = new TextDecoder();
|
|
167
|
+
const encoder = new TextEncoder();
|
|
118
168
|
const framer = new BoundedSseFrameBuffer(MAX_INSPECTION_SSE_FRAME_BYTES);
|
|
119
169
|
let terminal = false;
|
|
120
170
|
let done = false;
|
|
171
|
+
let pendingDone: { block: Uint8Array; delimiter: Uint8Array } | null = null;
|
|
121
172
|
let disposed = false;
|
|
122
173
|
|
|
123
174
|
const processFrames = (
|
|
@@ -129,16 +180,37 @@ export function createSseTerminalOutputBoundary(): SseTerminalOutputBoundary {
|
|
|
129
180
|
for (const frame of frames) {
|
|
130
181
|
const payload = sseDataPayload(decoder.decode(frame.block));
|
|
131
182
|
const isDone = payload === "[DONE]";
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
183
|
+
const parsed = payload === null ? undefined : parseSsePayload(payload);
|
|
184
|
+
const policyError = parsed !== undefined && isPolicyRewriteType(parsed)
|
|
185
|
+
? cyberPolicyTerminalError(parsed)
|
|
186
|
+
: undefined;
|
|
187
|
+
const outboundBlock = policyError
|
|
188
|
+
? encoder.encode(rewritePolicyTerminalBlock(
|
|
189
|
+
decoder.decode(frame.block),
|
|
190
|
+
policyFailurePayload(policyError, parsed),
|
|
191
|
+
))
|
|
192
|
+
: frame.block;
|
|
136
193
|
if (isDone) {
|
|
137
194
|
done = true;
|
|
195
|
+
if (responsesTerminal) {
|
|
196
|
+
output.push(outboundBlock, frame.delimiter);
|
|
197
|
+
} else if (!pendingDone) {
|
|
198
|
+
// Do not expose a sentinel before a Responses terminal. If EOF
|
|
199
|
+
// follows, the synthetic incomplete path owns the one sentinel;
|
|
200
|
+
// if a terminal arrives later, this pending frame is emitted then.
|
|
201
|
+
pendingDone = { block: outboundBlock, delimiter: frame.delimiter };
|
|
202
|
+
}
|
|
138
203
|
continue;
|
|
139
204
|
}
|
|
140
|
-
|
|
205
|
+
// Preserve every frame through the first Responses terminal. Every
|
|
206
|
+
// later non-DONE frame is dropped.
|
|
207
|
+
if (!responsesTerminal) output.push(outboundBlock, frame.delimiter);
|
|
208
|
+
if (!responsesTerminal && payload && terminalStatusFromParsed(parsed)) {
|
|
141
209
|
responsesTerminal = true;
|
|
210
|
+
if (pendingDone) {
|
|
211
|
+
output.push(pendingDone.block, pendingDone.delimiter);
|
|
212
|
+
pendingDone = null;
|
|
213
|
+
}
|
|
142
214
|
}
|
|
143
215
|
}
|
|
144
216
|
if (responsesTerminal) {
|
|
@@ -155,13 +227,22 @@ export function createSseTerminalOutputBoundary(): SseTerminalOutputBoundary {
|
|
|
155
227
|
},
|
|
156
228
|
finish() {
|
|
157
229
|
if (disposed || terminal) return new Uint8Array(0);
|
|
158
|
-
|
|
230
|
+
const tail = framer.finish();
|
|
231
|
+
if (tail.byteLength === 0) return new Uint8Array(0);
|
|
232
|
+
// EOF may cut off the final SSE block before its blank-line delimiter.
|
|
233
|
+
// Feed it through the exact same parser/rewrite/terminal path as a
|
|
234
|
+
// complete frame, using a synthetic delimiter so the client receives a
|
|
235
|
+
// dispatchable event rather than an unterminated tail.
|
|
236
|
+
const tailText = decoder.decode(tail);
|
|
237
|
+
const delimiter = encoder.encode(tailText.includes("\r\n") ? "\r\n\r\n" : "\n\n");
|
|
238
|
+
return processFrames([{ block: tail, delimiter }]);
|
|
159
239
|
},
|
|
160
240
|
terminalSeen: () => terminal,
|
|
161
241
|
doneSeen: () => done,
|
|
162
242
|
dispose() {
|
|
163
243
|
if (disposed) return;
|
|
164
244
|
disposed = true;
|
|
245
|
+
pendingDone = null;
|
|
165
246
|
framer.dispose();
|
|
166
247
|
},
|
|
167
248
|
};
|
|
@@ -198,7 +279,7 @@ export function relaySseWithFailedTail(
|
|
|
198
279
|
// Preserve through the terminal block only, add the conventional sentinel
|
|
199
280
|
// when there was no real [DONE] data event, then stop reading upstream.
|
|
200
281
|
if (!terminalBoundary.doneSeen()) {
|
|
201
|
-
controller.enqueue(encoder
|
|
282
|
+
controller.enqueue(doneFrame(encoder));
|
|
202
283
|
}
|
|
203
284
|
closed = true;
|
|
204
285
|
controller.close();
|
|
@@ -219,6 +300,16 @@ export function relaySseWithFailedTail(
|
|
|
219
300
|
if (done) {
|
|
220
301
|
const tail = terminalBoundary.finish();
|
|
221
302
|
if (tail.byteLength > 0) controller.enqueue(tail);
|
|
303
|
+
if (terminalBoundary.terminalSeen()) {
|
|
304
|
+
if (!terminalBoundary.doneSeen()) controller.enqueue(doneFrame(encoder));
|
|
305
|
+
} else {
|
|
306
|
+
// A clean upstream EOF is still a failed Responses turn when no
|
|
307
|
+
// protocol terminal arrived. Make that state explicit so Codex
|
|
308
|
+
// does not treat HTTP 200 + bare EOF as a retryable disconnect.
|
|
309
|
+
const incomplete = adapterEofIncompleteFrame(encoder);
|
|
310
|
+
controller.enqueue(incomplete);
|
|
311
|
+
controller.enqueue(doneFrame(encoder));
|
|
312
|
+
}
|
|
222
313
|
terminalBoundary.dispose();
|
|
223
314
|
controller.close();
|
|
224
315
|
return;
|
|
@@ -228,8 +319,10 @@ export function relaySseWithFailedTail(
|
|
|
228
319
|
}
|
|
229
320
|
} catch (err) {
|
|
230
321
|
let partial: Uint8Array = new Uint8Array(0);
|
|
322
|
+
let tailTerminal = false;
|
|
231
323
|
try {
|
|
232
324
|
partial = terminalBoundary.finish();
|
|
325
|
+
tailTerminal = terminalBoundary.terminalSeen();
|
|
233
326
|
} catch {
|
|
234
327
|
// A near-cap ambiguous delimiter tail may itself overflow at EOF.
|
|
235
328
|
// Preserve the original read/framing failure and continue emitting
|
|
@@ -237,11 +330,14 @@ export function relaySseWithFailedTail(
|
|
|
237
330
|
}
|
|
238
331
|
terminalBoundary.dispose();
|
|
239
332
|
if (closed) return;
|
|
240
|
-
const payload = buildFailedTailPayload(err);
|
|
241
333
|
try {
|
|
242
334
|
if (partial.byteLength > 0) controller.enqueue(partial);
|
|
243
|
-
|
|
244
|
-
|
|
335
|
+
if (tailTerminal) {
|
|
336
|
+
if (!terminalBoundary.doneSeen()) controller.enqueue(doneFrame(encoder));
|
|
337
|
+
} else {
|
|
338
|
+
// Leading blank line terminates a partial SSE block so the failed frame parses cleanly.
|
|
339
|
+
controller.enqueue(failedTailFrame(encoder, err));
|
|
340
|
+
}
|
|
245
341
|
controller.close();
|
|
246
342
|
} catch { /* client already torn down */ }
|
|
247
343
|
upstream.abort();
|
|
@@ -276,15 +372,139 @@ export function sseDataPayload(block: string): string | null {
|
|
|
276
372
|
return data.length > 0 ? data.join("\n") : null;
|
|
277
373
|
}
|
|
278
374
|
|
|
279
|
-
|
|
280
|
-
|
|
375
|
+
type JsonRecord = Record<string, unknown>;
|
|
376
|
+
|
|
377
|
+
function asJsonRecord(value: unknown): JsonRecord | null {
|
|
378
|
+
return value !== null && typeof value === "object" && !Array.isArray(value)
|
|
379
|
+
? value as JsonRecord
|
|
380
|
+
: null;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
function stringField(record: JsonRecord | null, key: string): string | undefined {
|
|
384
|
+
const value = record?.[key];
|
|
385
|
+
return typeof value === "string" && value.trim() ? value.trim() : undefined;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
/**
|
|
389
|
+
* Return a high-confidence policy error carried by an upstream terminal shape.
|
|
390
|
+
* Deliberately inspect structured error fields and known refusal copy only — a
|
|
391
|
+
* bare `cyber_policy` token in an unrelated payload is not sufficient.
|
|
392
|
+
*/
|
|
393
|
+
function cyberPolicyTerminalError(parsed: unknown): { message: string; type?: string } | undefined {
|
|
394
|
+
const root = asJsonRecord(parsed);
|
|
395
|
+
if (!root) return undefined;
|
|
396
|
+
const response = asJsonRecord(root.response);
|
|
397
|
+
const candidates = [
|
|
398
|
+
asJsonRecord(root.error),
|
|
399
|
+
asJsonRecord(root.last_error),
|
|
400
|
+
asJsonRecord(response?.error),
|
|
401
|
+
asJsonRecord(response?.incomplete_details),
|
|
402
|
+
root,
|
|
403
|
+
response,
|
|
404
|
+
];
|
|
405
|
+
for (const candidate of candidates) {
|
|
406
|
+
if (!candidate) continue;
|
|
407
|
+
if (isCyberPolicyCode(stringField(candidate, "code"))) {
|
|
408
|
+
return {
|
|
409
|
+
message: stringField(candidate, "message")
|
|
410
|
+
?? CYBER_POLICY_FALLBACK_MESSAGE,
|
|
411
|
+
...(stringField(candidate, "type") ? { type: stringField(candidate, "type") } : {}),
|
|
412
|
+
};
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
for (const candidate of candidates) {
|
|
416
|
+
const message = stringField(candidate, "message");
|
|
417
|
+
if (message && isCyberPolicyMessage(message)) {
|
|
418
|
+
return {
|
|
419
|
+
message,
|
|
420
|
+
...(stringField(candidate, "type") ? { type: stringField(candidate, "type") } : {}),
|
|
421
|
+
};
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
return undefined;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
function parseSsePayload(payload: string): unknown | undefined {
|
|
428
|
+
if (payload === "[DONE]") return undefined;
|
|
281
429
|
try {
|
|
282
|
-
return
|
|
430
|
+
return JSON.parse(payload);
|
|
283
431
|
} catch {
|
|
284
|
-
return
|
|
432
|
+
return undefined;
|
|
285
433
|
}
|
|
286
434
|
}
|
|
287
435
|
|
|
436
|
+
function isPolicyRewriteType(parsed: unknown): boolean {
|
|
437
|
+
const type = asJsonRecord(parsed)?.type;
|
|
438
|
+
return type === "response.failed" || type === "response.incomplete" || type === "error";
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
function rewritePolicyTerminalBlock(block: string, payload: string): string {
|
|
442
|
+
const newline = block.includes("\r\n") ? "\r\n" : "\n";
|
|
443
|
+
const rewritten = replaceSseDataPayload(block, payload);
|
|
444
|
+
const lines = rewritten.split(/\r?\n/);
|
|
445
|
+
let eventRewritten = false;
|
|
446
|
+
const withEvent = lines.map(line => {
|
|
447
|
+
if (!eventRewritten && line.startsWith("event:")) {
|
|
448
|
+
eventRewritten = true;
|
|
449
|
+
return "event: response.failed";
|
|
450
|
+
}
|
|
451
|
+
return line;
|
|
452
|
+
});
|
|
453
|
+
if (!eventRewritten) withEvent.unshift("event: response.failed");
|
|
454
|
+
return withEvent.join(newline);
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
function policyFailurePayload(policyError: { message: string; type?: string }, parsed: unknown): string {
|
|
458
|
+
const error = {
|
|
459
|
+
type: cyberPolicyErrorType(policyError.type),
|
|
460
|
+
code: CYBER_POLICY_ERROR_CODE,
|
|
461
|
+
message: redactSecretString(policyError.message).slice(0, MAX_TAIL_ERROR_MESSAGE_CHARS),
|
|
462
|
+
};
|
|
463
|
+
const root = asJsonRecord(parsed);
|
|
464
|
+
const originalResponse = asJsonRecord(root?.response);
|
|
465
|
+
const preservedResponse = Object.fromEntries(
|
|
466
|
+
Object.entries(originalResponse ?? {}).filter(([key]) => key !== "incomplete_details"),
|
|
467
|
+
);
|
|
468
|
+
const response = {
|
|
469
|
+
...preservedResponse,
|
|
470
|
+
status: "failed",
|
|
471
|
+
error,
|
|
472
|
+
last_error: error,
|
|
473
|
+
retryable: false,
|
|
474
|
+
};
|
|
475
|
+
// Responses event metadata such as sequence_number is normally top-level.
|
|
476
|
+
// Keep it (and any other non-error, non-response fields) while replacing only
|
|
477
|
+
// the protocol type and error envelope.
|
|
478
|
+
const preservedRoot = root
|
|
479
|
+
? Object.fromEntries(Object.entries(root).filter(([key]) => (
|
|
480
|
+
key !== "type"
|
|
481
|
+
&& key !== "response"
|
|
482
|
+
&& key !== "error"
|
|
483
|
+
&& key !== "last_error"
|
|
484
|
+
&& key !== "retryable"
|
|
485
|
+
)))
|
|
486
|
+
: {};
|
|
487
|
+
return JSON.stringify({
|
|
488
|
+
...preservedRoot,
|
|
489
|
+
type: "response.failed",
|
|
490
|
+
retryable: false,
|
|
491
|
+
response,
|
|
492
|
+
});
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
export function adapterEofIncompleteFrame(encoder: TextEncoder): Uint8Array {
|
|
496
|
+
return encoder.encode(`event: response.incomplete\ndata: ${ADAPTER_EOF_INCOMPLETE_PAYLOAD}\n\n`);
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
export function doneFrame(encoder: TextEncoder): Uint8Array {
|
|
500
|
+
return encoder.encode(DONE_SSE_FRAME_TEXT);
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
export function terminalStatusFromSsePayload(payload: string): ResponsesTerminalStatus | null {
|
|
504
|
+
if (payload === "[DONE]") return null;
|
|
505
|
+
return terminalStatusFromParsed(parseSsePayload(payload));
|
|
506
|
+
}
|
|
507
|
+
|
|
288
508
|
/** True when a native Responses SSE payload carries the FIRST kind of non-empty model output. */
|
|
289
509
|
export function isFirstOutputSsePayload(payload: string | null): boolean {
|
|
290
510
|
if (!payload || payload === "[DONE]") return false;
|
|
@@ -322,14 +542,16 @@ function createFirstOutputReporter(onFirstOutput?: () => void): {
|
|
|
322
542
|
}
|
|
323
543
|
|
|
324
544
|
export function terminalStatusFromParsed(parsed: unknown): ResponsesTerminalStatus | null {
|
|
325
|
-
|
|
326
|
-
switch (
|
|
545
|
+
const type = asJsonRecord(parsed)?.type;
|
|
546
|
+
switch (type) {
|
|
327
547
|
case "response.completed":
|
|
328
548
|
return "completed";
|
|
329
549
|
case "response.failed":
|
|
330
550
|
return "failed";
|
|
331
551
|
case "response.incomplete":
|
|
332
|
-
return "incomplete";
|
|
552
|
+
return cyberPolicyTerminalError(parsed) ? "failed" : "incomplete";
|
|
553
|
+
case "error":
|
|
554
|
+
return cyberPolicyTerminalError(parsed) ? "failed" : null;
|
|
333
555
|
default:
|
|
334
556
|
return null;
|
|
335
557
|
}
|
|
@@ -800,6 +1022,9 @@ export function createSseInspector(handlers: SseInspectorHandlers): SseInspector
|
|
|
800
1022
|
}
|
|
801
1023
|
reportFirstOutput.parsed(parsed);
|
|
802
1024
|
const status = terminalStatusFromParsed(parsed);
|
|
1025
|
+
const policyTerminal = status === "failed"
|
|
1026
|
+
&& isPolicyRewriteType(parsed)
|
|
1027
|
+
&& cyberPolicyTerminalError(parsed) !== undefined;
|
|
803
1028
|
if (status) sawTerminal = true;
|
|
804
1029
|
if (!reported && handlers.onTerminal && status) {
|
|
805
1030
|
try {
|
|
@@ -808,7 +1033,7 @@ export function createSseInspector(handlers: SseInspectorHandlers): SseInspector
|
|
|
808
1033
|
handlers.logCtx.transportPhase = "terminal_sse";
|
|
809
1034
|
handlers.logCtx.terminalSource = "upstream";
|
|
810
1035
|
}
|
|
811
|
-
handlers.onTerminal(status);
|
|
1036
|
+
handlers.onTerminal(status, policyTerminal ? 400 : undefined);
|
|
812
1037
|
} finally {
|
|
813
1038
|
if (status === "failed" || status === "incomplete") clearCompletedItems();
|
|
814
1039
|
}
|
|
@@ -1087,6 +1312,13 @@ function startBoundedInspectionPump(options: InspectionPumpOptions): void {
|
|
|
1087
1312
|
}
|
|
1088
1313
|
}
|
|
1089
1314
|
} catch {
|
|
1315
|
+
// A read error can follow a final SSE block without its blank-line
|
|
1316
|
+
// delimiter. Flush that candidate before classifying the transport as a
|
|
1317
|
+
// synthetic reset; otherwise a real completed/failed/policy terminal is
|
|
1318
|
+
// downgraded to 502 on the inspection branch.
|
|
1319
|
+
if (!cancelled) {
|
|
1320
|
+
try { inspector.finish(); } catch { /* preserve the original read error */ }
|
|
1321
|
+
}
|
|
1090
1322
|
if (clientGone) clientGoneWithoutTerminal = !inspector.terminalSeen();
|
|
1091
1323
|
else if (!cancelled) options.onReadError?.();
|
|
1092
1324
|
} finally {
|
|
@@ -61,6 +61,39 @@ export function sessionIdHeaderFromRequest(headers: Headers): string | null {
|
|
|
61
61
|
return headers.get("session_id") ?? headers.get("session-id");
|
|
62
62
|
}
|
|
63
63
|
|
|
64
|
+
/**
|
|
65
|
+
* Codex thread identity from inbound Responses headers when the parent-thread header is absent.
|
|
66
|
+
* Priority: x-codex-parent-thread-id > thread-id > session_id / session-id.
|
|
67
|
+
*/
|
|
68
|
+
export function inboundClientThreadIdFromRequest(headers: Headers): string | undefined {
|
|
69
|
+
return firstSanitizedConversationId(
|
|
70
|
+
headers.get("x-codex-parent-thread-id"),
|
|
71
|
+
headers.get("thread-id"),
|
|
72
|
+
sessionIdHeaderFromRequest(headers),
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Fixed-size logical turn lane (#820).
|
|
78
|
+
*
|
|
79
|
+
* A lane must be as SPECIFIC as the identity available, which is the opposite of what
|
|
80
|
+
* `codexPoolAffinityKey` wants. Affinity deliberately prefers the parent thread so a whole
|
|
81
|
+
* subagent fan-out pins to one account; a lane keyed that way would put every parallel
|
|
82
|
+
* subagent of one parent into a single lane and reject all but the first with 503 — the
|
|
83
|
+
* fan-out is the normal case, not an abuse.
|
|
84
|
+
*
|
|
85
|
+
* So the parent is a QUALIFIER, never the lane on its own when a child thread exists: the
|
|
86
|
+
* pair separates siblings while still keeping one conversation's overlapping turns together.
|
|
87
|
+
*/
|
|
88
|
+
export function sessionLaneIdFromRequest(headers: Headers): string | undefined {
|
|
89
|
+
const parent = normalizeLogConversationId(headers.get("x-codex-parent-thread-id"));
|
|
90
|
+
const thread = normalizeLogConversationId(headers.get("thread-id"));
|
|
91
|
+
const session = normalizeLogConversationId(sessionIdHeaderFromRequest(headers));
|
|
92
|
+
const specific = thread ?? session;
|
|
93
|
+
if (parent && specific) return `${parent}\u0000${specific}`;
|
|
94
|
+
return specific ?? parent;
|
|
95
|
+
}
|
|
96
|
+
|
|
64
97
|
function firstSanitizedConversationId(
|
|
65
98
|
...values: Array<string | null | undefined>
|
|
66
99
|
): string | undefined {
|
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
httpStatusFromTerminalError as httpStatusFromClassifiedTerminalError,
|
|
7
7
|
isClientClosedMessage,
|
|
8
8
|
isCyberPolicyCode,
|
|
9
|
+
isCyberPolicyMessage,
|
|
9
10
|
} from "../lib/errors";
|
|
10
11
|
import { CODEX_CONFIG_PATH, readRootTomlString } from "../codex/paths";
|
|
11
12
|
import { readCodexCatalogPath } from "../codex/catalog";
|
|
@@ -19,7 +20,7 @@ import {
|
|
|
19
20
|
isKnownAdmissionKind,
|
|
20
21
|
isKnownInboundProtocol,
|
|
21
22
|
isKnownUsageSurface,
|
|
22
|
-
|
|
23
|
+
isPersistableAccountLogLabel,
|
|
23
24
|
isValidReasoningWireValue,
|
|
24
25
|
readRecentUsageEntries,
|
|
25
26
|
usageForFinalLog,
|
|
@@ -65,6 +66,8 @@ export interface RequestLogContext {
|
|
|
65
66
|
/** Stable non-PII Codex Pool account identity for durable usage attribution. */
|
|
66
67
|
accountLogLabel?: string;
|
|
67
68
|
requestedModel?: string;
|
|
69
|
+
/** User-facing alias selector when routing resolved one; native model remains `model`. */
|
|
70
|
+
requestedAlias?: string;
|
|
68
71
|
/** Original bare helper model when the opt-in shadow-call route rewrote this request. */
|
|
69
72
|
shadowCallRewrittenFrom?: string;
|
|
70
73
|
/** Internal structural combo identity; omitted from RequestLogEntry/JSONL. */
|
|
@@ -144,6 +147,7 @@ export interface RequestLogEntry {
|
|
|
144
147
|
/** Best-effort chat/session correlation for Logs grouping (#330). */
|
|
145
148
|
conversationId?: string;
|
|
146
149
|
requestedModel?: string;
|
|
150
|
+
requestedAlias?: string;
|
|
147
151
|
/** Original bare helper model when the opt-in shadow-call route rewrote this request. */
|
|
148
152
|
shadowCallRewrittenFrom?: string;
|
|
149
153
|
requestedEffort?: string;
|
|
@@ -255,10 +259,11 @@ export function requestLogEntryFromPersistedUsage(entry: PersistedUsageEntry): R
|
|
|
255
259
|
...(entry.firstOutputMs !== undefined ? { firstOutputMs: entry.firstOutputMs } : {}),
|
|
256
260
|
...(isKnownUsageSurface(entry.surface) ? { surface: entry.surface } : {}),
|
|
257
261
|
...(entry.conversationId ? { conversationId: entry.conversationId } : {}),
|
|
258
|
-
...(
|
|
262
|
+
...(isPersistableAccountLogLabel(entry.accountLogLabel)
|
|
259
263
|
? { accountLogLabel: entry.accountLogLabel }
|
|
260
264
|
: {}),
|
|
261
265
|
...(entry.requestedModel ? { requestedModel: entry.requestedModel } : {}),
|
|
266
|
+
...(entry.requestedAlias ? { requestedAlias: entry.requestedAlias } : {}),
|
|
262
267
|
...(entry.shadowCallRewrittenFrom
|
|
263
268
|
? { shadowCallRewrittenFrom: entry.shadowCallRewrittenFrom }
|
|
264
269
|
: {}),
|
|
@@ -373,12 +378,13 @@ export function addRequestLog(entry: RequestLogEntry) {
|
|
|
373
378
|
...(entry.apiKeyId ? { apiKeyId: entry.apiKeyId } : {}),
|
|
374
379
|
...(isKnownAdmissionKind(entry.admissionKind) ? { admissionKind: entry.admissionKind } : {}),
|
|
375
380
|
...(isKnownInboundProtocol(entry.inboundProtocol) ? { inboundProtocol: entry.inboundProtocol } : {}),
|
|
376
|
-
...(
|
|
381
|
+
...(isPersistableAccountLogLabel(entry.accountLogLabel)
|
|
377
382
|
? { accountLogLabel: entry.accountLogLabel }
|
|
378
383
|
: {}),
|
|
379
384
|
...(entry.conversationId ? { conversationId: entry.conversationId } : {}),
|
|
380
385
|
...(entry.resolvedModel ? { resolvedModel: entry.resolvedModel } : {}),
|
|
381
386
|
...(entry.requestedModel ? { requestedModel: entry.requestedModel } : {}),
|
|
387
|
+
...(entry.requestedAlias ? { requestedAlias: entry.requestedAlias } : {}),
|
|
382
388
|
...(entry.shadowCallRewrittenFrom
|
|
383
389
|
? { shadowCallRewrittenFrom: entry.shadowCallRewrittenFrom }
|
|
384
390
|
: {}),
|
|
@@ -765,7 +771,7 @@ function captureUpstreamErrorParsed(
|
|
|
765
771
|
last_error?: { message?: unknown };
|
|
766
772
|
response?: {
|
|
767
773
|
error?: { type?: unknown; code?: unknown; message?: unknown };
|
|
768
|
-
incomplete_details?: { reason?: unknown };
|
|
774
|
+
incomplete_details?: { reason?: unknown; message?: unknown };
|
|
769
775
|
};
|
|
770
776
|
};
|
|
771
777
|
captureTerminalHttpStatus(logCtx, json);
|
|
@@ -779,7 +785,8 @@ function captureUpstreamErrorParsed(
|
|
|
779
785
|
if (logCtx.upstreamError) return;
|
|
780
786
|
const message = json?.error?.message
|
|
781
787
|
?? json?.last_error?.message
|
|
782
|
-
?? json?.response?.error?.message
|
|
788
|
+
?? json?.response?.error?.message
|
|
789
|
+
?? json?.response?.incomplete_details?.message;
|
|
783
790
|
if (typeof message === "string" && message.trim()) {
|
|
784
791
|
logCtx.upstreamError = redactSecretString(message).slice(0, 500);
|
|
785
792
|
return;
|
|
@@ -818,25 +825,43 @@ function captureTerminalHttpStatus(
|
|
|
818
825
|
logCtx: RequestLogContext,
|
|
819
826
|
json: {
|
|
820
827
|
type?: unknown;
|
|
821
|
-
|
|
828
|
+
code?: unknown;
|
|
829
|
+
message?: unknown;
|
|
830
|
+
error?: { type?: unknown; code?: unknown; message?: unknown };
|
|
831
|
+
last_error?: { type?: unknown; code?: unknown; message?: unknown };
|
|
832
|
+
response?: {
|
|
833
|
+
error?: { type?: unknown; code?: unknown; message?: unknown };
|
|
834
|
+
incomplete_details?: { code?: unknown; message?: unknown };
|
|
835
|
+
};
|
|
822
836
|
},
|
|
823
837
|
): void {
|
|
824
838
|
if (logCtx.terminalHttpStatus !== undefined) return;
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
const
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
839
|
+
const type = json.type;
|
|
840
|
+
if (type !== "response.failed" && type !== "response.incomplete" && type !== "error") return;
|
|
841
|
+
const responseError = json.response?.error;
|
|
842
|
+
const responseDetails = json.response?.incomplete_details;
|
|
843
|
+
const candidates = [json.error, json.last_error, responseError, responseDetails, json];
|
|
844
|
+
const policy = candidates.some(candidate => (
|
|
845
|
+
candidate?.code === null || typeof candidate?.code === "string"
|
|
846
|
+
) && isCyberPolicyCode(candidate.code as string | null | undefined))
|
|
847
|
+
|| candidates.some(candidate => (
|
|
848
|
+
typeof candidate?.message === "string"
|
|
849
|
+
&& candidate.message.trim().length > 0
|
|
850
|
+
&& isCyberPolicyMessage(candidate.message)
|
|
851
|
+
));
|
|
852
|
+
if (policy) {
|
|
832
853
|
logCtx.terminalErrorCode = CYBER_POLICY_ERROR_CODE;
|
|
833
|
-
|
|
834
|
-
|
|
854
|
+
logCtx.terminalHttpStatus = 400;
|
|
855
|
+
return;
|
|
835
856
|
}
|
|
857
|
+
if (type !== "response.failed" || !responseError || typeof responseError !== "object") return;
|
|
858
|
+
const responseCode = responseError.code === null || typeof responseError.code === "string"
|
|
859
|
+
? responseError.code
|
|
860
|
+
: undefined;
|
|
836
861
|
logCtx.terminalHttpStatus = httpStatusFromTerminalError({
|
|
837
|
-
type: typeof
|
|
838
|
-
code:
|
|
839
|
-
message: typeof
|
|
862
|
+
type: typeof responseError.type === "string" ? responseError.type : undefined,
|
|
863
|
+
code: responseCode,
|
|
864
|
+
message: typeof responseError.message === "string" ? responseError.message : undefined,
|
|
840
865
|
});
|
|
841
866
|
}
|
|
842
867
|
|
|
@@ -945,11 +970,12 @@ export function addFinalRequestLog(
|
|
|
945
970
|
...(logCtx.apiKeyId ? { apiKeyId: logCtx.apiKeyId } : {}),
|
|
946
971
|
...(logCtx.admissionKind ? { admissionKind: logCtx.admissionKind } : {}),
|
|
947
972
|
...(logCtx.inboundProtocol ? { inboundProtocol: logCtx.inboundProtocol } : {}),
|
|
948
|
-
...(
|
|
973
|
+
...(isPersistableAccountLogLabel(logCtx.accountLogLabel)
|
|
949
974
|
? { accountLogLabel: logCtx.accountLogLabel }
|
|
950
975
|
: {}),
|
|
951
976
|
...(logCtx.conversationId ? { conversationId: logCtx.conversationId } : {}),
|
|
952
977
|
...(logCtx.requestedModel ? { requestedModel: logCtx.requestedModel } : {}),
|
|
978
|
+
...(logCtx.requestedAlias ? { requestedAlias: logCtx.requestedAlias } : {}),
|
|
953
979
|
...(shadowCallRewrittenFrom ? { shadowCallRewrittenFrom } : {}),
|
|
954
980
|
...(logCtx.requestedEffort ? { requestedEffort: logCtx.requestedEffort } : {}),
|
|
955
981
|
...(logCtx.effectiveEffort ? { effectiveEffort: logCtx.effectiveEffort } : {}),
|
|
@@ -1170,7 +1196,7 @@ export function sealRequestAttemptIdentity(
|
|
|
1170
1196
|
if (!attempt) return;
|
|
1171
1197
|
attempt.provider = provider;
|
|
1172
1198
|
attempt.adapter = adapter;
|
|
1173
|
-
if (
|
|
1199
|
+
if (isPersistableAccountLogLabel(accountLogLabel)) attempt.accountLogLabel = accountLogLabel;
|
|
1174
1200
|
}
|
|
1175
1201
|
|
|
1176
1202
|
export function noteAttemptSend(
|
|
@@ -1200,6 +1226,7 @@ export function finishRequestAttempt(
|
|
|
1200
1226
|
status: number,
|
|
1201
1227
|
durationMs: number,
|
|
1202
1228
|
usage?: OcxUsage,
|
|
1229
|
+
upstreamError?: string,
|
|
1203
1230
|
): PersistedUsageAttempt {
|
|
1204
1231
|
const finalized = finalizedUsage(
|
|
1205
1232
|
attempt.adapter,
|
|
@@ -1214,7 +1241,7 @@ export function finishRequestAttempt(
|
|
|
1214
1241
|
else delete attempt.usage;
|
|
1215
1242
|
if (finalized.totalTokens !== undefined) attempt.totalTokens = finalized.totalTokens;
|
|
1216
1243
|
else delete attempt.totalTokens;
|
|
1217
|
-
const errorCode = requestLogErrorCode(status);
|
|
1244
|
+
const errorCode = requestLogErrorCode(status, upstreamError);
|
|
1218
1245
|
if (errorCode) attempt.errorCode = errorCode;
|
|
1219
1246
|
else delete attempt.errorCode;
|
|
1220
1247
|
return attempt;
|