@bitkyc08/opencodex 2.8.0 → 2.9.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 +24 -0
- package/bin/ocx.mjs +32 -4
- package/gui/dist/assets/index-CHwf3tTD.css +1 -0
- package/gui/dist/assets/index-u5eFOv2y.js +67 -0
- package/gui/dist/index.html +2 -2
- package/package.json +1 -1
- package/src/adapters/anthropic-image-normalize.ts +114 -20
- package/src/adapters/anthropic.ts +126 -10
- package/src/adapters/azure.ts +3 -3
- package/src/adapters/base.ts +7 -3
- package/src/adapters/cursor/discovery.ts +14 -4
- package/src/adapters/cursor/effort-map.ts +18 -6
- package/src/adapters/cursor/framing.ts +102 -27
- package/src/adapters/cursor/kv-store.ts +30 -3
- package/src/adapters/cursor/live-models.ts +22 -2
- package/src/adapters/cursor/live-transport.ts +245 -49
- package/src/adapters/cursor/mcp-manager.ts +105 -8
- package/src/adapters/cursor/native-exec-mcp.ts +5 -3
- package/src/adapters/cursor/native-exec-shell.ts +296 -14
- package/src/adapters/cursor/native-exec.ts +381 -33
- package/src/adapters/cursor/protobuf-events.ts +28 -1
- package/src/adapters/cursor/protobuf-request.ts +71 -39
- package/src/adapters/cursor/request-builder.ts +2 -2
- package/src/adapters/cursor/transport.ts +2 -0
- package/src/adapters/cursor.ts +13 -2
- package/src/adapters/google-antigravity-replay.ts +184 -17
- package/src/adapters/google.ts +58 -8
- package/src/adapters/kiro-thinking.ts +23 -9
- package/src/adapters/kiro-tools.ts +49 -18
- package/src/adapters/kiro.ts +377 -133
- package/src/adapters/mimo-free.ts +36 -4
- package/src/adapters/openai-chat.ts +143 -17
- package/src/adapters/openai-responses.ts +130 -14
- package/src/adapters/run-turn-queue.ts +7 -1
- package/src/bridge.ts +466 -69
- package/src/chat/outbound.ts +144 -38
- package/src/claude/inbound-debug.ts +53 -8
- package/src/claude/outbound.ts +224 -38
- package/src/cli/agent-driven.ts +34 -1
- package/src/cli/catalog-prewarm.ts +5 -2
- package/src/cli/claude-desktop.ts +2 -2
- package/src/cli/doctor.ts +12 -0
- package/src/cli/export-command.ts +187 -0
- package/src/cli/help.ts +11 -0
- package/src/cli/index.ts +13 -3
- package/src/cli/init.ts +129 -102
- package/src/cli/opencode.ts +36 -151
- package/src/cli/star-prompt.ts +13 -4
- package/src/cli/status-oauth.ts +12 -2
- package/src/clients/config-export.ts +377 -0
- package/src/codex/account-runtime-state.ts +19 -1
- package/src/codex/account-store.ts +162 -82
- package/src/codex/auth-api.ts +467 -159
- package/src/codex/auth-context.ts +15 -2
- package/src/codex/catalog/aggregation.ts +15 -0
- package/src/codex/catalog/effort.ts +16 -6
- package/src/codex/catalog/metadata.ts +6 -0
- package/src/codex/catalog/parsing.ts +3 -1
- package/src/codex/catalog/provider-fetch.ts +29 -0
- package/src/codex/catalog/sync.ts +64 -7
- package/src/codex/catalog.ts +2 -2
- package/src/codex/inject.ts +5 -5
- package/src/codex/main-account-cache.ts +8 -1
- package/src/codex/model-cache.ts +81 -2
- package/src/codex/pool-rotation.ts +39 -0
- package/src/codex/project-config-warnings.ts +12 -1
- package/src/codex/quota.ts +35 -3
- package/src/codex/routing.ts +46 -1
- package/src/codex/shim.ts +10 -4
- package/src/codex/subagent-model-fallback.ts +12 -0
- package/src/codex/websocket-registry.ts +27 -0
- package/src/combos/failover.ts +31 -1
- package/src/combos/request.ts +9 -0
- package/src/combos/resolve.ts +60 -4
- package/src/combos/types.ts +12 -0
- package/src/config.ts +510 -55
- package/src/github/star-state.ts +13 -1
- package/src/images/fulfill.ts +39 -1
- package/src/images/loop.ts +52 -12
- package/src/lib/admission.ts +83 -0
- package/src/lib/app-owned-memory-stores.ts +173 -0
- package/src/lib/app-owned-memory.ts +265 -0
- package/src/lib/bun-stream-caps.ts +31 -7
- package/src/lib/config-ownership.ts +33 -0
- package/src/lib/crash-guard.ts +65 -5
- package/src/lib/debug-log-buffer.ts +47 -6
- package/src/lib/destination-policy.ts +12 -1
- package/src/lib/errors.ts +3 -0
- package/src/lib/gcp-adc.ts +40 -2
- package/src/lib/injection-debug-log.ts +26 -2
- package/src/lib/provider-outbound.ts +3 -0
- package/src/lib/sidecar-tracker.ts +5 -2
- package/src/lib/sse-decoder.ts +257 -37
- package/src/lib/state-store-registrations.ts +109 -0
- package/src/lib/state-store-sweeper.ts +184 -0
- package/src/lib/translator-budget.ts +356 -0
- package/src/lib/windows-secret-acl.ts +33 -12
- package/src/lib/winsw.ts +14 -1
- package/src/oauth/anthropic-routing.ts +31 -7
- package/src/oauth/google-antigravity.ts +2 -1
- package/src/oauth/health.ts +30 -12
- package/src/oauth/index.ts +127 -23
- package/src/oauth/kiro-credentials.ts +72 -1
- package/src/oauth/kiro.ts +23 -4
- package/src/oauth/store.ts +165 -18
- package/src/oauth/token-guardian.ts +43 -4
- package/src/oauth/types.ts +2 -1
- package/src/providers/base-url-choices.ts +10 -0
- package/src/providers/derive.ts +12 -0
- package/src/providers/free-directory.ts +4 -1
- package/src/providers/key-failover.ts +12 -0
- package/src/providers/openai-sidecar.ts +4 -1
- package/src/providers/quota.ts +68 -7
- package/src/providers/registry.ts +279 -3
- package/src/responses/parser.ts +5 -1
- package/src/responses/spill-store.ts +394 -0
- package/src/responses/state.ts +520 -102
- package/src/router.ts +18 -1
- package/src/server/adapter-resolve.ts +20 -3
- package/src/server/auth-cors.ts +121 -28
- package/src/server/chat-completions.ts +57 -12
- package/src/server/claude-messages.ts +85 -13
- package/src/server/index.ts +242 -100
- package/src/server/lifecycle.ts +155 -25
- package/src/server/management/agent-settings-routes.ts +79 -36
- package/src/server/management/api-key-usage.ts +167 -0
- package/src/server/management/body.ts +35 -0
- package/src/server/management/combo-routes.ts +5 -1
- package/src/server/management/config-routes.ts +42 -12
- package/src/server/management/logs-usage-routes.ts +41 -21
- package/src/server/management/model-routes.ts +188 -54
- package/src/server/management/oauth-account-routes.ts +115 -26
- package/src/server/management/provider-routes.ts +56 -6
- package/src/server/management/shared.ts +16 -3
- package/src/server/management/sidebar-routes.ts +50 -1
- package/src/server/management/system-restart.ts +13 -6
- package/src/server/management/system-routes.ts +15 -3
- package/src/server/management/usage-summary-cache.ts +86 -0
- package/src/server/management-api.ts +39 -5
- package/src/server/management-auth.ts +65 -14
- package/src/server/port-reclaim.ts +58 -12
- package/src/server/ports.ts +2 -0
- package/src/server/proxy-liveness.ts +60 -14
- package/src/server/relay-eager.ts +20 -4
- package/src/server/relay.ts +548 -154
- package/src/server/request-decompress.ts +51 -4
- package/src/server/request-log.ts +134 -15
- package/src/server/responses/collaboration.ts +15 -4
- package/src/server/responses/compact.ts +3 -0
- package/src/server/responses/core.ts +241 -66
- package/src/server/responses-image-gen-repair.ts +19 -5
- package/src/server/responses-item-id-repair.ts +23 -5
- package/src/server/sse-payload-rewrite.ts +71 -12
- package/src/server/startup-health-cache.ts +14 -1
- package/src/server/system-env.ts +8 -1
- package/src/server/windows-tcp-drop.ts +15 -5
- package/src/server/ws-bridge.ts +25 -0
- package/src/service.ts +179 -13
- package/src/storage/policy-job.ts +93 -23
- package/src/storage/policy-worker.ts +6 -0
- package/src/storage/restore-job.ts +62 -16
- package/src/storage/restore-worker.ts +6 -0
- package/src/storage/storage-mutation-coordinator.ts +36 -6
- package/src/storage/worker-lifecycle.ts +181 -47
- package/src/tray/windows.ts +97 -25
- package/src/types.ts +39 -6
- package/src/update/index.ts +24 -5
- package/src/update/job.ts +598 -73
- package/src/usage/log.ts +115 -17
- package/src/usage/summary.ts +67 -2
- package/src/vision/index.ts +112 -22
- package/src/web-search/loop.ts +38 -6
- package/src/web-search/progress-stream.ts +14 -3
- package/gui/dist/assets/index-BDjpkcRN.js +0 -67
- package/gui/dist/assets/index-BHsKRFh9.css +0 -1
package/src/bridge.ts
CHANGED
|
@@ -4,6 +4,12 @@ import { encodeCompactionSummary } from "./responses/compaction";
|
|
|
4
4
|
import { encodeReasoningEnvelope, type ReasoningEnvelope } from "./responses/reasoning-envelope";
|
|
5
5
|
import { resolveStallTimeoutSec } from "./stall-timeout";
|
|
6
6
|
import { usageDisplayTotalTokens } from "./usage/totals";
|
|
7
|
+
import {
|
|
8
|
+
isTranslatorBudgetExceededError,
|
|
9
|
+
releaseTranslatedEvent,
|
|
10
|
+
type TranslatorBudget,
|
|
11
|
+
type TranslatorBufferKind,
|
|
12
|
+
} from "./lib/translator-budget";
|
|
7
13
|
|
|
8
14
|
function uuid(): string {
|
|
9
15
|
return crypto.randomUUID().replace(/-/g, "");
|
|
@@ -63,6 +69,23 @@ function responseError(status: number, type: string, message: string): OcxErrorP
|
|
|
63
69
|
return classifyError(status, type, message);
|
|
64
70
|
}
|
|
65
71
|
|
|
72
|
+
/**
|
|
73
|
+
* Whether assembled function-call arguments are usable JSON.
|
|
74
|
+
* An empty buffer is valid (no-arg tools send no deltas). Non-empty must parse —
|
|
75
|
+
* once fragments have been streamed to the client they cannot be repaired the way
|
|
76
|
+
* non-stream adapters degrade a bad payload to `{}`.
|
|
77
|
+
*/
|
|
78
|
+
function toolCallArgumentsUsable(args: string): boolean {
|
|
79
|
+
const trimmed = args.trim();
|
|
80
|
+
if (!trimmed) return true;
|
|
81
|
+
try {
|
|
82
|
+
JSON.parse(trimmed);
|
|
83
|
+
return true;
|
|
84
|
+
} catch {
|
|
85
|
+
return false;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
66
89
|
function adapterFailureFromEvent(event: Extract<AdapterEvent, { type: "error" }>): { httpStatus: number; error: OcxErrorPayload } {
|
|
67
90
|
if (event.status === undefined && event.errorType === undefined && event.code === undefined) {
|
|
68
91
|
return adapterFailureFromMessage(event.message);
|
|
@@ -133,6 +156,7 @@ export function bridgeToResponsesSSE(
|
|
|
133
156
|
* from this callback instead of re-parsing the bridged SSE.
|
|
134
157
|
*/
|
|
135
158
|
onUsage?: (usage: OcxUsage | undefined) => void;
|
|
159
|
+
translatorBudget?: TranslatorBudget;
|
|
136
160
|
/**
|
|
137
161
|
* Test seam for the wire/stall beat loop. Production omits this and uses the
|
|
138
162
|
* global timers; injecting here must not change scheduling semantics.
|
|
@@ -184,6 +208,43 @@ export function bridgeToResponsesSSE(
|
|
|
184
208
|
try { const o = JSON.parse(args); return o && typeof o === "object" ? o : {}; } catch { return {}; }
|
|
185
209
|
};
|
|
186
210
|
const encoder = new TextEncoder();
|
|
211
|
+
const budget = options?.translatorBudget;
|
|
212
|
+
const bytesOf = (value: string): number => Buffer.byteLength(value);
|
|
213
|
+
const appendString = (
|
|
214
|
+
previous: string,
|
|
215
|
+
previousBytes: number,
|
|
216
|
+
fragment: string,
|
|
217
|
+
kind: TranslatorBufferKind,
|
|
218
|
+
callId?: string,
|
|
219
|
+
): { value: string; bytes: number } => {
|
|
220
|
+
const fragmentBytes = bytesOf(fragment);
|
|
221
|
+
const nextBytes = previousBytes + fragmentBytes;
|
|
222
|
+
if (!budget) return { value: previous + fragment, bytes: nextBytes };
|
|
223
|
+
const scope = { kind, ...(callId ? { callId } : {}) };
|
|
224
|
+
const reservation = budget.reserveTransient(nextBytes, scope);
|
|
225
|
+
try {
|
|
226
|
+
const value = previous + fragment;
|
|
227
|
+
reservation.commitRetained();
|
|
228
|
+
budget.releaseRetained(previousBytes, scope);
|
|
229
|
+
return { value, bytes: nextBytes };
|
|
230
|
+
} catch (error) {
|
|
231
|
+
reservation.release();
|
|
232
|
+
throw error;
|
|
233
|
+
}
|
|
234
|
+
};
|
|
235
|
+
const replaceRetainedString = (previousBytes: number, next: string, kind: TranslatorBufferKind): number => {
|
|
236
|
+
const nextBytes = bytesOf(next);
|
|
237
|
+
if (!budget) return nextBytes;
|
|
238
|
+
const reservation = budget.reserveTransient(nextBytes, { kind });
|
|
239
|
+
reservation.commitRetained();
|
|
240
|
+
budget.releaseRetained(previousBytes, { kind });
|
|
241
|
+
return nextBytes;
|
|
242
|
+
};
|
|
243
|
+
const chargeValue = (value: unknown, kind: TranslatorBufferKind): number => {
|
|
244
|
+
const bytes = bytesOf(JSON.stringify(value));
|
|
245
|
+
budget?.chargeRetained(bytes, { kind });
|
|
246
|
+
return bytes;
|
|
247
|
+
};
|
|
187
248
|
const responseId = options?.responseId ?? `resp_${uuid()}`;
|
|
188
249
|
let seq = 0;
|
|
189
250
|
// Set once the client is gone (cancel) or an enqueue throws on a torn-down controller, so we
|
|
@@ -195,7 +256,7 @@ export function bridgeToResponsesSSE(
|
|
|
195
256
|
const reportTerminal = (status: ResponsesTerminalStatus) => {
|
|
196
257
|
if (terminalReported || clientCancelled || closed) return;
|
|
197
258
|
terminalReported = true;
|
|
198
|
-
options?.onTerminal?.(status);
|
|
259
|
+
try { options?.onTerminal?.(status); } catch { /* terminal metrics must not break the stream */ }
|
|
199
260
|
};
|
|
200
261
|
// RC3 keep-alive: Codex's idle timer is timeout(idle_timeout, stream.next()) over an
|
|
201
262
|
// eventsource_stream; ANY received event re-arms it, while an unknown type is ignored
|
|
@@ -209,22 +270,43 @@ export function bridgeToResponsesSSE(
|
|
|
209
270
|
let emittedFrames = 0;
|
|
210
271
|
let gated = false;
|
|
211
272
|
let stepping = false;
|
|
212
|
-
|
|
273
|
+
let terminateForTranslatorOverflow: ((error: unknown) => void) | undefined;
|
|
274
|
+
const emit = (name: string, data: Record<string, unknown>) => {
|
|
213
275
|
if (closed) return;
|
|
214
276
|
wireActivity = true;
|
|
215
277
|
try {
|
|
216
|
-
|
|
278
|
+
const frameText = sseEvent(name, { type: name, sequence_number: seq++, ...data });
|
|
279
|
+
const frameBytes = bytesOf(frameText);
|
|
280
|
+
const reservation = budget?.reserveTransient(frameBytes, { kind: "live_transient" });
|
|
281
|
+
const frame = encoder.encode(frameText);
|
|
282
|
+
reservation?.commitRetained();
|
|
283
|
+
controller.enqueue(frame);
|
|
284
|
+
budget?.releaseRetained(frameBytes, { kind: "live_transient" });
|
|
217
285
|
emittedFrames++;
|
|
218
|
-
} catch {
|
|
286
|
+
} catch (error) {
|
|
287
|
+
if (isTranslatorBudgetExceededError(error)) {
|
|
288
|
+
terminateForTranslatorOverflow?.(error);
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
219
291
|
closed = true;
|
|
220
292
|
}
|
|
221
293
|
};
|
|
222
294
|
const emitDone = () => {
|
|
223
295
|
if (closed) return;
|
|
224
296
|
try {
|
|
225
|
-
|
|
297
|
+
const done = "data: [DONE]\n\n";
|
|
298
|
+
const doneBytes = bytesOf(done);
|
|
299
|
+
const reservation = budget?.reserveTransient(doneBytes, { kind: "live_transient" });
|
|
300
|
+
const frame = encoder.encode(done);
|
|
301
|
+
reservation?.commitRetained();
|
|
302
|
+
controller.enqueue(frame);
|
|
303
|
+
budget?.releaseRetained(doneBytes, { kind: "live_transient" });
|
|
226
304
|
emittedFrames++;
|
|
227
|
-
} catch {
|
|
305
|
+
} catch (error) {
|
|
306
|
+
if (isTranslatorBudgetExceededError(error)) {
|
|
307
|
+
terminateForTranslatorOverflow?.(error);
|
|
308
|
+
return;
|
|
309
|
+
}
|
|
228
310
|
closed = true;
|
|
229
311
|
}
|
|
230
312
|
};
|
|
@@ -232,6 +314,13 @@ export function bridgeToResponsesSSE(
|
|
|
232
314
|
const createdAt = Math.floor(Date.now() / 1000);
|
|
233
315
|
let outputIndex = 0;
|
|
234
316
|
const finishedItems: OutputItem[] = [];
|
|
317
|
+
const retainFinishedItem = (item: OutputItem, replacedBytes = 0, kind: TranslatorBufferKind = "retained_collectors") => {
|
|
318
|
+
const itemBytes = bytesOf(JSON.stringify(item));
|
|
319
|
+
const reservation = budget?.reserveTransient(itemBytes, { kind });
|
|
320
|
+
finishedItems.push(item);
|
|
321
|
+
reservation?.commitRetained();
|
|
322
|
+
if (replacedBytes > 0) budget?.releaseRetained(replacedBytes, { kind });
|
|
323
|
+
};
|
|
235
324
|
|
|
236
325
|
const responseSnapshot = (status: string, output: OutputItem[], endTurn?: boolean) => ({
|
|
237
326
|
id: responseId, object: "response", created_at: createdAt,
|
|
@@ -244,37 +333,48 @@ export function bridgeToResponsesSSE(
|
|
|
244
333
|
const stallSec = resolveStallTimeoutSec(options?.stallTimeoutSec);
|
|
245
334
|
const maxStallTicks = Math.ceil((stallSec * 1000) / heartbeatMs);
|
|
246
335
|
|
|
247
|
-
let currentMsg: { itemId: string; outputIndex: number; text: string; phase?: OcxMessagePhase } | null = null;
|
|
248
|
-
let currentReasoning: { itemId: string; outputIndex: number; text: string } | null = null;
|
|
249
|
-
let currentRawReasoning: { itemId: string; outputIndex: number; text: string } | null = null;
|
|
336
|
+
let currentMsg: { itemId: string; outputIndex: number; text: string; textBytes: number; phase?: OcxMessagePhase } | null = null;
|
|
337
|
+
let currentReasoning: { itemId: string; outputIndex: number; text: string; textBytes: number } | null = null;
|
|
338
|
+
let currentRawReasoning: { itemId: string; outputIndex: number; text: string; textBytes: number } | null = null;
|
|
250
339
|
// Anthropic extended-thinking round-trip state: the signature signs the CURRENT thinking
|
|
251
340
|
// block; redacted blocks are opaque payloads replayed verbatim. Attached to the reasoning
|
|
252
341
|
// item as an ocxr1 encrypted_content envelope on close. hiddenThinkingText collects the
|
|
253
342
|
// suppressed text under hideThinkingSummary so the signed text still round-trips.
|
|
254
343
|
let pendingSignature: string | undefined;
|
|
344
|
+
let pendingSignatureBytes = 0;
|
|
255
345
|
let pendingRedacted: string[] = [];
|
|
256
346
|
let hiddenThinkingText = "";
|
|
347
|
+
let hiddenThinkingBytes = 0;
|
|
257
348
|
const takeReasoningEnvelope = (hiddenText?: string): string | undefined => {
|
|
258
349
|
if (!pendingSignature && pendingRedacted.length === 0) return undefined;
|
|
259
350
|
const envelope: ReasoningEnvelope = {};
|
|
260
351
|
if (pendingSignature) envelope.sig = pendingSignature;
|
|
261
352
|
if (pendingRedacted.length > 0) envelope.red = pendingRedacted;
|
|
262
353
|
if (hiddenText) envelope.txt = hiddenText;
|
|
354
|
+
const previousBytes = pendingSignatureBytes
|
|
355
|
+
+ pendingRedacted.reduce((sum, value) => sum + bytesOf(value), 0)
|
|
356
|
+
+ (hiddenText ? hiddenThinkingBytes : 0);
|
|
357
|
+
const encoded = encodeReasoningEnvelope(envelope);
|
|
358
|
+
const reservation = budget?.reserveTransient(bytesOf(encoded), { kind: "reasoning" });
|
|
263
359
|
pendingSignature = undefined;
|
|
360
|
+
pendingSignatureBytes = 0;
|
|
264
361
|
pendingRedacted = [];
|
|
265
|
-
|
|
362
|
+
reservation?.commitRetained();
|
|
363
|
+
budget?.releaseRetained(previousBytes, { kind: "reasoning" });
|
|
364
|
+
return encoded;
|
|
266
365
|
};
|
|
267
366
|
// hideThinkingSummary path: no visible reasoning item exists, but a signed thinking block
|
|
268
367
|
// must still round-trip — emit an envelope-only reasoning item (empty summary, no text leak).
|
|
269
368
|
const flushHiddenReasoningEnvelope = () => {
|
|
270
369
|
const encrypted = takeReasoningEnvelope(hiddenThinkingText || undefined);
|
|
271
370
|
hiddenThinkingText = "";
|
|
371
|
+
hiddenThinkingBytes = 0;
|
|
272
372
|
if (!encrypted) return;
|
|
273
373
|
const itemId = `rs_${uuid()}`;
|
|
274
374
|
const item = { type: "reasoning", id: itemId, summary: [] as never[], encrypted_content: encrypted };
|
|
275
375
|
emit("response.output_item.added", { output_index: outputIndex, item });
|
|
276
376
|
emit("response.output_item.done", { output_index: outputIndex, item });
|
|
277
|
-
|
|
377
|
+
retainFinishedItem(item as OutputItem, bytesOf(encrypted), "reasoning");
|
|
278
378
|
outputIndex++;
|
|
279
379
|
};
|
|
280
380
|
// hideThinkingSummary for RAW reasoning (openai-chat reasoning_content, kiro tags): no
|
|
@@ -283,21 +383,28 @@ export function bridgeToResponsesSSE(
|
|
|
283
383
|
// preserveReasoningContentModels replay (GLM interleaved thinking) keeps working. Direct
|
|
284
384
|
// encodeReasoningEnvelope: takeReasoningEnvelope's sig/red guard would drop txt-only.
|
|
285
385
|
let hiddenRawReasoningText = "";
|
|
386
|
+
let hiddenRawReasoningBytes = 0;
|
|
286
387
|
const flushHiddenRawReasoning = () => {
|
|
287
388
|
if (!hiddenRawReasoningText) return;
|
|
389
|
+
const previousBytes = hiddenRawReasoningBytes;
|
|
288
390
|
const encrypted = encodeReasoningEnvelope({ txt: hiddenRawReasoningText });
|
|
391
|
+
const reservation = budget?.reserveTransient(bytesOf(encrypted), { kind: "reasoning" });
|
|
289
392
|
hiddenRawReasoningText = "";
|
|
393
|
+
hiddenRawReasoningBytes = 0;
|
|
394
|
+
reservation?.commitRetained();
|
|
395
|
+
budget?.releaseRetained(previousBytes, { kind: "reasoning" });
|
|
290
396
|
const itemId = `rs_${uuid()}`;
|
|
291
397
|
const item = { type: "reasoning", id: itemId, summary: [] as never[], encrypted_content: encrypted };
|
|
292
398
|
emit("response.output_item.added", { output_index: outputIndex, item });
|
|
293
399
|
emit("response.output_item.done", { output_index: outputIndex, item });
|
|
294
|
-
|
|
400
|
+
retainFinishedItem(item as OutputItem, bytesOf(encrypted), "reasoning");
|
|
295
401
|
outputIndex++;
|
|
296
402
|
};
|
|
297
403
|
// Full assistant text of a compaction turn (across message boundaries) — becomes the
|
|
298
404
|
// synthetic compaction item's payload on done.
|
|
299
405
|
let compactionText = "";
|
|
300
|
-
let
|
|
406
|
+
let compactionTextBytes = 0;
|
|
407
|
+
let currentToolCall: { itemId: string; outputIndex: number; callId: string; name: string; args: string; argsBytes: number; namespace?: string; freeform?: boolean; toolSearch?: boolean; inputEmitted?: string } | null = null;
|
|
301
408
|
// Open native web-search cell (between begin and end). Holds the output index allocated on
|
|
302
409
|
// begin so the matching done reuses it; closed as `failed` if the stream terminates early.
|
|
303
410
|
let currentWebSearch: { itemId: string; eventId: string; outputIndex: number } | null = null;
|
|
@@ -310,7 +417,12 @@ export function bridgeToResponsesSSE(
|
|
|
310
417
|
const anns = pendingWebSources.map(s => ({
|
|
311
418
|
type: "url_citation", url: s.url, ...(s.title ? { title: s.title } : {}), start_index: 0, end_index: 0,
|
|
312
419
|
}));
|
|
420
|
+
const sourceBytes = pendingWebSources.reduce((sum, source) => sum + bytesOf(JSON.stringify(source)), 0);
|
|
421
|
+
const annotationBytes = bytesOf(JSON.stringify(anns));
|
|
422
|
+
const reservation = budget?.reserveTransient(annotationBytes, { kind: "retained_collectors" });
|
|
313
423
|
pendingWebSources = [];
|
|
424
|
+
reservation?.commitRetained();
|
|
425
|
+
budget?.releaseRetained(sourceBytes, { kind: "tool_search_sources" });
|
|
314
426
|
return anns;
|
|
315
427
|
};
|
|
316
428
|
|
|
@@ -337,7 +449,7 @@ export function bridgeToResponsesSSE(
|
|
|
337
449
|
...(phase ? { phase } : {}),
|
|
338
450
|
};
|
|
339
451
|
emit("response.output_item.done", { output_index: currentMsg.outputIndex, item });
|
|
340
|
-
|
|
452
|
+
retainFinishedItem(item as OutputItem, currentMsg.textBytes + bytesOf(JSON.stringify(annotations)));
|
|
341
453
|
outputIndex++;
|
|
342
454
|
currentMsg = null;
|
|
343
455
|
};
|
|
@@ -358,7 +470,7 @@ export function bridgeToResponsesSSE(
|
|
|
358
470
|
...(encrypted ? { encrypted_content: encrypted } : {}),
|
|
359
471
|
};
|
|
360
472
|
emit("response.output_item.done", { output_index: currentReasoning.outputIndex, item });
|
|
361
|
-
|
|
473
|
+
retainFinishedItem(item as OutputItem, currentReasoning.textBytes + bytesOf(encrypted ?? ""), "reasoning");
|
|
362
474
|
outputIndex++;
|
|
363
475
|
currentReasoning = null;
|
|
364
476
|
};
|
|
@@ -370,7 +482,7 @@ export function bridgeToResponsesSSE(
|
|
|
370
482
|
content: [{ type: "reasoning_text", text: currentRawReasoning.text }],
|
|
371
483
|
};
|
|
372
484
|
emit("response.output_item.done", { output_index: currentRawReasoning.outputIndex, item });
|
|
373
|
-
|
|
485
|
+
retainFinishedItem(item as OutputItem, currentRawReasoning.textBytes, "reasoning");
|
|
374
486
|
outputIndex++;
|
|
375
487
|
currentRawReasoning = null;
|
|
376
488
|
};
|
|
@@ -412,11 +524,52 @@ export function bridgeToResponsesSSE(
|
|
|
412
524
|
...(currentToolCall.namespace ? { namespace: currentToolCall.namespace } : {}),
|
|
413
525
|
};
|
|
414
526
|
emit("response.output_item.done", { output_index: currentToolCall.outputIndex, item });
|
|
415
|
-
|
|
527
|
+
retainFinishedItem(item as OutputItem);
|
|
528
|
+
budget?.closeCall(currentToolCall.callId);
|
|
416
529
|
outputIndex++;
|
|
417
530
|
currentToolCall = null;
|
|
418
531
|
};
|
|
419
532
|
|
|
533
|
+
// Terminal-error / incomplete path for an open tool call (#765 remainder).
|
|
534
|
+
// Closing via closeCurrentToolCall() would emit function_call_arguments.done and
|
|
535
|
+
// status:"completed" BEFORE response.failed — the client still sees an issued call.
|
|
536
|
+
// Cancel instead: no *.done argument frames, status:"incomplete" (same pattern as an
|
|
537
|
+
// in-flight web_search_call closing as "failed"). Args still serialize as "{}" when
|
|
538
|
+
// empty so echoed items cannot poison the next turn with JSON.parse("").
|
|
539
|
+
const failCurrentToolCall = () => {
|
|
540
|
+
if (!currentToolCall) return;
|
|
541
|
+
const argsStr = currentToolCall.args || "{}";
|
|
542
|
+
const item = currentToolCall.toolSearch
|
|
543
|
+
? {
|
|
544
|
+
type: "tool_search_call", id: currentToolCall.itemId,
|
|
545
|
+
call_id: currentToolCall.callId, execution: "client",
|
|
546
|
+
arguments: parseArgsObj(currentToolCall.args), status: "incomplete",
|
|
547
|
+
}
|
|
548
|
+
: currentToolCall.freeform
|
|
549
|
+
? {
|
|
550
|
+
type: "custom_tool_call", id: currentToolCall.itemId,
|
|
551
|
+
call_id: currentToolCall.callId, name: currentToolCall.name,
|
|
552
|
+
input: freeformInput(currentToolCall.args), status: "incomplete",
|
|
553
|
+
}
|
|
554
|
+
: {
|
|
555
|
+
type: "function_call", id: currentToolCall.itemId,
|
|
556
|
+
call_id: currentToolCall.callId, name: currentToolCall.name,
|
|
557
|
+
arguments: argsStr, status: "incomplete",
|
|
558
|
+
...(currentToolCall.namespace ? { namespace: currentToolCall.namespace } : {}),
|
|
559
|
+
};
|
|
560
|
+
emit("response.output_item.done", { output_index: currentToolCall.outputIndex, item });
|
|
561
|
+
retainFinishedItem(item as OutputItem);
|
|
562
|
+
budget?.closeCall(currentToolCall.callId);
|
|
563
|
+
outputIndex++;
|
|
564
|
+
currentToolCall = null;
|
|
565
|
+
};
|
|
566
|
+
|
|
567
|
+
const abortCurrentToolCallForTranslatorOverflow = () => {
|
|
568
|
+
if (!currentToolCall) return;
|
|
569
|
+
budget?.closeCall(currentToolCall.callId);
|
|
570
|
+
currentToolCall = null;
|
|
571
|
+
};
|
|
572
|
+
|
|
420
573
|
// Finalize an open web-search cell. `status` is "completed" on a normal end, or "failed" when
|
|
421
574
|
// the stream terminates (error/incomplete) while a search was still in flight, so Codex never
|
|
422
575
|
// leaves a "Searching the web" spinner spinning forever.
|
|
@@ -430,15 +583,19 @@ export function bridgeToResponsesSSE(
|
|
|
430
583
|
...(sources && sources.length > 0 ? { sources } : {}),
|
|
431
584
|
};
|
|
432
585
|
emit("response.output_item.done", { output_index: currentWebSearch.outputIndex, item });
|
|
433
|
-
|
|
586
|
+
retainFinishedItem(item as OutputItem);
|
|
434
587
|
outputIndex++;
|
|
435
588
|
currentWebSearch = null;
|
|
436
589
|
};
|
|
437
590
|
|
|
438
591
|
// RC1: guarantee the Responses stream always ends with exactly one terminal event. Set true
|
|
439
592
|
// when a done/error/catch terminal is emitted; if the adapter generator returns without one
|
|
440
|
-
// we synthesize
|
|
593
|
+
// we synthesize a terminal below, so Codex never hits the parser's
|
|
441
594
|
// "stream closed before response.completed" (responses.rs) -> ApiError::Stream.
|
|
595
|
+
// That synthesized terminal is response.incomplete with reason "adapter_eof", NOT
|
|
596
|
+
// response.completed: a generator that returns without a terminal event is a truncated
|
|
597
|
+
// stream, and reporting it as a clean finish is the failure mode this whole path exists
|
|
598
|
+
// to avoid. The comment said "completed" long after the code stopped doing that.
|
|
442
599
|
let terminated = false;
|
|
443
600
|
let firstOutputReported = false;
|
|
444
601
|
const reportFirstOutput = (event: AdapterEvent): void => {
|
|
@@ -482,6 +639,52 @@ export function bridgeToResponsesSSE(
|
|
|
482
639
|
}
|
|
483
640
|
finishReturn();
|
|
484
641
|
};
|
|
642
|
+
let upstreamCancelled = false;
|
|
643
|
+
const cancelUpstreamOnce = () => {
|
|
644
|
+
if (upstreamCancelled) return;
|
|
645
|
+
upstreamCancelled = true;
|
|
646
|
+
try { onCancel?.(); } catch { /* cancellation must not strand the client stream */ }
|
|
647
|
+
returnIterator();
|
|
648
|
+
};
|
|
649
|
+
let handlingTranslatorOverflow = false;
|
|
650
|
+
terminateForTranslatorOverflow = _error => {
|
|
651
|
+
if (handlingTranslatorOverflow || terminated || clientCancelled || closed) return;
|
|
652
|
+
handlingTranslatorOverflow = true;
|
|
653
|
+
abortCurrentToolCallForTranslatorOverflow();
|
|
654
|
+
currentWebSearch = null;
|
|
655
|
+
const failure = adapterFailureFromEvent({
|
|
656
|
+
type: "error",
|
|
657
|
+
status: 502,
|
|
658
|
+
errorType: "upstream_error",
|
|
659
|
+
code: "translation_buffer_limit",
|
|
660
|
+
message: "upstream translation buffer exceeded the safe limit",
|
|
661
|
+
}).error;
|
|
662
|
+
const failedFrame = sseEvent("response.failed", {
|
|
663
|
+
type: "response.failed",
|
|
664
|
+
sequence_number: seq++,
|
|
665
|
+
response: {
|
|
666
|
+
...responseSnapshot("failed", finishedItems),
|
|
667
|
+
error: failure,
|
|
668
|
+
last_error: failure,
|
|
669
|
+
},
|
|
670
|
+
});
|
|
671
|
+
try {
|
|
672
|
+
controller.enqueue(encoder.encode(failedFrame));
|
|
673
|
+
controller.enqueue(encoder.encode("data: [DONE]\n\n"));
|
|
674
|
+
emittedFrames += 2;
|
|
675
|
+
} catch {
|
|
676
|
+
/* client already tore down the stream */
|
|
677
|
+
}
|
|
678
|
+
reportTerminal("failed");
|
|
679
|
+
terminated = true;
|
|
680
|
+
cancelUpstreamOnce();
|
|
681
|
+
if (beat !== undefined) clearBeatInterval(beat);
|
|
682
|
+
beat = undefined;
|
|
683
|
+
try { controller.close(); } catch { /* already closed */ }
|
|
684
|
+
closed = true;
|
|
685
|
+
gated = true;
|
|
686
|
+
stepping = false;
|
|
687
|
+
};
|
|
485
688
|
const step = async () => {
|
|
486
689
|
if (stepping || closed) return;
|
|
487
690
|
stepping = true;
|
|
@@ -505,7 +708,15 @@ export function bridgeToResponsesSSE(
|
|
|
505
708
|
// expansion (rememberResponseState stores input + output). Codex ignores extra items but
|
|
506
709
|
// its compaction UI renders nothing mid-turn, so nothing is lost visually.
|
|
507
710
|
if (options?.compaction) {
|
|
508
|
-
if (event.type === "text_delta") {
|
|
711
|
+
if (event.type === "text_delta") {
|
|
712
|
+
({ value: compactionText, bytes: compactionTextBytes } = appendString(
|
|
713
|
+
compactionText,
|
|
714
|
+
compactionTextBytes,
|
|
715
|
+
event.text,
|
|
716
|
+
"retained_collectors",
|
|
717
|
+
));
|
|
718
|
+
continue;
|
|
719
|
+
}
|
|
509
720
|
if (event.type !== "done" && event.type !== "incomplete" && event.type !== "error") continue;
|
|
510
721
|
}
|
|
511
722
|
switch (event.type) {
|
|
@@ -542,9 +753,14 @@ export function bridgeToResponsesSSE(
|
|
|
542
753
|
item_id: itemId, output_index: outputIndex, content_index: 0,
|
|
543
754
|
part: { type: "output_text", text: "", annotations: [] },
|
|
544
755
|
});
|
|
545
|
-
currentMsg = { itemId, outputIndex, text: "", ...(event.phase ? { phase: event.phase } : {}) };
|
|
756
|
+
currentMsg = { itemId, outputIndex, text: "", textBytes: 0, ...(event.phase ? { phase: event.phase } : {}) };
|
|
546
757
|
}
|
|
547
|
-
currentMsg.text
|
|
758
|
+
({ value: currentMsg.text, bytes: currentMsg.textBytes } = appendString(
|
|
759
|
+
currentMsg.text,
|
|
760
|
+
currentMsg.textBytes,
|
|
761
|
+
event.text,
|
|
762
|
+
"retained_collectors",
|
|
763
|
+
));
|
|
548
764
|
emit("response.output_text.delta", {
|
|
549
765
|
item_id: currentMsg.itemId, output_index: currentMsg.outputIndex,
|
|
550
766
|
content_index: 0, delta: event.text,
|
|
@@ -552,7 +768,15 @@ export function bridgeToResponsesSSE(
|
|
|
552
768
|
break;
|
|
553
769
|
}
|
|
554
770
|
case "thinking_delta": {
|
|
555
|
-
if (options?.hideThinkingSummary) {
|
|
771
|
+
if (options?.hideThinkingSummary) {
|
|
772
|
+
({ value: hiddenThinkingText, bytes: hiddenThinkingBytes } = appendString(
|
|
773
|
+
hiddenThinkingText,
|
|
774
|
+
hiddenThinkingBytes,
|
|
775
|
+
event.thinking,
|
|
776
|
+
"reasoning",
|
|
777
|
+
));
|
|
778
|
+
break;
|
|
779
|
+
}
|
|
556
780
|
if (currentMsg) closeCurrentMessage("commentary");
|
|
557
781
|
if (currentRawReasoning) closeCurrentRawReasoning();
|
|
558
782
|
flushHiddenRawReasoning();
|
|
@@ -565,9 +789,14 @@ export function bridgeToResponsesSSE(
|
|
|
565
789
|
item_id: itemId, output_index: outputIndex, summary_index: 0,
|
|
566
790
|
part: { type: "summary_text", text: "" },
|
|
567
791
|
});
|
|
568
|
-
currentReasoning = { itemId, outputIndex, text: "" };
|
|
792
|
+
currentReasoning = { itemId, outputIndex, text: "", textBytes: 0 };
|
|
569
793
|
}
|
|
570
|
-
currentReasoning.text
|
|
794
|
+
({ value: currentReasoning.text, bytes: currentReasoning.textBytes } = appendString(
|
|
795
|
+
currentReasoning.text,
|
|
796
|
+
currentReasoning.textBytes,
|
|
797
|
+
event.thinking,
|
|
798
|
+
"reasoning",
|
|
799
|
+
));
|
|
571
800
|
emit("response.reasoning_summary_text.delta", {
|
|
572
801
|
item_id: currentReasoning.itemId, output_index: currentReasoning.outputIndex,
|
|
573
802
|
summary_index: 0, delta: event.thinking,
|
|
@@ -575,6 +804,7 @@ export function bridgeToResponsesSSE(
|
|
|
575
804
|
break;
|
|
576
805
|
}
|
|
577
806
|
case "thinking_signature": {
|
|
807
|
+
pendingSignatureBytes = replaceRetainedString(pendingSignatureBytes, event.signature, "reasoning");
|
|
578
808
|
pendingSignature = event.signature;
|
|
579
809
|
// Signature arrives at the end of the thinking block. With a visible reasoning item
|
|
580
810
|
// open, closeCurrentReasoning attaches the envelope; hidden/suppressed blocks flush
|
|
@@ -583,11 +813,20 @@ export function bridgeToResponsesSSE(
|
|
|
583
813
|
break;
|
|
584
814
|
}
|
|
585
815
|
case "redacted_thinking": {
|
|
816
|
+
budget?.chargeRetained(bytesOf(event.data), { kind: "reasoning" });
|
|
586
817
|
pendingRedacted.push(event.data);
|
|
587
818
|
break;
|
|
588
819
|
}
|
|
589
820
|
case "reasoning_raw_delta": {
|
|
590
|
-
if (options?.hideThinkingSummary) {
|
|
821
|
+
if (options?.hideThinkingSummary) {
|
|
822
|
+
({ value: hiddenRawReasoningText, bytes: hiddenRawReasoningBytes } = appendString(
|
|
823
|
+
hiddenRawReasoningText,
|
|
824
|
+
hiddenRawReasoningBytes,
|
|
825
|
+
event.text,
|
|
826
|
+
"reasoning",
|
|
827
|
+
));
|
|
828
|
+
break;
|
|
829
|
+
}
|
|
591
830
|
if (currentMsg) closeCurrentMessage("commentary");
|
|
592
831
|
if (currentReasoning) closeCurrentReasoning();
|
|
593
832
|
if (currentToolCall) closeCurrentToolCall();
|
|
@@ -595,9 +834,14 @@ export function bridgeToResponsesSSE(
|
|
|
595
834
|
const itemId = `rs_${uuid()}`;
|
|
596
835
|
const item = { type: "reasoning", id: itemId, summary: [] as never[], content: [] as { type: string; text: string }[] };
|
|
597
836
|
emit("response.output_item.added", { output_index: outputIndex, item });
|
|
598
|
-
currentRawReasoning = { itemId, outputIndex, text: "" };
|
|
837
|
+
currentRawReasoning = { itemId, outputIndex, text: "", textBytes: 0 };
|
|
599
838
|
}
|
|
600
|
-
currentRawReasoning.text
|
|
839
|
+
({ value: currentRawReasoning.text, bytes: currentRawReasoning.textBytes } = appendString(
|
|
840
|
+
currentRawReasoning.text,
|
|
841
|
+
currentRawReasoning.textBytes,
|
|
842
|
+
event.text,
|
|
843
|
+
"reasoning",
|
|
844
|
+
));
|
|
601
845
|
emit("response.reasoning_text.delta", {
|
|
602
846
|
item_id: currentRawReasoning.itemId, output_index: currentRawReasoning.outputIndex,
|
|
603
847
|
content_index: 0, delta: event.text,
|
|
@@ -622,12 +866,19 @@ export function bridgeToResponsesSSE(
|
|
|
622
866
|
? { type: "custom_tool_call", id: itemId, call_id: event.id, name: realName, input: "", status: "in_progress" }
|
|
623
867
|
: { type: "function_call", id: itemId, call_id: event.id, name: realName, arguments: "", status: "in_progress", ...(ns ? { namespace: ns } : {}) };
|
|
624
868
|
emit("response.output_item.added", { output_index: outputIndex, item });
|
|
625
|
-
currentToolCall = { itemId, outputIndex, callId: event.id, name: realName, args: "", namespace: ns, freeform, toolSearch };
|
|
869
|
+
currentToolCall = { itemId, outputIndex, callId: event.id, name: realName, args: "", argsBytes: 0, namespace: ns, freeform, toolSearch };
|
|
870
|
+
budget?.openCall(event.id);
|
|
626
871
|
break;
|
|
627
872
|
}
|
|
628
873
|
case "tool_call_delta": {
|
|
629
874
|
if (currentToolCall) {
|
|
630
|
-
currentToolCall.args
|
|
875
|
+
({ value: currentToolCall.args, bytes: currentToolCall.argsBytes } = appendString(
|
|
876
|
+
currentToolCall.args,
|
|
877
|
+
currentToolCall.argsBytes,
|
|
878
|
+
event.arguments,
|
|
879
|
+
"tool_args",
|
|
880
|
+
currentToolCall.callId,
|
|
881
|
+
));
|
|
631
882
|
if (!currentToolCall.freeform && !currentToolCall.toolSearch) {
|
|
632
883
|
emit("response.function_call_arguments.delta", {
|
|
633
884
|
item_id: currentToolCall.itemId, output_index: currentToolCall.outputIndex,
|
|
@@ -653,6 +904,32 @@ export function bridgeToResponsesSSE(
|
|
|
653
904
|
break;
|
|
654
905
|
}
|
|
655
906
|
case "tool_call_end": {
|
|
907
|
+
// Fragments already streamed cannot be repaired. Refuse to complete a function call
|
|
908
|
+
// whose assembled arguments do not parse — cancel the item and fail the turn so the
|
|
909
|
+
// client never sees status:"completed" for unusable args (#765 stream remainder).
|
|
910
|
+
if (
|
|
911
|
+
currentToolCall
|
|
912
|
+
&& !currentToolCall.freeform
|
|
913
|
+
&& !currentToolCall.toolSearch
|
|
914
|
+
&& !toolCallArgumentsUsable(currentToolCall.args)
|
|
915
|
+
) {
|
|
916
|
+
failCurrentToolCall();
|
|
917
|
+
const failure = responseError(
|
|
918
|
+
502,
|
|
919
|
+
"upstream_error",
|
|
920
|
+
"upstream stream produced malformed tool call arguments",
|
|
921
|
+
);
|
|
922
|
+
emit("response.failed", {
|
|
923
|
+
response: {
|
|
924
|
+
...responseSnapshot("failed", finishedItems),
|
|
925
|
+
error: failure,
|
|
926
|
+
last_error: failure,
|
|
927
|
+
},
|
|
928
|
+
});
|
|
929
|
+
reportTerminal("failed");
|
|
930
|
+
terminalEvent = true;
|
|
931
|
+
break;
|
|
932
|
+
}
|
|
656
933
|
closeCurrentToolCall();
|
|
657
934
|
break;
|
|
658
935
|
}
|
|
@@ -691,7 +968,11 @@ export function bridgeToResponsesSSE(
|
|
|
691
968
|
if (event.sources) {
|
|
692
969
|
const seen = new Set(pendingWebSources.map(s => s.url));
|
|
693
970
|
for (const s of event.sources) {
|
|
694
|
-
if (!seen.has(s.url)) {
|
|
971
|
+
if (!seen.has(s.url)) {
|
|
972
|
+
seen.add(s.url);
|
|
973
|
+
chargeValue(s, "tool_search_sources");
|
|
974
|
+
pendingWebSources.push(s);
|
|
975
|
+
}
|
|
695
976
|
}
|
|
696
977
|
}
|
|
697
978
|
break;
|
|
@@ -713,7 +994,7 @@ export function bridgeToResponsesSSE(
|
|
|
713
994
|
encrypted_content: encodeCompactionSummary(compactionText),
|
|
714
995
|
};
|
|
715
996
|
emit("response.output_item.done", { output_index: outputIndex, item });
|
|
716
|
-
|
|
997
|
+
retainFinishedItem(item as OutputItem, compactionTextBytes);
|
|
717
998
|
outputIndex++;
|
|
718
999
|
}
|
|
719
1000
|
if (event.stopReason === "max_tokens" || event.stopReason === "content_filter") {
|
|
@@ -749,7 +1030,7 @@ export function bridgeToResponsesSSE(
|
|
|
749
1030
|
if (currentReasoning) closeCurrentReasoning();
|
|
750
1031
|
if (currentRawReasoning) closeCurrentRawReasoning();
|
|
751
1032
|
flushHiddenRawReasoning();
|
|
752
|
-
if (currentToolCall)
|
|
1033
|
+
if (currentToolCall) failCurrentToolCall();
|
|
753
1034
|
if (currentWebSearch) closeCurrentWebSearch("failed", []);
|
|
754
1035
|
flushHiddenReasoningEnvelope();
|
|
755
1036
|
options?.onUsage?.(event.usage);
|
|
@@ -769,11 +1050,15 @@ export function bridgeToResponsesSSE(
|
|
|
769
1050
|
break;
|
|
770
1051
|
}
|
|
771
1052
|
case "error": {
|
|
1053
|
+
if (event.code === "translation_buffer_limit") {
|
|
1054
|
+
terminateForTranslatorOverflow(event);
|
|
1055
|
+
return;
|
|
1056
|
+
}
|
|
772
1057
|
if (currentMsg) closeCurrentMessage();
|
|
773
1058
|
if (currentReasoning) closeCurrentReasoning();
|
|
774
1059
|
if (currentRawReasoning) closeCurrentRawReasoning();
|
|
775
1060
|
flushHiddenRawReasoning();
|
|
776
|
-
if (currentToolCall)
|
|
1061
|
+
if (currentToolCall) failCurrentToolCall();
|
|
777
1062
|
if (currentWebSearch) closeCurrentWebSearch("failed", []);
|
|
778
1063
|
const failure = adapterFailureFromEvent(event);
|
|
779
1064
|
if (event.usage) options?.onUsage?.(event.usage);
|
|
@@ -794,15 +1079,19 @@ export function bridgeToResponsesSSE(
|
|
|
794
1079
|
}
|
|
795
1080
|
}
|
|
796
1081
|
if (terminalEvent) {
|
|
797
|
-
|
|
1082
|
+
cancelUpstreamOnce();
|
|
798
1083
|
terminated = true;
|
|
799
|
-
returnIterator();
|
|
800
1084
|
break;
|
|
801
1085
|
}
|
|
802
1086
|
}
|
|
803
1087
|
} catch (err) {
|
|
1088
|
+
if (isTranslatorBudgetExceededError(err)) {
|
|
1089
|
+
terminateForTranslatorOverflow(err);
|
|
1090
|
+
return;
|
|
1091
|
+
}
|
|
804
1092
|
if (!terminated) {
|
|
805
1093
|
flushHiddenRawReasoning();
|
|
1094
|
+
if (currentToolCall) failCurrentToolCall();
|
|
806
1095
|
if (currentWebSearch) closeCurrentWebSearch("failed", []);
|
|
807
1096
|
emit("response.failed", {
|
|
808
1097
|
response: {
|
|
@@ -812,9 +1101,8 @@ export function bridgeToResponsesSSE(
|
|
|
812
1101
|
},
|
|
813
1102
|
});
|
|
814
1103
|
reportTerminal("failed");
|
|
815
|
-
|
|
1104
|
+
cancelUpstreamOnce();
|
|
816
1105
|
terminated = true;
|
|
817
|
-
returnIterator();
|
|
818
1106
|
}
|
|
819
1107
|
}
|
|
820
1108
|
|
|
@@ -832,7 +1120,7 @@ export function bridgeToResponsesSSE(
|
|
|
832
1120
|
if (currentReasoning) closeCurrentReasoning();
|
|
833
1121
|
if (currentRawReasoning) closeCurrentRawReasoning();
|
|
834
1122
|
flushHiddenRawReasoning();
|
|
835
|
-
if (currentToolCall)
|
|
1123
|
+
if (currentToolCall) failCurrentToolCall();
|
|
836
1124
|
if (currentWebSearch) closeCurrentWebSearch("failed", []);
|
|
837
1125
|
options?.onUsage?.(undefined);
|
|
838
1126
|
emit("response.incomplete", {
|
|
@@ -872,7 +1160,7 @@ export function bridgeToResponsesSSE(
|
|
|
872
1160
|
if (currentReasoning) closeCurrentReasoning();
|
|
873
1161
|
if (currentRawReasoning) closeCurrentRawReasoning();
|
|
874
1162
|
flushHiddenRawReasoning();
|
|
875
|
-
if (currentToolCall)
|
|
1163
|
+
if (currentToolCall) failCurrentToolCall();
|
|
876
1164
|
if (currentWebSearch) closeCurrentWebSearch("failed", []);
|
|
877
1165
|
emit("response.incomplete", {
|
|
878
1166
|
response: {
|
|
@@ -881,9 +1169,8 @@ export function bridgeToResponsesSSE(
|
|
|
881
1169
|
},
|
|
882
1170
|
});
|
|
883
1171
|
reportTerminal("incomplete");
|
|
884
|
-
|
|
1172
|
+
cancelUpstreamOnce();
|
|
885
1173
|
terminated = true;
|
|
886
|
-
returnIterator();
|
|
887
1174
|
emitDone();
|
|
888
1175
|
if (beat !== undefined) clearBeatInterval(beat);
|
|
889
1176
|
beat = undefined;
|
|
@@ -919,8 +1206,7 @@ export function bridgeToResponsesSSE(
|
|
|
919
1206
|
clientCancelled = true;
|
|
920
1207
|
closed = true;
|
|
921
1208
|
if (beat !== undefined) clearBeatInterval(beat);
|
|
922
|
-
|
|
923
|
-
returnIterator();
|
|
1209
|
+
cancelUpstreamOnce();
|
|
924
1210
|
},
|
|
925
1211
|
});
|
|
926
1212
|
}
|
|
@@ -938,10 +1224,49 @@ export function buildResponseJSON(
|
|
|
938
1224
|
onProviderState?: (state: OcxProviderContinuationState) => void;
|
|
939
1225
|
/** Raw adapter-reported usage before wire normalization (see bridgeToResponsesSSE onUsage). */
|
|
940
1226
|
onUsage?: (usage: OcxUsage | undefined) => void;
|
|
1227
|
+
translatorBudget?: TranslatorBudget;
|
|
941
1228
|
},
|
|
942
1229
|
): Record<string, unknown> {
|
|
943
1230
|
const responseId = `resp_${uuid()}`;
|
|
944
1231
|
const output: OutputItem[] = [];
|
|
1232
|
+
const budget = options?.translatorBudget;
|
|
1233
|
+
const encoder = new TextEncoder();
|
|
1234
|
+
const bytesOf = (value: string): number => Buffer.byteLength(value);
|
|
1235
|
+
const appendBatchString = (
|
|
1236
|
+
previous: string,
|
|
1237
|
+
previousBytes: number,
|
|
1238
|
+
fragment: string,
|
|
1239
|
+
kind: TranslatorBufferKind,
|
|
1240
|
+
callId?: string,
|
|
1241
|
+
): { value: string; bytes: number } => {
|
|
1242
|
+
const nextBytes = previousBytes + bytesOf(fragment);
|
|
1243
|
+
if (!budget) return { value: previous + fragment, bytes: nextBytes };
|
|
1244
|
+
const scope = { kind, ...(callId ? { callId } : {}) };
|
|
1245
|
+
const reservation = budget.reserveTransient(nextBytes, scope);
|
|
1246
|
+
try {
|
|
1247
|
+
const value = previous + fragment;
|
|
1248
|
+
reservation.commitRetained();
|
|
1249
|
+
budget.releaseRetained(previousBytes, scope);
|
|
1250
|
+
return { value, bytes: nextBytes };
|
|
1251
|
+
} catch (error) {
|
|
1252
|
+
reservation.release();
|
|
1253
|
+
throw error;
|
|
1254
|
+
}
|
|
1255
|
+
};
|
|
1256
|
+
const replaceBatchRetainedString = (previousBytes: number, next: string, kind: TranslatorBufferKind): number => {
|
|
1257
|
+
const nextBytes = bytesOf(next);
|
|
1258
|
+
if (!budget) return nextBytes;
|
|
1259
|
+
const reservation = budget.reserveTransient(nextBytes, { kind });
|
|
1260
|
+
reservation.commitRetained();
|
|
1261
|
+
budget.releaseRetained(previousBytes, { kind });
|
|
1262
|
+
return nextBytes;
|
|
1263
|
+
};
|
|
1264
|
+
const pushOutput = (item: OutputItem, replacedBytes = 0, kind: TranslatorBufferKind = "retained_collectors") => {
|
|
1265
|
+
const reservation = budget?.reserveTransient(bytesOf(JSON.stringify(item)), { kind });
|
|
1266
|
+
output.push(item);
|
|
1267
|
+
reservation?.commitRetained();
|
|
1268
|
+
if (replacedBytes > 0) budget?.releaseRetained(replacedBytes, { kind });
|
|
1269
|
+
};
|
|
945
1270
|
let usage: OcxUsage | undefined;
|
|
946
1271
|
let errorEvent: Extract<AdapterEvent, { type: "error" }> | undefined;
|
|
947
1272
|
let incompleteEvent: Extract<AdapterEvent, { type: "incomplete" }> | undefined;
|
|
@@ -949,17 +1274,24 @@ export function buildResponseJSON(
|
|
|
949
1274
|
let stopReason: string | undefined;
|
|
950
1275
|
let cleanDone = false;
|
|
951
1276
|
let compactionText = "";
|
|
1277
|
+
let compactionTextBytes = 0;
|
|
952
1278
|
|
|
953
1279
|
let currentText = "";
|
|
1280
|
+
let currentTextBytes = 0;
|
|
954
1281
|
let currentTextPhase: OcxMessagePhase | undefined;
|
|
955
1282
|
let currentSummaryReasoning = "";
|
|
1283
|
+
let currentSummaryReasoningBytes = 0;
|
|
956
1284
|
let currentRawReasoning = "";
|
|
1285
|
+
let currentRawReasoningBytes = 0;
|
|
957
1286
|
// Anthropic extended-thinking round-trip (batch): see bridgeToResponsesSSE counterpart.
|
|
958
1287
|
let batchSignature: string | undefined;
|
|
1288
|
+
let batchSignatureBytes = 0;
|
|
959
1289
|
let batchRedacted: string[] = [];
|
|
1290
|
+
let batchRedactedBytes = 0;
|
|
960
1291
|
let currentToolCallId = "";
|
|
961
1292
|
let currentToolCallName = "";
|
|
962
1293
|
let currentToolCallArgs = "";
|
|
1294
|
+
let currentToolCallArgsBytes = 0;
|
|
963
1295
|
// Web-search citations awaiting the next assistant message (attached as url_citation annotations).
|
|
964
1296
|
let pendingWebSources: { url: string; title?: string }[] = [];
|
|
965
1297
|
|
|
@@ -978,12 +1310,14 @@ export function buildResponseJSON(
|
|
|
978
1310
|
type: "url_citation", url: s.url, ...(s.title ? { title: s.title } : {}), start_index: 0, end_index: 0,
|
|
979
1311
|
}));
|
|
980
1312
|
pendingWebSources = [];
|
|
981
|
-
|
|
1313
|
+
const item = {
|
|
982
1314
|
type: "message", id: `msg_${uuid()}`, role: "assistant", status: "completed",
|
|
983
1315
|
content: [{ type: "output_text", text: currentText, annotations }],
|
|
984
1316
|
...(phase ? { phase } : {}),
|
|
985
|
-
}
|
|
1317
|
+
} as OutputItem;
|
|
1318
|
+
pushOutput(item, currentTextBytes);
|
|
986
1319
|
currentText = "";
|
|
1320
|
+
currentTextBytes = 0;
|
|
987
1321
|
currentTextPhase = undefined;
|
|
988
1322
|
};
|
|
989
1323
|
const flushSummaryReasoning = () => {
|
|
@@ -994,34 +1328,46 @@ export function buildResponseJSON(
|
|
|
994
1328
|
const hidden = options?.hideThinkingSummary === true;
|
|
995
1329
|
if (hidden && currentSummaryReasoning && (envelope.sig || envelope.red)) envelope.txt = currentSummaryReasoning;
|
|
996
1330
|
const encrypted = envelope.sig || envelope.red || envelope.txt ? encodeReasoningEnvelope(envelope) : undefined;
|
|
1331
|
+
const sourceBytes = currentSummaryReasoningBytes + batchSignatureBytes + batchRedactedBytes;
|
|
997
1332
|
batchSignature = undefined;
|
|
1333
|
+
batchSignatureBytes = 0;
|
|
998
1334
|
batchRedacted = [];
|
|
999
|
-
|
|
1000
|
-
|
|
1335
|
+
batchRedactedBytes = 0;
|
|
1336
|
+
if (hidden && !encrypted) {
|
|
1337
|
+
budget?.releaseRetained(sourceBytes, { kind: "reasoning" });
|
|
1338
|
+
currentSummaryReasoning = "";
|
|
1339
|
+
currentSummaryReasoningBytes = 0;
|
|
1340
|
+
return;
|
|
1341
|
+
}
|
|
1342
|
+
const item = {
|
|
1001
1343
|
type: "reasoning", id: `rs_${uuid()}`,
|
|
1002
1344
|
summary: !hidden && currentSummaryReasoning ? [{ type: "summary_text", text: currentSummaryReasoning }] : [],
|
|
1003
1345
|
...(encrypted ? { encrypted_content: encrypted } : {}),
|
|
1004
|
-
}
|
|
1346
|
+
} as OutputItem;
|
|
1347
|
+
pushOutput(item, sourceBytes, "reasoning");
|
|
1005
1348
|
currentSummaryReasoning = "";
|
|
1349
|
+
currentSummaryReasoningBytes = 0;
|
|
1006
1350
|
};
|
|
1007
1351
|
const flushRawReasoning = () => {
|
|
1008
1352
|
if (!currentRawReasoning) return;
|
|
1009
1353
|
if (options?.hideThinkingSummary === true) {
|
|
1010
1354
|
// Same contract as the streaming path: no visible reasoning, txt-only envelope round-trip.
|
|
1011
|
-
|
|
1355
|
+
pushOutput({
|
|
1012
1356
|
type: "reasoning", id: `rs_${uuid()}`, summary: [],
|
|
1013
1357
|
encrypted_content: encodeReasoningEnvelope({ txt: currentRawReasoning }),
|
|
1014
|
-
});
|
|
1358
|
+
}, currentRawReasoningBytes, "reasoning");
|
|
1015
1359
|
currentRawReasoning = "";
|
|
1360
|
+
currentRawReasoningBytes = 0;
|
|
1016
1361
|
return;
|
|
1017
1362
|
}
|
|
1018
|
-
|
|
1363
|
+
pushOutput({
|
|
1019
1364
|
type: "reasoning", id: `rs_${uuid()}`, summary: [],
|
|
1020
1365
|
content: [{ type: "reasoning_text", text: currentRawReasoning }],
|
|
1021
|
-
});
|
|
1366
|
+
}, currentRawReasoningBytes, "reasoning");
|
|
1022
1367
|
currentRawReasoning = "";
|
|
1368
|
+
currentRawReasoningBytes = 0;
|
|
1023
1369
|
};
|
|
1024
|
-
const flushToolCall = () => {
|
|
1370
|
+
const flushToolCall = (status: "completed" | "incomplete" = "completed") => {
|
|
1025
1371
|
if (!currentToolCallId) return;
|
|
1026
1372
|
const mapped = options?.toolNsMap?.get(currentToolCallName);
|
|
1027
1373
|
const realName = mapped?.name ?? currentToolCallName;
|
|
@@ -1029,28 +1375,30 @@ export function buildResponseJSON(
|
|
|
1029
1375
|
const toolSearch = options?.toolSearchToolNames?.has(realName) ?? false;
|
|
1030
1376
|
const freeform = !toolSearch && (options?.freeformToolNames?.has(realName) ?? false);
|
|
1031
1377
|
if (toolSearch) {
|
|
1032
|
-
|
|
1378
|
+
pushOutput({
|
|
1033
1379
|
type: "tool_search_call", id: `tsc_${uuid()}`,
|
|
1034
1380
|
call_id: currentToolCallId, execution: "client",
|
|
1035
|
-
arguments: parseArgsObj(currentToolCallArgs), status
|
|
1381
|
+
arguments: parseArgsObj(currentToolCallArgs), status,
|
|
1036
1382
|
});
|
|
1037
1383
|
} else if (freeform) {
|
|
1038
|
-
|
|
1384
|
+
pushOutput({
|
|
1039
1385
|
type: "custom_tool_call", id: `ctc_${uuid()}`,
|
|
1040
1386
|
call_id: currentToolCallId, name: realName,
|
|
1041
|
-
input: freeformInput(currentToolCallArgs), status
|
|
1387
|
+
input: freeformInput(currentToolCallArgs), status,
|
|
1042
1388
|
});
|
|
1043
1389
|
} else {
|
|
1044
|
-
|
|
1390
|
+
pushOutput({
|
|
1045
1391
|
type: "function_call", id: `fc_${uuid()}`,
|
|
1046
1392
|
call_id: currentToolCallId, name: realName,
|
|
1047
|
-
arguments: currentToolCallArgs || "{}", status
|
|
1393
|
+
arguments: currentToolCallArgs || "{}", status,
|
|
1048
1394
|
...(ns ? { namespace: ns } : {}),
|
|
1049
1395
|
});
|
|
1050
1396
|
}
|
|
1397
|
+
budget?.closeCall(currentToolCallId);
|
|
1051
1398
|
currentToolCallId = "";
|
|
1052
1399
|
currentToolCallName = "";
|
|
1053
1400
|
currentToolCallArgs = "";
|
|
1401
|
+
currentToolCallArgsBytes = 0;
|
|
1054
1402
|
};
|
|
1055
1403
|
|
|
1056
1404
|
for (const e of events) {
|
|
@@ -1070,32 +1418,52 @@ export function buildResponseJSON(
|
|
|
1070
1418
|
if (currentToolCallId) flushToolCall();
|
|
1071
1419
|
// Compaction turns keep the summary out of normal message output (replay dedup — see
|
|
1072
1420
|
// bridgeToResponsesSSE); it ships only inside the synthetic compaction item below.
|
|
1073
|
-
if (options?.compaction)
|
|
1421
|
+
if (options?.compaction) {
|
|
1422
|
+
({ value: compactionText, bytes: compactionTextBytes } = appendBatchString(
|
|
1423
|
+
compactionText, compactionTextBytes, e.text, "retained_collectors",
|
|
1424
|
+
));
|
|
1425
|
+
}
|
|
1074
1426
|
else {
|
|
1075
1427
|
if (e.phase !== undefined) currentTextPhase = e.phase;
|
|
1076
|
-
currentText
|
|
1428
|
+
({ value: currentText, bytes: currentTextBytes } = appendBatchString(
|
|
1429
|
+
currentText, currentTextBytes, e.text, "retained_collectors",
|
|
1430
|
+
));
|
|
1077
1431
|
}
|
|
1078
1432
|
break;
|
|
1079
1433
|
case "thinking_delta":
|
|
1080
1434
|
if (currentText) flushText("commentary");
|
|
1081
1435
|
if (currentRawReasoning) flushRawReasoning();
|
|
1082
1436
|
if (currentToolCallId) flushToolCall();
|
|
1083
|
-
|
|
1437
|
+
{
|
|
1438
|
+
({ value: currentSummaryReasoning, bytes: currentSummaryReasoningBytes } = appendBatchString(
|
|
1439
|
+
currentSummaryReasoning, currentSummaryReasoningBytes, e.thinking, "reasoning",
|
|
1440
|
+
));
|
|
1441
|
+
}
|
|
1084
1442
|
break;
|
|
1085
1443
|
case "thinking_signature":
|
|
1086
1444
|
// End of the current thinking block — flush it WITH the signature envelope so the
|
|
1087
1445
|
// block/signature pairing survives multi-block turns.
|
|
1446
|
+
batchSignatureBytes = replaceBatchRetainedString(batchSignatureBytes, e.signature, "reasoning");
|
|
1088
1447
|
batchSignature = e.signature;
|
|
1089
1448
|
flushSummaryReasoning();
|
|
1090
1449
|
break;
|
|
1091
1450
|
case "redacted_thinking":
|
|
1451
|
+
{
|
|
1452
|
+
const dataBytes = bytesOf(e.data);
|
|
1453
|
+
budget?.chargeRetained(dataBytes, { kind: "reasoning" });
|
|
1454
|
+
batchRedactedBytes += dataBytes;
|
|
1455
|
+
}
|
|
1092
1456
|
batchRedacted.push(e.data);
|
|
1093
1457
|
break;
|
|
1094
1458
|
case "reasoning_raw_delta":
|
|
1095
1459
|
if (currentText) flushText("commentary");
|
|
1096
1460
|
if (currentSummaryReasoning) flushSummaryReasoning();
|
|
1097
1461
|
if (currentToolCallId) flushToolCall();
|
|
1098
|
-
|
|
1462
|
+
{
|
|
1463
|
+
({ value: currentRawReasoning, bytes: currentRawReasoningBytes } = appendBatchString(
|
|
1464
|
+
currentRawReasoning, currentRawReasoningBytes, e.text, "reasoning",
|
|
1465
|
+
));
|
|
1466
|
+
}
|
|
1099
1467
|
break;
|
|
1100
1468
|
case "tool_call_start":
|
|
1101
1469
|
if (currentText) flushText("commentary");
|
|
@@ -1103,13 +1471,36 @@ export function buildResponseJSON(
|
|
|
1103
1471
|
if (currentRawReasoning) flushRawReasoning();
|
|
1104
1472
|
flushToolCall();
|
|
1105
1473
|
currentToolCallId = e.id;
|
|
1474
|
+
budget?.openCall(e.id);
|
|
1106
1475
|
currentToolCallName = e.name;
|
|
1107
1476
|
currentToolCallArgs = "";
|
|
1477
|
+
currentToolCallArgsBytes = 0;
|
|
1108
1478
|
break;
|
|
1109
1479
|
case "tool_call_delta":
|
|
1110
|
-
|
|
1480
|
+
{
|
|
1481
|
+
({ value: currentToolCallArgs, bytes: currentToolCallArgsBytes } = appendBatchString(
|
|
1482
|
+
currentToolCallArgs, currentToolCallArgsBytes, e.arguments, "tool_args", currentToolCallId,
|
|
1483
|
+
));
|
|
1484
|
+
}
|
|
1111
1485
|
break;
|
|
1112
1486
|
case "tool_call_end":
|
|
1487
|
+
if (!toolCallArgumentsUsable(currentToolCallArgs) && currentToolCallId) {
|
|
1488
|
+
// Mirror the streaming path: refuse to complete unusable arguments.
|
|
1489
|
+
const mapped = options?.toolNsMap?.get(currentToolCallName);
|
|
1490
|
+
const realName = mapped?.name ?? currentToolCallName;
|
|
1491
|
+
const toolSearch = options?.toolSearchToolNames?.has(realName) ?? false;
|
|
1492
|
+
const freeform = !toolSearch && (options?.freeformToolNames?.has(realName) ?? false);
|
|
1493
|
+
if (!freeform && !toolSearch) {
|
|
1494
|
+
flushToolCall("incomplete");
|
|
1495
|
+
errorEvent = {
|
|
1496
|
+
type: "error",
|
|
1497
|
+
message: "upstream stream produced malformed tool call arguments",
|
|
1498
|
+
status: 502,
|
|
1499
|
+
errorType: "upstream_error",
|
|
1500
|
+
};
|
|
1501
|
+
break;
|
|
1502
|
+
}
|
|
1503
|
+
}
|
|
1113
1504
|
flushToolCall();
|
|
1114
1505
|
break;
|
|
1115
1506
|
case "web_search_call_begin":
|
|
@@ -1121,7 +1512,7 @@ export function buildResponseJSON(
|
|
|
1121
1512
|
if (currentSummaryReasoning) flushSummaryReasoning();
|
|
1122
1513
|
if (currentRawReasoning) flushRawReasoning();
|
|
1123
1514
|
flushToolCall();
|
|
1124
|
-
|
|
1515
|
+
pushOutput({
|
|
1125
1516
|
type: "web_search_call", id: `ws_${uuid()}`, status: e.status ?? "completed",
|
|
1126
1517
|
action: webSearchAction(e.queries),
|
|
1127
1518
|
...(e.sources && e.sources.length > 0 ? { sources: e.sources } : {}),
|
|
@@ -1129,7 +1520,11 @@ export function buildResponseJSON(
|
|
|
1129
1520
|
if (e.sources) {
|
|
1130
1521
|
const seen = new Set(pendingWebSources.map(s => s.url));
|
|
1131
1522
|
for (const s of e.sources) {
|
|
1132
|
-
if (!seen.has(s.url)) {
|
|
1523
|
+
if (!seen.has(s.url)) {
|
|
1524
|
+
seen.add(s.url);
|
|
1525
|
+
budget?.chargeRetained(bytesOf(JSON.stringify(s)), { kind: "tool_search_sources" });
|
|
1526
|
+
pendingWebSources.push(s);
|
|
1527
|
+
}
|
|
1133
1528
|
}
|
|
1134
1529
|
}
|
|
1135
1530
|
break;
|
|
@@ -1151,11 +1546,13 @@ export function buildResponseJSON(
|
|
|
1151
1546
|
if (e.stopReason === "max_tokens" || e.stopReason === "content_filter") stopReason = e.stopReason;
|
|
1152
1547
|
break;
|
|
1153
1548
|
}
|
|
1549
|
+
if (budget) releaseTranslatedEvent(e, budget);
|
|
1154
1550
|
}
|
|
1155
1551
|
flushText(cleanDone && !errorEvent && !incompleteEvent ? "final_answer" : undefined);
|
|
1156
1552
|
flushSummaryReasoning();
|
|
1157
1553
|
flushRawReasoning();
|
|
1158
|
-
|
|
1554
|
+
// Open tool call on a failed/incomplete turn must not land as status:"completed".
|
|
1555
|
+
if (currentToolCallId) flushToolCall(errorEvent || incompleteEvent ? "incomplete" : "completed");
|
|
1159
1556
|
// A truncated turn must never be installed as replacement history: emit the
|
|
1160
1557
|
// compaction item only when the turn actually completed (#422).
|
|
1161
1558
|
if (
|
|
@@ -1165,7 +1562,7 @@ export function buildResponseJSON(
|
|
|
1165
1562
|
&& stopReason !== "max_tokens"
|
|
1166
1563
|
&& stopReason !== "content_filter"
|
|
1167
1564
|
) {
|
|
1168
|
-
|
|
1565
|
+
pushOutput({ type: "compaction", id: `cmp_${uuid()}`, encrypted_content: encodeCompactionSummary(compactionText) }, compactionTextBytes);
|
|
1169
1566
|
}
|
|
1170
1567
|
|
|
1171
1568
|
const failure = errorEvent ? adapterFailureFromEvent(errorEvent) : undefined;
|