@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/chat/outbound.ts
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
type Rec = Record<string, unknown>;
|
|
9
9
|
|
|
10
10
|
import { decodeServerSentEvents } from "../lib/sse-decoder";
|
|
11
|
+
import { isTranslatorBudgetExceededError, type TranslatorBudget } from "../lib/translator-budget";
|
|
11
12
|
import { classifyError, CYBER_POLICY_ERROR_CODE, isCyberPolicyCode, isCyberPolicyMessage } from "../lib/errors";
|
|
12
13
|
|
|
13
14
|
function isRec(v: unknown): v is Rec {
|
|
@@ -135,13 +136,28 @@ function chunkBase(id: string, model: string, created: number): Rec {
|
|
|
135
136
|
};
|
|
136
137
|
}
|
|
137
138
|
|
|
139
|
+
function appendedUtf8Bytes(previous: string, previousBytes: number, fragment: string): number {
|
|
140
|
+
let nextBytes = previousBytes + Buffer.byteLength(fragment);
|
|
141
|
+
const previousLast = previous.charCodeAt(previous.length - 1);
|
|
142
|
+
const fragmentFirst = fragment.charCodeAt(0);
|
|
143
|
+
if (previousLast >= 0xd800 && previousLast <= 0xdbff
|
|
144
|
+
&& fragmentFirst >= 0xdc00 && fragmentFirst <= 0xdfff) {
|
|
145
|
+
// Buffer.byteLength() replaces each isolated surrogate with three bytes, while the joined
|
|
146
|
+
// pair is one four-byte scalar. Preserve full-string sizing without re-encoding the prefix.
|
|
147
|
+
nextBytes -= 2;
|
|
148
|
+
}
|
|
149
|
+
return nextBytes;
|
|
150
|
+
}
|
|
151
|
+
|
|
138
152
|
/**
|
|
139
153
|
* Streaming: Responses SSE bytes -> Chat Completions SSE bytes.
|
|
140
154
|
*/
|
|
141
155
|
export function responsesSseToChatCompletionsSse(
|
|
142
156
|
upstream: ReadableStream<Uint8Array>,
|
|
143
157
|
model: string,
|
|
158
|
+
opts: { translatorBudget: TranslatorBudget },
|
|
144
159
|
): ReadableStream<Uint8Array> {
|
|
160
|
+
const translatorBudget = opts.translatorBudget;
|
|
145
161
|
const encoder = new TextEncoder();
|
|
146
162
|
let terminated = false;
|
|
147
163
|
let cancelled = false;
|
|
@@ -155,6 +171,7 @@ export function responsesSseToChatCompletionsSse(
|
|
|
155
171
|
const toolCallIdByIndex = new Map<number, string>();
|
|
156
172
|
const toolNameByIndex = new Map<number, string>();
|
|
157
173
|
const toolArgumentsByIndex = new Map<number, string>();
|
|
174
|
+
const toolArgumentBytesByIndex = new Map<number, number>();
|
|
158
175
|
const emittedToolIndexes = new Set<number>();
|
|
159
176
|
let nextToolIndex = 0;
|
|
160
177
|
let sseIterator: AsyncGenerator<{ event?: string; data: string }> | undefined;
|
|
@@ -164,9 +181,54 @@ export function responsesSseToChatCompletionsSse(
|
|
|
164
181
|
let emittedFrames = 0;
|
|
165
182
|
let stepping = false;
|
|
166
183
|
let decoderStarted = false;
|
|
184
|
+
const queuedLiveFrameBytes: number[] = [];
|
|
185
|
+
const enqueueLiveFrame = (frame: Uint8Array) => {
|
|
186
|
+
const reservation = translatorBudget.reserveTransient(frame.byteLength, { kind: "live_transient" });
|
|
187
|
+
controller.enqueue(frame);
|
|
188
|
+
reservation.commitRetained();
|
|
189
|
+
queuedLiveFrameBytes.push(frame.byteLength);
|
|
190
|
+
};
|
|
191
|
+
const releaseDeliveredFrame = () => {
|
|
192
|
+
const bytes = queuedLiveFrameBytes.shift();
|
|
193
|
+
if (bytes !== undefined) translatorBudget.releaseRetained(bytes, { kind: "live_transient" });
|
|
194
|
+
};
|
|
195
|
+
const closeToolCalls = () => {
|
|
196
|
+
for (const callId of toolIndexByCallId.keys()) translatorBudget.closeCall(callId);
|
|
197
|
+
};
|
|
198
|
+
const replaceToolArguments = (toolIndex: number, next: string) => {
|
|
199
|
+
const previous = toolArgumentsByIndex.get(toolIndex) ?? "";
|
|
200
|
+
if (previous === next && toolArgumentsByIndex.has(toolIndex)) return;
|
|
201
|
+
const previousBytes = toolArgumentBytesByIndex.get(toolIndex) ?? 0;
|
|
202
|
+
const nextBytes = Buffer.byteLength(next);
|
|
203
|
+
const callId = toolCallIdByIndex.get(toolIndex);
|
|
204
|
+
const scope = { kind: "tool_args" as const, ...(callId ? { callId } : {}) };
|
|
205
|
+
const reservation = translatorBudget.reserveTransient(nextBytes, scope);
|
|
206
|
+
toolArgumentsByIndex.set(toolIndex, next);
|
|
207
|
+
toolArgumentBytesByIndex.set(toolIndex, nextBytes);
|
|
208
|
+
reservation.commitRetained();
|
|
209
|
+
translatorBudget.releaseRetained(previousBytes, scope);
|
|
210
|
+
};
|
|
211
|
+
const appendToolArguments = (toolIndex: number, fragment: string) => {
|
|
212
|
+
const previous = toolArgumentsByIndex.get(toolIndex) ?? "";
|
|
213
|
+
const previousBytes = toolArgumentBytesByIndex.get(toolIndex) ?? 0;
|
|
214
|
+
const nextBytes = appendedUtf8Bytes(previous, previousBytes, fragment);
|
|
215
|
+
const callId = toolCallIdByIndex.get(toolIndex);
|
|
216
|
+
const scope = { kind: "tool_args" as const, ...(callId ? { callId } : {}) };
|
|
217
|
+
const reservation = translatorBudget.reserveTransient(nextBytes, scope);
|
|
218
|
+
try {
|
|
219
|
+
const next = previous + fragment;
|
|
220
|
+
toolArgumentsByIndex.set(toolIndex, next);
|
|
221
|
+
toolArgumentBytesByIndex.set(toolIndex, nextBytes);
|
|
222
|
+
reservation.commitRetained();
|
|
223
|
+
translatorBudget.releaseRetained(previousBytes, scope);
|
|
224
|
+
} catch (error) {
|
|
225
|
+
reservation.release();
|
|
226
|
+
throw error;
|
|
227
|
+
}
|
|
228
|
+
};
|
|
167
229
|
const emit = (payload: Rec | "[DONE]") => {
|
|
168
230
|
if (failed) return;
|
|
169
|
-
|
|
231
|
+
enqueueLiveFrame(encoder.encode(dataFrame(payload)));
|
|
170
232
|
emittedFrames++;
|
|
171
233
|
};
|
|
172
234
|
const ensureRole = () => {
|
|
@@ -251,21 +313,32 @@ export function responsesSseToChatCompletionsSse(
|
|
|
251
313
|
// like response.text().
|
|
252
314
|
const statusHint = details?.status ?? streamErrorStatus(message);
|
|
253
315
|
const classified = classifyError(statusHint, details?.type ?? "upstream_error", message);
|
|
254
|
-
|
|
316
|
+
const translatorOverflow = details?.code === "translation_buffer_limit";
|
|
317
|
+
if (translatorOverflow) {
|
|
318
|
+
upstreamAbort.abort(new Error("upstream translation buffer exceeded the safe limit"));
|
|
319
|
+
closeToolCalls();
|
|
320
|
+
try { void sseIterator?.return(undefined).catch(() => {}); } catch { /* already closed */ }
|
|
321
|
+
classified.code = "translation_buffer_limit";
|
|
322
|
+
classified.type = "invalid_request_error";
|
|
323
|
+
} else if (isCyberPolicyCode(details?.code) || classified.code === CYBER_POLICY_ERROR_CODE) {
|
|
255
324
|
classified.code = CYBER_POLICY_ERROR_CODE;
|
|
256
325
|
classified.type = "invalid_request_error";
|
|
257
326
|
} else if (details?.code !== undefined && details.code !== null && !classified.code) {
|
|
258
327
|
classified.code = details.code;
|
|
259
328
|
}
|
|
260
329
|
try {
|
|
261
|
-
|
|
330
|
+
const frame = encoder.encode(dataFrame({
|
|
262
331
|
error: {
|
|
263
332
|
message: classified.message,
|
|
264
333
|
type: classified.type,
|
|
265
334
|
param: null,
|
|
266
335
|
code: classified.code,
|
|
267
336
|
},
|
|
268
|
-
}))
|
|
337
|
+
}));
|
|
338
|
+
// The budget is already exhausted. This bounded emergency frame is the sole
|
|
339
|
+
// typed overflow closure and therefore cannot reserve from that budget again.
|
|
340
|
+
if (translatorOverflow) controller.enqueue(frame);
|
|
341
|
+
else enqueueLiveFrame(frame);
|
|
269
342
|
emittedFrames++;
|
|
270
343
|
} catch {
|
|
271
344
|
/* controller may already be closed */
|
|
@@ -303,12 +376,11 @@ export function responsesSseToChatCompletionsSse(
|
|
|
303
376
|
toolCallIdByIndex.set(toolIndex, callId);
|
|
304
377
|
if (typeof item.id === "string") toolIndexByItemId.set(item.id, toolIndex);
|
|
305
378
|
if (name) toolNameByIndex.set(toolIndex, name);
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
}
|
|
379
|
+
translatorBudget.openCall(callId);
|
|
380
|
+
if (!toolArgumentsByIndex.has(toolIndex)) replaceToolArguments(
|
|
381
|
+
toolIndex,
|
|
382
|
+
typeof item.arguments === "string" ? item.arguments : "",
|
|
383
|
+
);
|
|
312
384
|
break;
|
|
313
385
|
}
|
|
314
386
|
case "response.function_call_arguments.delta": {
|
|
@@ -317,10 +389,7 @@ export function responsesSseToChatCompletionsSse(
|
|
|
317
389
|
const toolIndex = (itemId ? toolIndexByItemId.get(itemId) : undefined)
|
|
318
390
|
?? (nextToolIndex > 0 ? nextToolIndex - 1 : 0);
|
|
319
391
|
ensureRole();
|
|
320
|
-
|
|
321
|
-
toolIndex,
|
|
322
|
-
(toolArgumentsByIndex.get(toolIndex) ?? "") + data.delta,
|
|
323
|
-
);
|
|
392
|
+
appendToolArguments(toolIndex, data.delta);
|
|
324
393
|
break;
|
|
325
394
|
}
|
|
326
395
|
case "response.function_call_arguments.done": {
|
|
@@ -331,10 +400,7 @@ export function responsesSseToChatCompletionsSse(
|
|
|
331
400
|
const name = typeof data.name === "string" ? data.name : "";
|
|
332
401
|
if (name) toolNameByIndex.set(toolIndex, name);
|
|
333
402
|
const streamedArgs = toolArgumentsByIndex.get(toolIndex) ?? "";
|
|
334
|
-
|
|
335
|
-
toolIndex,
|
|
336
|
-
resolveFinalArguments(data.arguments, streamedArgs),
|
|
337
|
-
);
|
|
403
|
+
replaceToolArguments(toolIndex, resolveFinalArguments(data.arguments, streamedArgs));
|
|
338
404
|
break;
|
|
339
405
|
}
|
|
340
406
|
case "response.output_item.done": {
|
|
@@ -351,6 +417,7 @@ export function responsesSseToChatCompletionsSse(
|
|
|
351
417
|
const toolIndex = existingIndex ?? nextToolIndex++;
|
|
352
418
|
toolIndexByCallId.set(callId, toolIndex);
|
|
353
419
|
toolCallIdByIndex.set(toolIndex, callId);
|
|
420
|
+
translatorBudget.openCall(callId);
|
|
354
421
|
if (itemId) toolIndexByItemId.set(itemId, toolIndex);
|
|
355
422
|
if (name) toolNameByIndex.set(toolIndex, name);
|
|
356
423
|
const finalName = name || toolNameByIndex.get(toolIndex) || "";
|
|
@@ -361,7 +428,7 @@ export function responsesSseToChatCompletionsSse(
|
|
|
361
428
|
// accumulate them. Emit one complete call here; finish() flushes any item whose
|
|
362
429
|
// done event was omitted, and replace-style clients still get a complete object.
|
|
363
430
|
const args = resolveFinalArguments(item.arguments, streamedArgs);
|
|
364
|
-
|
|
431
|
+
replaceToolArguments(toolIndex, args);
|
|
365
432
|
emitToolCall(toolIndex, callId, finalName, args);
|
|
366
433
|
}
|
|
367
434
|
break;
|
|
@@ -400,8 +467,9 @@ export function responsesSseToChatCompletionsSse(
|
|
|
400
467
|
const type = typeof error.type === "string" ? error.type : undefined;
|
|
401
468
|
fail(message, {
|
|
402
469
|
code,
|
|
403
|
-
|
|
404
|
-
|
|
470
|
+
...(code === "translation_buffer_limit"
|
|
471
|
+
? { status: 413, type: "invalid_request_error" }
|
|
472
|
+
: { type, ...(code === CYBER_POLICY_ERROR_CODE ? { status: 400 } : {}) }),
|
|
405
473
|
});
|
|
406
474
|
break;
|
|
407
475
|
}
|
|
@@ -413,7 +481,7 @@ export function responsesSseToChatCompletionsSse(
|
|
|
413
481
|
// Shared spec-shaped SSE decoder: handles CRLF framing, arbitrary chunk boundaries,
|
|
414
482
|
// multi-line data, and a terminal event without a trailing blank line (Sol audit
|
|
415
483
|
// blocker 3 — the hand-rolled "\n\n" splitter misreported those as truncation).
|
|
416
|
-
sseIterator = decodeServerSentEvents(upstream, { signal: upstreamAbort.signal });
|
|
484
|
+
sseIterator = decodeServerSentEvents(upstream, { signal: upstreamAbort.signal, translatorBudget });
|
|
417
485
|
const step = async () => {
|
|
418
486
|
if (stepping || cancelled) return;
|
|
419
487
|
stepping = true;
|
|
@@ -443,7 +511,13 @@ export function responsesSseToChatCompletionsSse(
|
|
|
443
511
|
handleFrame(eventName, data);
|
|
444
512
|
}
|
|
445
513
|
} catch (err) {
|
|
446
|
-
|
|
514
|
+
if (isTranslatorBudgetExceededError(err)) {
|
|
515
|
+
upstreamAbort.abort(err);
|
|
516
|
+
closeToolCalls();
|
|
517
|
+
fail(err.message, { status: 413, type: "invalid_request_error", code: err.code });
|
|
518
|
+
} else {
|
|
519
|
+
fail(err instanceof Error ? err.message : String(err));
|
|
520
|
+
}
|
|
447
521
|
} finally {
|
|
448
522
|
stepping = false;
|
|
449
523
|
}
|
|
@@ -456,10 +530,13 @@ export function responsesSseToChatCompletionsSse(
|
|
|
456
530
|
// decoding pauses until the chat consumer creates demand again.
|
|
457
531
|
},
|
|
458
532
|
pull() {
|
|
533
|
+
releaseDeliveredFrame();
|
|
459
534
|
return step();
|
|
460
535
|
},
|
|
461
536
|
cancel(reason) {
|
|
462
537
|
cancelled = true;
|
|
538
|
+
while (queuedLiveFrameBytes.length > 0) releaseDeliveredFrame();
|
|
539
|
+
closeToolCalls();
|
|
463
540
|
// Abort first: it cancels the decoder's underlying reader, settling any in-flight
|
|
464
541
|
// read() so the generator's return() below resolves promptly instead of hanging
|
|
465
542
|
// behind an idle upstream (Sol re-verification blocker).
|
|
@@ -545,15 +622,22 @@ export function responsesJsonToChatCompletion(json: unknown, model: string): Rec
|
|
|
545
622
|
export async function collectChatCompletion(
|
|
546
623
|
stream: ReadableStream<Uint8Array>,
|
|
547
624
|
model: string,
|
|
625
|
+
translatorBudget: TranslatorBudget,
|
|
548
626
|
): Promise<Rec> {
|
|
549
627
|
const decoder = new TextDecoder();
|
|
550
628
|
let buffer = "";
|
|
551
629
|
let content = "";
|
|
552
630
|
let reasoning = "";
|
|
553
|
-
const toolCalls = new Map<number, { id: string; name: string; arguments: string }>();
|
|
631
|
+
const toolCalls = new Map<number, { id: string; name: string; arguments: string; argumentBytes: number }>();
|
|
554
632
|
let finishReason = "stop";
|
|
555
633
|
let usage: unknown;
|
|
556
634
|
let streamError: ChatCompletionsStreamError | null = null;
|
|
635
|
+
const replaceRetained = (previous: string, next: string, kind: "live_transient" | "retained_collectors") => {
|
|
636
|
+
const reservation = translatorBudget.reserveTransient(Buffer.byteLength(next), { kind });
|
|
637
|
+
reservation.commitRetained();
|
|
638
|
+
translatorBudget.releaseRetained(Buffer.byteLength(previous), { kind });
|
|
639
|
+
return next;
|
|
640
|
+
};
|
|
557
641
|
const reader = stream.getReader();
|
|
558
642
|
try {
|
|
559
643
|
for (;;) {
|
|
@@ -563,15 +647,22 @@ export async function collectChatCompletion(
|
|
|
563
647
|
({ done, value } = await reader.read());
|
|
564
648
|
} catch (err) {
|
|
565
649
|
if (isChatCompletionsStreamError(err)) throw err;
|
|
650
|
+
if (isTranslatorBudgetExceededError(err)) {
|
|
651
|
+
throw new ChatCompletionsStreamError(err.message, {
|
|
652
|
+
status: 413,
|
|
653
|
+
type: "invalid_request_error",
|
|
654
|
+
code: err.code,
|
|
655
|
+
});
|
|
656
|
+
}
|
|
566
657
|
throw new ChatCompletionsStreamError(err instanceof Error ? err.message : String(err));
|
|
567
658
|
}
|
|
568
659
|
if (done) break;
|
|
569
660
|
if (!value) continue;
|
|
570
|
-
buffer
|
|
661
|
+
buffer = replaceRetained(buffer, buffer + decoder.decode(value, { stream: true }), "live_transient");
|
|
571
662
|
let sep: number;
|
|
572
663
|
while ((sep = buffer.indexOf("\n\n")) !== -1) {
|
|
573
664
|
const rawFrame = buffer.slice(0, sep);
|
|
574
|
-
buffer = buffer.slice(sep + 2);
|
|
665
|
+
buffer = replaceRetained(buffer, buffer.slice(sep + 2), "live_transient");
|
|
575
666
|
for (const line of rawFrame.split("\n")) {
|
|
576
667
|
if (!line.startsWith("data: ")) continue;
|
|
577
668
|
const data = line.slice(6).trim();
|
|
@@ -585,9 +676,11 @@ export async function collectChatCompletion(
|
|
|
585
676
|
: "upstream request failed";
|
|
586
677
|
const type = typeof parsed.error.type === "string" ? parsed.error.type : "server_error";
|
|
587
678
|
const code = typeof parsed.error.code === "string" ? parsed.error.code : null;
|
|
588
|
-
const status = code ===
|
|
589
|
-
?
|
|
590
|
-
:
|
|
679
|
+
const status = code === "translation_buffer_limit"
|
|
680
|
+
? 413
|
|
681
|
+
: code === CYBER_POLICY_ERROR_CODE || isCyberPolicyMessage(message)
|
|
682
|
+
? 400
|
|
683
|
+
: streamErrorStatus(message);
|
|
591
684
|
streamError = new ChatCompletionsStreamError(message, { status, type, code });
|
|
592
685
|
continue;
|
|
593
686
|
}
|
|
@@ -598,22 +691,31 @@ export async function collectChatCompletion(
|
|
|
598
691
|
if (typeof choice.finish_reason === "string") finishReason = choice.finish_reason;
|
|
599
692
|
const delta = isRec(choice.delta) ? choice.delta : null;
|
|
600
693
|
if (!delta) continue;
|
|
601
|
-
if (typeof delta.content === "string") content
|
|
602
|
-
if (typeof delta.reasoning_content === "string") reasoning
|
|
694
|
+
if (typeof delta.content === "string") content = replaceRetained(content, content + delta.content, "retained_collectors");
|
|
695
|
+
if (typeof delta.reasoning_content === "string") reasoning = replaceRetained(reasoning, reasoning + delta.reasoning_content, "retained_collectors");
|
|
603
696
|
if (Array.isArray(delta.tool_calls)) {
|
|
604
697
|
for (const tc of delta.tool_calls) {
|
|
605
698
|
if (!isRec(tc)) continue;
|
|
606
699
|
const index = typeof tc.index === "number" ? tc.index : 0;
|
|
607
|
-
const current = toolCalls.get(index) ?? { id: "", name: "", arguments: "" };
|
|
700
|
+
const current = toolCalls.get(index) ?? { id: "", name: "", arguments: "", argumentBytes: 0 };
|
|
608
701
|
if (typeof tc.id === "string") current.id = tc.id;
|
|
609
702
|
const fn = isRec(tc.function) ? tc.function : {};
|
|
610
703
|
// Done-frame final arguments are authoritative last-write-wins snapshots.
|
|
611
704
|
if (typeof fn.name === "string" && fn.name.length > 0) current.name = fn.name;
|
|
612
705
|
if (typeof fn.arguments === "string") {
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
current.arguments
|
|
706
|
+
const replace = fn.arguments.startsWith("{") || fn.arguments.startsWith("[") || current.arguments.length === 0;
|
|
707
|
+
const nextBytes = replace
|
|
708
|
+
? Buffer.byteLength(fn.arguments)
|
|
709
|
+
: appendedUtf8Bytes(current.arguments, current.argumentBytes, fn.arguments);
|
|
710
|
+
const reservation = translatorBudget.reserveTransient(nextBytes, { kind: "retained_collectors" });
|
|
711
|
+
try {
|
|
712
|
+
current.arguments = replace ? fn.arguments : current.arguments + fn.arguments;
|
|
713
|
+
reservation.commitRetained();
|
|
714
|
+
translatorBudget.releaseRetained(current.argumentBytes, { kind: "retained_collectors" });
|
|
715
|
+
current.argumentBytes = nextBytes;
|
|
716
|
+
} catch (error) {
|
|
717
|
+
reservation.release();
|
|
718
|
+
throw error;
|
|
617
719
|
}
|
|
618
720
|
}
|
|
619
721
|
toolCalls.set(index, current);
|
|
@@ -635,11 +737,15 @@ export async function collectChatCompletion(
|
|
|
635
737
|
if (toolCalls.size > 0) {
|
|
636
738
|
message.tool_calls = [...toolCalls.entries()]
|
|
637
739
|
.sort((a, b) => a[0] - b[0])
|
|
638
|
-
.map(([, tc]) =>
|
|
740
|
+
.map(([, tc]) => {
|
|
741
|
+
const copy = {
|
|
639
742
|
id: tc.id || `call_${uuid().slice(0, 16)}`,
|
|
640
743
|
type: "function",
|
|
641
744
|
function: { name: tc.name, arguments: tc.arguments },
|
|
642
|
-
|
|
745
|
+
};
|
|
746
|
+
translatorBudget.chargeRetained(Buffer.byteLength(JSON.stringify(copy)), { kind: "retained_collectors" });
|
|
747
|
+
return copy;
|
|
748
|
+
});
|
|
643
749
|
if (finishReason === "stop") finishReason = "tool_calls";
|
|
644
750
|
}
|
|
645
751
|
|
|
@@ -14,6 +14,8 @@
|
|
|
14
14
|
*/
|
|
15
15
|
import { createHmac, randomBytes } from "node:crypto";
|
|
16
16
|
import { isClaudeDebugEnabled } from "../lib/debug-settings";
|
|
17
|
+
import { retainedUtf8Bytes, truncateRetainedUtf8 } from "../lib/admission";
|
|
18
|
+
import { enforceAppOwnedMemoryBudget } from "../lib/app-owned-memory";
|
|
17
19
|
|
|
18
20
|
export interface ClaudeInboundDebugEntry {
|
|
19
21
|
/** Monotonic capture id — unique even when several entries share Date.now(). */
|
|
@@ -28,6 +30,8 @@ export interface ClaudeInboundDebugEntry {
|
|
|
28
30
|
thinkingBudgetTokens?: number;
|
|
29
31
|
outputConfigEffort?: string;
|
|
30
32
|
metadataKeys?: string[];
|
|
33
|
+
metadataKeysDropped?: number;
|
|
34
|
+
rowTruncated?: true;
|
|
31
35
|
hasMetadataUserId: boolean;
|
|
32
36
|
hasSystem: boolean;
|
|
33
37
|
/** Raw anthropic-beta header (comma list) — carries context-1m / effort betas. */
|
|
@@ -38,7 +42,11 @@ export interface ClaudeInboundDebugEntry {
|
|
|
38
42
|
}
|
|
39
43
|
|
|
40
44
|
const RING_LIMIT = 20;
|
|
45
|
+
const MAX_DIAGNOSTIC_VALUE_BYTES = 8 * 1024;
|
|
46
|
+
const MAX_CLAUDE_INBOUND_METADATA_KEYS = 64;
|
|
47
|
+
const MAX_CLAUDE_INBOUND_ROW_BYTES = 32 * 1024;
|
|
41
48
|
const ring: ClaudeInboundDebugEntry[] = [];
|
|
49
|
+
let ringBytes = 0;
|
|
42
50
|
const salt = randomBytes(16).toString("hex");
|
|
43
51
|
let lastEnabled = false;
|
|
44
52
|
let nextCaptureId = 0;
|
|
@@ -64,6 +72,18 @@ function systemText(system: unknown): string | undefined {
|
|
|
64
72
|
return undefined;
|
|
65
73
|
}
|
|
66
74
|
|
|
75
|
+
function entryBytes(entry: ClaudeInboundDebugEntry): number {
|
|
76
|
+
return retainedUtf8Bytes(JSON.stringify(entry));
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function removeOldest(): number {
|
|
80
|
+
const removed = ring.shift();
|
|
81
|
+
if (!removed) return 0;
|
|
82
|
+
const bytes = entryBytes(removed);
|
|
83
|
+
ringBytes -= bytes;
|
|
84
|
+
return bytes;
|
|
85
|
+
}
|
|
86
|
+
|
|
67
87
|
/** Record one inbound request. No-op (and ring flush) when the claude debug flag is off. */
|
|
68
88
|
export function captureClaudeInbound(
|
|
69
89
|
endpoint: "messages" | "count_tokens",
|
|
@@ -73,7 +93,7 @@ export function captureClaudeInbound(
|
|
|
73
93
|
): void {
|
|
74
94
|
const enabled = isClaudeDebugEnabled();
|
|
75
95
|
if (!enabled) {
|
|
76
|
-
if (lastEnabled)
|
|
96
|
+
if (lastEnabled) clearClaudeInboundDebug(); // flag turned off: drop captured entries
|
|
77
97
|
lastEnabled = false;
|
|
78
98
|
return;
|
|
79
99
|
}
|
|
@@ -84,26 +104,42 @@ export function captureClaudeInbound(
|
|
|
84
104
|
const metadata = isRec(body.metadata) ? body.metadata : undefined;
|
|
85
105
|
const userId = metadata && typeof metadata.user_id === "string" ? metadata.user_id : undefined;
|
|
86
106
|
const system = systemText(body.system);
|
|
107
|
+
const metadataNames = metadata ? Object.keys(metadata) : [];
|
|
87
108
|
const entry: ClaudeInboundDebugEntry = {
|
|
88
109
|
id: ++nextCaptureId,
|
|
89
110
|
at: Date.now(),
|
|
90
111
|
endpoint,
|
|
91
|
-
model: typeof body.model === "string" ? body.model : "unknown",
|
|
92
|
-
...(resolvedModel ? { resolvedModel } : {}),
|
|
112
|
+
model: truncateRetainedUtf8(typeof body.model === "string" ? body.model : "unknown", MAX_DIAGNOSTIC_VALUE_BYTES),
|
|
113
|
+
...(resolvedModel ? { resolvedModel: truncateRetainedUtf8(resolvedModel, MAX_DIAGNOSTIC_VALUE_BYTES) } : {}),
|
|
93
114
|
...(typeof body.stream === "boolean" ? { stream: body.stream } : {}),
|
|
94
115
|
...(typeof body.max_tokens === "number" ? { maxTokens: body.max_tokens } : {}),
|
|
95
|
-
...(thinking && typeof thinking.type === "string" ? { thinkingType: thinking.type } : {}),
|
|
116
|
+
...(thinking && typeof thinking.type === "string" ? { thinkingType: truncateRetainedUtf8(thinking.type, MAX_DIAGNOSTIC_VALUE_BYTES) } : {}),
|
|
96
117
|
...(thinking && typeof thinking.budget_tokens === "number" ? { thinkingBudgetTokens: thinking.budget_tokens } : {}),
|
|
97
|
-
...(outputConfig && typeof outputConfig.effort === "string" ? { outputConfigEffort: outputConfig.effort } : {}),
|
|
98
|
-
...(metadata ? { metadataKeys:
|
|
118
|
+
...(outputConfig && typeof outputConfig.effort === "string" ? { outputConfigEffort: truncateRetainedUtf8(outputConfig.effort, MAX_DIAGNOSTIC_VALUE_BYTES) } : {}),
|
|
119
|
+
...(metadata ? { metadataKeys: metadataNames.slice(0, MAX_CLAUDE_INBOUND_METADATA_KEYS).map(key => truncateRetainedUtf8(key, MAX_DIAGNOSTIC_VALUE_BYTES)) } : {}),
|
|
120
|
+
...(metadataNames.length > MAX_CLAUDE_INBOUND_METADATA_KEYS ? { metadataKeysDropped: metadataNames.length - MAX_CLAUDE_INBOUND_METADATA_KEYS } : {}),
|
|
99
121
|
hasMetadataUserId: userId !== undefined,
|
|
100
122
|
hasSystem: system !== undefined,
|
|
101
|
-
...(anthropicBeta ? { anthropicBeta } : {}),
|
|
123
|
+
...(anthropicBeta ? { anthropicBeta: truncateRetainedUtf8(anthropicBeta, MAX_DIAGNOSTIC_VALUE_BYTES) } : {}),
|
|
102
124
|
...(userId !== undefined ? { userIdTag: tag(userId) } : {}),
|
|
103
125
|
...(system !== undefined ? { systemTag: tag(system) } : {}),
|
|
104
126
|
};
|
|
127
|
+
while (entryBytes(entry) > MAX_CLAUDE_INBOUND_ROW_BYTES && entry.metadataKeys?.length) {
|
|
128
|
+
entry.metadataKeys.pop();
|
|
129
|
+
entry.metadataKeysDropped = (entry.metadataKeysDropped ?? 0) + 1;
|
|
130
|
+
entry.rowTruncated = true;
|
|
131
|
+
}
|
|
132
|
+
for (const key of ["anthropicBeta", "resolvedModel", "outputConfigEffort", "thinkingType"] as const) {
|
|
133
|
+
if (entryBytes(entry) <= MAX_CLAUDE_INBOUND_ROW_BYTES) break;
|
|
134
|
+
if (entry[key] !== undefined) {
|
|
135
|
+
delete entry[key];
|
|
136
|
+
entry.rowTruncated = true;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
105
139
|
ring.push(entry);
|
|
106
|
-
|
|
140
|
+
ringBytes += entryBytes(entry);
|
|
141
|
+
if (ring.length > RING_LIMIT) removeOldest();
|
|
142
|
+
enforceAppOwnedMemoryBudget();
|
|
107
143
|
}
|
|
108
144
|
|
|
109
145
|
/** Newest-first snapshot for /api/claude/inbound-debug. */
|
|
@@ -114,5 +150,14 @@ export function getClaudeInboundDebugEntries(): ClaudeInboundDebugEntry[] {
|
|
|
114
150
|
/** Test isolation / explicit clear. */
|
|
115
151
|
export function clearClaudeInboundDebug(): void {
|
|
116
152
|
ring.length = 0;
|
|
153
|
+
ringBytes = 0;
|
|
117
154
|
lastEnabled = false;
|
|
118
155
|
}
|
|
156
|
+
|
|
157
|
+
export function claudeInboundDebugMetrics(): { entries: number; bytes: number; oldestAt: number | null } {
|
|
158
|
+
return { entries: ring.length, bytes: ringBytes, oldestAt: ring[0]?.at ?? null };
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export function evictOldestClaudeInboundForBudget(): number {
|
|
162
|
+
return removeOldest();
|
|
163
|
+
}
|