@iislee/opencodex 2.31.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/bin/ocx.mjs +99 -70
- package/gui/dist/assets/index-Be1sDpWw.css +1 -0
- package/gui/dist/assets/index-D4OKgUV4.js +114 -0
- package/gui/dist/index.html +2 -2
- package/package.json +3 -1
- package/src/adapters/anthropic.ts +104 -15
- package/src/adapters/base.ts +29 -1
- package/src/adapters/command-code.ts +42 -6
- package/src/adapters/cursor/call-id.ts +44 -0
- package/src/adapters/cursor/catalog.ts +541 -0
- package/src/adapters/cursor/checkpoint-store.ts +15 -10
- package/src/adapters/cursor/cursor-errors.ts +15 -0
- package/src/adapters/cursor/discovery.ts +69 -18
- package/src/adapters/cursor/effort-map.ts +79 -1
- package/src/adapters/cursor/envelope-echo.ts +290 -0
- package/src/adapters/cursor/live-models.ts +7 -2
- package/src/adapters/cursor/live-transport.ts +14 -0
- package/src/adapters/cursor/message-mapper.ts +4 -1
- package/src/adapters/cursor/native-exec-desktop.ts +23 -0
- package/src/adapters/cursor/native-exec-fs.ts +2 -2
- package/src/adapters/cursor/native-exec-network.ts +1 -1
- package/src/adapters/cursor/native-exec-shell.ts +5 -2
- package/src/adapters/cursor/native-exec.ts +20 -0
- package/src/adapters/cursor/protobuf-request.ts +68 -14
- package/src/adapters/cursor/request-builder.ts +40 -10
- package/src/adapters/cursor/tool-definitions.ts +14 -4
- package/src/adapters/cursor/tool-result-normalize.ts +28 -5
- package/src/adapters/cursor/types.ts +20 -0
- package/src/adapters/cursor.ts +209 -38
- package/src/adapters/exec-tool-result-normalize.ts +99 -0
- package/src/adapters/google-antigravity-replay.ts +173 -25
- package/src/adapters/google-antigravity-wire.ts +5 -0
- package/src/adapters/google-errors.ts +44 -12
- package/src/adapters/google.ts +136 -24
- package/src/adapters/image.ts +1 -1
- package/src/adapters/kiro-constants.ts +27 -0
- package/src/adapters/kiro-tools.ts +55 -16
- package/src/adapters/kiro.ts +182 -23
- package/src/adapters/openai-chat.ts +300 -242
- package/src/adapters/openai-responses.ts +353 -27
- package/src/adapters/run-turn-queue.ts +36 -1
- package/src/adapters/tool-catalog-nudge.ts +4 -3
- package/src/adapters/xai-tool-schema.ts +436 -0
- package/src/adapters/xai-web-search.ts +17 -16
- package/src/bridge.ts +90 -37
- package/src/chat/inbound.ts +14 -1
- package/src/chat/outbound.ts +15 -7
- package/src/claude/agents-inject.ts +8 -1
- package/src/claude/context-windows.ts +16 -9
- package/src/claude/outbound.ts +24 -11
- package/src/cli/access.ts +46 -3
- package/src/cli/account-api.ts +116 -19
- package/src/cli/account-extended.ts +262 -29
- package/src/cli/account-main.ts +12 -12
- package/src/cli/account.ts +69 -15
- package/src/cli/agent.ts +8 -1
- package/src/cli/alias.ts +66 -0
- package/src/cli/capabilities-command.ts +94 -0
- package/src/cli/capabilities.ts +496 -0
- package/src/cli/claude-desktop.ts +31 -11
- package/src/cli/claude.ts +26 -1
- package/src/cli/dispatch.ts +256 -28
- package/src/cli/doctor.ts +126 -14
- package/src/cli/help.ts +17 -6
- package/src/cli/index.ts +39 -9
- package/src/cli/init.ts +1 -0
- package/src/cli/inspect.ts +230 -0
- package/src/cli/models-runtime.ts +95 -0
- package/src/cli/models.ts +26 -10
- package/src/cli/observe.ts +31 -8
- package/src/cli/provider-runtime.ts +16 -2
- package/src/cli/provider.ts +2 -1
- package/src/cli/registry.ts +49 -10
- package/src/cli/runtime-api.ts +51 -7
- package/src/cli/status.ts +41 -1
- package/src/cli/storage.ts +234 -0
- package/src/cli/system-command.ts +16 -0
- package/src/cli/system-restart-client.ts +1 -1
- package/src/cli/usage-report.ts +184 -0
- package/src/cli/v2.ts +34 -10
- package/src/cli/version-skew.ts +46 -0
- package/src/clients/config-export.ts +85 -3
- package/src/clients/effective-status.ts +30 -2
- package/src/clients/probes/cline.ts +81 -0
- package/src/codex/account-label.ts +21 -0
- package/src/codex/account-pause.ts +2 -1
- package/src/codex/account-priority.ts +3 -2
- package/src/codex/app-server-processes.ts +83 -7
- package/src/codex/auth-api.ts +50 -10
- package/src/codex/auth-context.ts +21 -18
- package/src/codex/autostart-health.ts +16 -0
- package/src/codex/catalog/aggregation.ts +18 -0
- package/src/codex/catalog/effort.ts +18 -3
- package/src/codex/catalog/metadata.ts +27 -1
- package/src/codex/catalog/native-models.ts +5 -2
- package/src/codex/catalog/parsing.ts +72 -27
- package/src/codex/catalog/provider-fetch.ts +187 -31
- package/src/codex/catalog/sync.ts +131 -5
- 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 +27 -2
- package/src/codex/desired-state.ts +2 -2
- package/src/codex/desktop-app-restart.ts +355 -0
- package/src/codex/history-job.ts +32 -3
- package/src/codex/history-manifest.ts +112 -0
- package/src/codex/history-migration-guardian.ts +5 -5
- package/src/codex/history-provider.ts +825 -247
- package/src/codex/history-worker.ts +8 -5
- package/src/codex/inject-coordination.ts +83 -0
- package/src/codex/inject.ts +63 -22
- package/src/codex/injected-marker.ts +1 -1
- package/src/codex/internal/history-writer.ts +4 -3
- package/src/codex/log-guard/inspect.ts +22 -4
- package/src/codex/model-entitlements.ts +9 -2
- package/src/codex/native-profile-startup.ts +157 -27
- package/src/codex/native-residue.ts +26 -33
- package/src/codex/prompt-layers.ts +371 -25
- package/src/codex/prompt-text-probe.ts +238 -0
- package/src/codex/quota.ts +91 -10
- package/src/codex/routing.ts +9 -0
- package/src/codex/shim.ts +56 -3
- package/src/codex/subagent-model-fallback.ts +104 -25
- package/src/codex/transition-state.ts +116 -8
- package/src/combos/failover.ts +27 -0
- package/src/combos/types.ts +10 -0
- package/src/compatibility/index.ts +26 -0
- package/src/compatibility/manifest.ts +253 -0
- package/src/compatibility/openai-responses.ts +121 -0
- package/src/config/atomic-write.ts +219 -0
- package/src/config/paths.ts +40 -0
- package/src/config/process-state.ts +309 -0
- package/src/config/provider-validation.ts +177 -0
- package/src/config/rebase-provenance.ts +68 -0
- package/src/config.ts +206 -824
- package/src/generated/compatibility-version.json +373 -213
- package/src/images/loop.ts +5 -4
- package/src/images/plan.ts +5 -4
- package/src/integrations/ownership-policy.ts +160 -0
- package/src/integrations/ownership.ts +46 -2
- package/src/integrations/registry.ts +7 -0
- package/src/integrations/state.ts +79 -7
- package/src/integrations/writer.ts +26 -2
- package/src/lab/conformance/fixtures/protocol-v1-cases.json +1 -1
- package/src/lab/fabric/producer-child.ts +1 -1
- package/src/lib/admin-secrets.ts +24 -0
- package/src/lib/bounded-body.ts +14 -2
- package/src/lib/config-ownership.ts +20 -0
- package/src/lib/errors.ts +36 -3
- package/src/lib/package-tree-integrity.ts +101 -0
- package/src/lib/process-control.ts +2 -1
- package/src/lib/service-secrets.ts +15 -0
- package/src/lib/tool-argument-integers.ts +56 -5
- package/src/oauth/callback-server.ts +22 -2
- package/src/oauth/cline.ts +203 -0
- package/src/oauth/generic-account-failover.ts +231 -0
- package/src/oauth/health.ts +1 -1
- package/src/oauth/index.ts +65 -6
- package/src/oauth/key-providers.ts +10 -1
- package/src/oauth/kimi.ts +9 -1
- package/src/oauth/kiro.ts +45 -0
- package/src/oauth/local-token-detect.ts +2 -0
- package/src/oauth/open-browser-choice.ts +26 -0
- package/src/oauth/store.ts +14 -5
- package/src/oauth/types.ts +15 -0
- package/src/providers/auto-compact-budget.ts +65 -0
- 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 +28 -0
- package/src/providers/fastwire.ts +7 -1
- package/src/providers/label.ts +34 -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 +65 -7
- package/src/providers/registry.ts +184 -69
- package/src/providers/slug-codec.ts +52 -0
- package/src/providers/xai-transport.ts +21 -0
- package/src/reasoning-effort.ts +19 -2
- package/src/responses/apply-patch-envelope.ts +63 -0
- package/src/responses/code-mode-helper-compat.ts +50 -0
- package/src/responses/custom-tool-compat.ts +157 -39
- package/src/responses/namespace-tool-compat.ts +84 -4
- package/src/responses/parser.ts +7 -2
- package/src/responses/reasoning-replay-cache.ts +81 -3
- package/src/responses/schema.ts +5 -1
- package/src/responses/state.ts +94 -4
- package/src/responses/thought-signature-replay.ts +17 -0
- package/src/responses/turn-termination.ts +107 -0
- package/src/router.ts +40 -2
- package/src/routing/analytics.ts +1 -0
- package/src/routing/quota.ts +10 -0
- package/src/server/auth-cors.ts +27 -7
- package/src/server/chat-completions.ts +24 -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/index.ts +179 -32
- package/src/server/lifecycle.ts +52 -1
- package/src/server/local-management-read-client.ts +1 -1
- package/src/server/local-provider-reload-client.ts +1 -1
- package/src/server/management/agent-settings-routes.ts +45 -23
- 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 +59 -12
- package/src/server/management/context.ts +10 -1
- package/src/server/management/logs-usage-routes.ts +30 -24
- package/src/server/management/model-routes.ts +274 -4
- package/src/server/management/model-rows.ts +4 -0
- package/src/server/management/native-integration-routes.ts +2 -1
- package/src/server/management/oauth-account-routes.ts +10 -4
- package/src/server/management/provider-capability-config.ts +1 -1
- package/src/server/management/provider-routes.ts +81 -11
- package/src/server/management/route-registry.ts +332 -0
- package/src/server/management/routing-profile-routes.ts +2 -2
- package/src/server/management/system-restart.ts +1 -1
- package/src/server/management-api.ts +18 -16
- package/src/server/port-reclaim.ts +1 -1
- package/src/server/proxy-liveness.ts +29 -5
- package/src/server/relay-eager.ts +147 -20
- package/src/server/relay.ts +251 -19
- package/src/server/request-log-conversation.ts +51 -0
- package/src/server/request-log.ts +70 -16
- package/src/server/responses/codex-auth-error.ts +55 -0
- package/src/server/responses/collaboration.ts +37 -3
- package/src/server/responses/combo-stream-preflight.ts +178 -0
- package/src/server/responses/compact.ts +6 -21
- package/src/server/responses/core.ts +810 -208
- package/src/server/responses/empty-completion-guard.ts +63 -6
- package/src/server/responses/fetch-helpers.ts +17 -102
- package/src/server/responses/input-admission.ts +3 -1
- package/src/server/responses/passthrough-error.ts +33 -9
- package/src/server/responses/responses-field-backfill.ts +105 -13
- package/src/server/responses/ws-upstream.ts +75 -2
- package/src/server/responses-custom-tool-repair.ts +87 -6
- package/src/server/responses-terminal-repair.ts +25 -4
- package/src/server/responses-undeclared-tool-guard.ts +241 -18
- package/src/server/sse-frame-buffer.ts +31 -4
- package/src/server/ws-bridge.ts +14 -2
- package/src/service.ts +42 -4
- package/src/storage/policy-job.ts +14 -4
- package/src/storage/policy.ts +88 -23
- package/src/types/config.ts +66 -0
- package/src/types/provider.ts +69 -0
- package/src/types/request.ts +17 -4
- package/src/types/tools.ts +116 -11
- package/src/types.ts +3 -1
- package/src/update/index.ts +5 -4
- package/src/update/job.ts +3 -1
- package/src/update/transactional-install.mjs +8 -1
- package/src/usage/expected-prices.ts +43 -24
- package/src/usage/log.ts +63 -12
- package/src/usage/summary.ts +211 -8
- package/src/vision/describe.ts +18 -13
- package/src/web-search/exa-executor.ts +40 -9
- package/src/web-search/executor.ts +10 -3
- package/src/web-search/loop.ts +5 -4
- package/gui/dist/assets/index-BF38heuV.js +0 -104
- package/gui/dist/assets/index-DMiI18Kv.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,57 @@ export function sessionIdHeaderFromRequest(headers: Headers): string | null {
|
|
|
61
61
|
return headers.get("session_id") ?? headers.get("session-id");
|
|
62
62
|
}
|
|
63
63
|
|
|
64
|
+
/**
|
|
65
|
+
* Fixed-size logical turn lane (#820).
|
|
66
|
+
*
|
|
67
|
+
* A lane must be as SPECIFIC as the identity available, which is the opposite of what
|
|
68
|
+
* `codexPoolAffinityKey` wants. Affinity deliberately prefers the parent thread so a whole
|
|
69
|
+
* subagent fan-out pins to one account; a lane keyed that way would put every parallel
|
|
70
|
+
* subagent of one parent into a single lane and reject all but the first with 503 — the
|
|
71
|
+
* fan-out is the normal case, not an abuse.
|
|
72
|
+
*
|
|
73
|
+
* So the parent is a QUALIFIER, never the lane on its own when a child thread exists: the
|
|
74
|
+
* pair separates siblings while still keeping one conversation's overlapping turns together.
|
|
75
|
+
*/
|
|
76
|
+
export function sessionLaneIdFromRequest(headers: Headers): string | undefined {
|
|
77
|
+
const parent = normalizeLogConversationId(headers.get("x-codex-parent-thread-id"));
|
|
78
|
+
const thread = normalizeLogConversationId(headers.get("thread-id"));
|
|
79
|
+
const session = normalizeLogConversationId(sessionIdHeaderFromRequest(headers));
|
|
80
|
+
const specific = thread ?? session;
|
|
81
|
+
if (parent && specific) return `${parent}\u0000${specific}`;
|
|
82
|
+
return specific ?? parent;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function firstSanitizedConversationId(
|
|
86
|
+
...values: Array<string | null | undefined>
|
|
87
|
+
): string | undefined {
|
|
88
|
+
for (const value of values) {
|
|
89
|
+
const sanitized = sanitizeConversationIdInput(value);
|
|
90
|
+
if (sanitized) return sanitized;
|
|
91
|
+
}
|
|
92
|
+
return undefined;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Conversation namespace for reasoning replay. Unlike the persisted log id, this stays the raw
|
|
97
|
+
* sanitized identity so mixed headers that carry the same conversation still hit one serving
|
|
98
|
+
* record. Do not hash, and do not prefer session_id over a true per-conversation thread/Cursor
|
|
99
|
+
* identity: session_id can be synthesized from a shared prompt_cache_key.
|
|
100
|
+
*/
|
|
101
|
+
export function reasoningReplayConversationIdFromResponsesRequest(input: {
|
|
102
|
+
clientThreadId?: string;
|
|
103
|
+
threadIdHeader?: string | null;
|
|
104
|
+
cursorConversationId?: string;
|
|
105
|
+
sessionIdHeader?: string | null;
|
|
106
|
+
}): string | undefined {
|
|
107
|
+
return firstSanitizedConversationId(
|
|
108
|
+
input.clientThreadId,
|
|
109
|
+
input.threadIdHeader,
|
|
110
|
+
input.cursorConversationId,
|
|
111
|
+
input.sessionIdHeader,
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
|
|
64
115
|
export function conversationIdFromResponsesRequest(input: {
|
|
65
116
|
clientThreadId?: string;
|
|
66
117
|
sessionIdHeader?: string | null;
|
|
@@ -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";
|
|
@@ -62,9 +63,18 @@ export interface RequestLogContext {
|
|
|
62
63
|
* product: widening that enum would merge Responses and Chat Completions,
|
|
63
64
|
* since both leave it undefined. */
|
|
64
65
|
inboundProtocol?: "responses" | "chat" | "messages";
|
|
66
|
+
/**
|
|
67
|
+
* Set when an adapter answered the turn locally and no upstream request was made
|
|
68
|
+
* (`ProviderAdapter.localTerminal`). A fixed identifier naming the code path, never
|
|
69
|
+
* conversation-derived: it exists so a request log showing zero sends is explainable
|
|
70
|
+
* rather than looking like a lost request.
|
|
71
|
+
*/
|
|
72
|
+
localTerminalReason?: string;
|
|
65
73
|
/** Stable non-PII Codex Pool account identity for durable usage attribution. */
|
|
66
74
|
accountLogLabel?: string;
|
|
67
75
|
requestedModel?: string;
|
|
76
|
+
/** User-facing alias selector when routing resolved one; native model remains `model`. */
|
|
77
|
+
requestedAlias?: string;
|
|
68
78
|
/** Original bare helper model when the opt-in shadow-call route rewrote this request. */
|
|
69
79
|
shadowCallRewrittenFrom?: string;
|
|
70
80
|
/** Internal structural combo identity; omitted from RequestLogEntry/JSONL. */
|
|
@@ -130,6 +140,12 @@ export interface RequestLogEntry {
|
|
|
130
140
|
/** TTFT: ms from request start to the first non-empty model output delta; unset for non-streaming/tool-only. */
|
|
131
141
|
firstOutputMs?: number;
|
|
132
142
|
surface?: "claude" | "claude-desktop" | "grok";
|
|
143
|
+
/**
|
|
144
|
+
* Set when the proxy answered this turn locally and sent nothing upstream. Without it a zero-send
|
|
145
|
+
* row is indistinguishable from a request that vanished. A fixed adapter-supplied identifier,
|
|
146
|
+
* never conversation-derived.
|
|
147
|
+
*/
|
|
148
|
+
localTerminalReason?: string;
|
|
133
149
|
/** The matched configured key's id. Set ONLY for admissionKind "configured" —
|
|
134
150
|
* never a sentinel, so a hand-edited entry whose id happens to be "loopback"
|
|
135
151
|
* cannot absorb unrelated traffic. */
|
|
@@ -144,6 +160,7 @@ export interface RequestLogEntry {
|
|
|
144
160
|
/** Best-effort chat/session correlation for Logs grouping (#330). */
|
|
145
161
|
conversationId?: string;
|
|
146
162
|
requestedModel?: string;
|
|
163
|
+
requestedAlias?: string;
|
|
147
164
|
/** Original bare helper model when the opt-in shadow-call route rewrote this request. */
|
|
148
165
|
shadowCallRewrittenFrom?: string;
|
|
149
166
|
requestedEffort?: string;
|
|
@@ -259,6 +276,7 @@ export function requestLogEntryFromPersistedUsage(entry: PersistedUsageEntry): R
|
|
|
259
276
|
? { accountLogLabel: entry.accountLogLabel }
|
|
260
277
|
: {}),
|
|
261
278
|
...(entry.requestedModel ? { requestedModel: entry.requestedModel } : {}),
|
|
279
|
+
...(entry.requestedAlias ? { requestedAlias: entry.requestedAlias } : {}),
|
|
262
280
|
...(entry.shadowCallRewrittenFrom
|
|
263
281
|
? { shadowCallRewrittenFrom: entry.shadowCallRewrittenFrom }
|
|
264
282
|
: {}),
|
|
@@ -379,6 +397,7 @@ export function addRequestLog(entry: RequestLogEntry) {
|
|
|
379
397
|
...(entry.conversationId ? { conversationId: entry.conversationId } : {}),
|
|
380
398
|
...(entry.resolvedModel ? { resolvedModel: entry.resolvedModel } : {}),
|
|
381
399
|
...(entry.requestedModel ? { requestedModel: entry.requestedModel } : {}),
|
|
400
|
+
...(entry.requestedAlias ? { requestedAlias: entry.requestedAlias } : {}),
|
|
382
401
|
...(entry.shadowCallRewrittenFrom
|
|
383
402
|
? { shadowCallRewrittenFrom: entry.shadowCallRewrittenFrom }
|
|
384
403
|
: {}),
|
|
@@ -764,7 +783,7 @@ function captureUpstreamErrorParsed(
|
|
|
764
783
|
last_error?: { message?: unknown };
|
|
765
784
|
response?: {
|
|
766
785
|
error?: { type?: unknown; code?: unknown; message?: unknown };
|
|
767
|
-
incomplete_details?: { reason?: unknown };
|
|
786
|
+
incomplete_details?: { reason?: unknown; message?: unknown };
|
|
768
787
|
};
|
|
769
788
|
};
|
|
770
789
|
captureTerminalHttpStatus(logCtx, json);
|
|
@@ -778,7 +797,8 @@ function captureUpstreamErrorParsed(
|
|
|
778
797
|
if (logCtx.upstreamError) return;
|
|
779
798
|
const message = json?.error?.message
|
|
780
799
|
?? json?.last_error?.message
|
|
781
|
-
?? json?.response?.error?.message
|
|
800
|
+
?? json?.response?.error?.message
|
|
801
|
+
?? json?.response?.incomplete_details?.message;
|
|
782
802
|
if (typeof message === "string" && message.trim()) {
|
|
783
803
|
logCtx.upstreamError = redactSecretString(message).slice(0, 500);
|
|
784
804
|
return;
|
|
@@ -817,25 +837,43 @@ function captureTerminalHttpStatus(
|
|
|
817
837
|
logCtx: RequestLogContext,
|
|
818
838
|
json: {
|
|
819
839
|
type?: unknown;
|
|
820
|
-
|
|
840
|
+
code?: unknown;
|
|
841
|
+
message?: unknown;
|
|
842
|
+
error?: { type?: unknown; code?: unknown; message?: unknown };
|
|
843
|
+
last_error?: { type?: unknown; code?: unknown; message?: unknown };
|
|
844
|
+
response?: {
|
|
845
|
+
error?: { type?: unknown; code?: unknown; message?: unknown };
|
|
846
|
+
incomplete_details?: { code?: unknown; message?: unknown };
|
|
847
|
+
};
|
|
821
848
|
},
|
|
822
849
|
): void {
|
|
823
850
|
if (logCtx.terminalHttpStatus !== undefined) return;
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
const
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
851
|
+
const type = json.type;
|
|
852
|
+
if (type !== "response.failed" && type !== "response.incomplete" && type !== "error") return;
|
|
853
|
+
const responseError = json.response?.error;
|
|
854
|
+
const responseDetails = json.response?.incomplete_details;
|
|
855
|
+
const candidates = [json.error, json.last_error, responseError, responseDetails, json];
|
|
856
|
+
const policy = candidates.some(candidate => (
|
|
857
|
+
candidate?.code === null || typeof candidate?.code === "string"
|
|
858
|
+
) && isCyberPolicyCode(candidate.code as string | null | undefined))
|
|
859
|
+
|| candidates.some(candidate => (
|
|
860
|
+
typeof candidate?.message === "string"
|
|
861
|
+
&& candidate.message.trim().length > 0
|
|
862
|
+
&& isCyberPolicyMessage(candidate.message)
|
|
863
|
+
));
|
|
864
|
+
if (policy) {
|
|
831
865
|
logCtx.terminalErrorCode = CYBER_POLICY_ERROR_CODE;
|
|
832
|
-
|
|
833
|
-
|
|
866
|
+
logCtx.terminalHttpStatus = 400;
|
|
867
|
+
return;
|
|
834
868
|
}
|
|
869
|
+
if (type !== "response.failed" || !responseError || typeof responseError !== "object") return;
|
|
870
|
+
const responseCode = responseError.code === null || typeof responseError.code === "string"
|
|
871
|
+
? responseError.code
|
|
872
|
+
: undefined;
|
|
835
873
|
logCtx.terminalHttpStatus = httpStatusFromTerminalError({
|
|
836
|
-
type: typeof
|
|
837
|
-
code:
|
|
838
|
-
message: typeof
|
|
874
|
+
type: typeof responseError.type === "string" ? responseError.type : undefined,
|
|
875
|
+
code: responseCode,
|
|
876
|
+
message: typeof responseError.message === "string" ? responseError.message : undefined,
|
|
839
877
|
});
|
|
840
878
|
}
|
|
841
879
|
|
|
@@ -917,6 +955,7 @@ export function addFinalRequestLog(
|
|
|
917
955
|
logCtx.usage,
|
|
918
956
|
logCtx.usageLogInputTokens,
|
|
919
957
|
contextWindowForModel(logCtx.providerAdapter ?? logCtx.provider, logCtx.model),
|
|
958
|
+
logCtx.localTerminalReason !== undefined,
|
|
920
959
|
);
|
|
921
960
|
const attempts = logCtx.attempts?.map(attempt => ({
|
|
922
961
|
...attempt,
|
|
@@ -944,11 +983,15 @@ export function addFinalRequestLog(
|
|
|
944
983
|
...(logCtx.apiKeyId ? { apiKeyId: logCtx.apiKeyId } : {}),
|
|
945
984
|
...(logCtx.admissionKind ? { admissionKind: logCtx.admissionKind } : {}),
|
|
946
985
|
...(logCtx.inboundProtocol ? { inboundProtocol: logCtx.inboundProtocol } : {}),
|
|
986
|
+
...(logCtx.localTerminalReason
|
|
987
|
+
? { localTerminalReason: sanitizeLogMetadataString(logCtx.localTerminalReason) }
|
|
988
|
+
: {}),
|
|
947
989
|
...(isCodexUsageAccountLogLabel(logCtx.accountLogLabel)
|
|
948
990
|
? { accountLogLabel: logCtx.accountLogLabel }
|
|
949
991
|
: {}),
|
|
950
992
|
...(logCtx.conversationId ? { conversationId: logCtx.conversationId } : {}),
|
|
951
993
|
...(logCtx.requestedModel ? { requestedModel: logCtx.requestedModel } : {}),
|
|
994
|
+
...(logCtx.requestedAlias ? { requestedAlias: logCtx.requestedAlias } : {}),
|
|
952
995
|
...(shadowCallRewrittenFrom ? { shadowCallRewrittenFrom } : {}),
|
|
953
996
|
...(logCtx.requestedEffort ? { requestedEffort: logCtx.requestedEffort } : {}),
|
|
954
997
|
...(logCtx.effectiveEffort ? { effectiveEffort: logCtx.effectiveEffort } : {}),
|
|
@@ -1007,6 +1050,15 @@ export function filterRequestLogs(logs: RequestLogEntry[], params: URLSearchPara
|
|
|
1007
1050
|
if (conversationId) {
|
|
1008
1051
|
filtered = filtered.filter(entry => matchesLogConversationId(entry.conversationId, conversationId));
|
|
1009
1052
|
}
|
|
1053
|
+
// #2704: there was no `model` clause at all, so `?model=x` was ACCEPTED and silently
|
|
1054
|
+
// ignored -- worse than an error, because it yields wrong conclusions from output that
|
|
1055
|
+
// looks correct. Attempts are matched for the same reason `provider` matches them: a
|
|
1056
|
+
// request that failed over should be findable by the model that actually served it.
|
|
1057
|
+
const model = params.get("model")?.trim();
|
|
1058
|
+
if (model) {
|
|
1059
|
+
filtered = filtered.filter(entry => entry.model === model
|
|
1060
|
+
|| entry.attempts?.some(attempt => attempt.model === model));
|
|
1061
|
+
}
|
|
1010
1062
|
const status = params.get("status")?.trim().toLowerCase();
|
|
1011
1063
|
if (status) {
|
|
1012
1064
|
filtered = /^[1-5]xx$/.test(status)
|
|
@@ -1076,6 +1128,7 @@ function finalizedUsage(
|
|
|
1076
1128
|
usage: OcxUsage | undefined,
|
|
1077
1129
|
inputTokenEstimate: number | undefined,
|
|
1078
1130
|
contextWindow: number | undefined,
|
|
1131
|
+
locallyAnswered = false,
|
|
1079
1132
|
): FinalizedUsageResult {
|
|
1080
1133
|
// The ESTIMATE itself is capped at the model's context window (codex-router PR #140). The
|
|
1081
1134
|
// combined value below keeps its max(inputTokens, estimate) behavior — a provider-reported
|
|
@@ -1085,7 +1138,7 @@ function finalizedUsage(
|
|
|
1085
1138
|
&& inputTokenEstimate >= 0
|
|
1086
1139
|
? capEstimateAtContextWindow(inputTokenEstimate, contextWindow)
|
|
1087
1140
|
: undefined;
|
|
1088
|
-
const finalUsage = usageForFinalLog(adapter, usage);
|
|
1141
|
+
const finalUsage = usageForFinalLog(adapter, usage, locallyAnswered);
|
|
1089
1142
|
const usageFallback = !finalUsage && estimate !== undefined
|
|
1090
1143
|
? { inputTokens: estimate, outputTokens: 0, estimated: true }
|
|
1091
1144
|
: undefined;
|
|
@@ -1185,6 +1238,7 @@ export function finishRequestAttempt(
|
|
|
1185
1238
|
usage ?? attempt.usage,
|
|
1186
1239
|
attempt.inputTokenEstimate,
|
|
1187
1240
|
contextWindowForModel(attempt.adapter, attempt.model),
|
|
1241
|
+
attempt.locallyAnswered === true,
|
|
1188
1242
|
);
|
|
1189
1243
|
attempt.status = status;
|
|
1190
1244
|
attempt.durationMs = Math.max(0, durationMs);
|