@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/usage/log.ts
CHANGED
|
@@ -44,6 +44,12 @@ export interface PersistedUsageEntry {
|
|
|
44
44
|
provider: string;
|
|
45
45
|
model: string;
|
|
46
46
|
surface?: "claude" | "claude-desktop" | "grok";
|
|
47
|
+
/** Matched configured key id; absent for environment/loopback admissions and
|
|
48
|
+
* for every row written before attribution existed. */
|
|
49
|
+
apiKeyId?: string;
|
|
50
|
+
admissionKind?: "configured" | "environment" | "loopback";
|
|
51
|
+
/** The inbound wire, not the client product — see `surface`. */
|
|
52
|
+
inboundProtocol?: "responses" | "chat" | "messages";
|
|
47
53
|
/** Best-effort chat/session correlation for Logs grouping (#330). */
|
|
48
54
|
conversationId?: string;
|
|
49
55
|
resolvedModel?: string;
|
|
@@ -94,6 +100,24 @@ export function isKnownUsageSurface(value: unknown): value is NonNullable<Persis
|
|
|
94
100
|
return typeof value === "string" && KNOWN_USAGE_SURFACES.has(value as NonNullable<PersistedUsageEntry["surface"]>);
|
|
95
101
|
}
|
|
96
102
|
|
|
103
|
+
const KNOWN_ADMISSION_KINDS = new Set<NonNullable<PersistedUsageEntry["admissionKind"]>>([
|
|
104
|
+
"configured", "environment", "loopback",
|
|
105
|
+
]);
|
|
106
|
+
|
|
107
|
+
const KNOWN_INBOUND_PROTOCOLS = new Set<NonNullable<PersistedUsageEntry["inboundProtocol"]>>([
|
|
108
|
+
"responses", "chat", "messages",
|
|
109
|
+
]);
|
|
110
|
+
|
|
111
|
+
/** Same closed-set discipline as `isKnownUsageSurface`: an old or corrupted row
|
|
112
|
+
* carrying an unexpected value drops the field instead of poisoning the enum. */
|
|
113
|
+
export function isKnownAdmissionKind(value: unknown): value is NonNullable<PersistedUsageEntry["admissionKind"]> {
|
|
114
|
+
return typeof value === "string" && KNOWN_ADMISSION_KINDS.has(value as NonNullable<PersistedUsageEntry["admissionKind"]>);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export function isKnownInboundProtocol(value: unknown): value is NonNullable<PersistedUsageEntry["inboundProtocol"]> {
|
|
118
|
+
return typeof value === "string" && KNOWN_INBOUND_PROTOCOLS.has(value as NonNullable<PersistedUsageEntry["inboundProtocol"]>);
|
|
119
|
+
}
|
|
120
|
+
|
|
97
121
|
export function usageLogPath(): string {
|
|
98
122
|
return join(getConfigDir(), "usage.jsonl");
|
|
99
123
|
}
|
|
@@ -263,6 +287,11 @@ function capMetadataString(s: string): string {
|
|
|
263
287
|
return s.length > MAX_METADATA_STRING_LEN ? s.slice(0, MAX_METADATA_STRING_LEN) : s;
|
|
264
288
|
}
|
|
265
289
|
|
|
290
|
+
/** Test seam: the normalization branch old rows take is worth asserting directly. */
|
|
291
|
+
export function normalizeUsageEntryForTest(entry: PersistedUsageEntry): PersistedUsageEntry {
|
|
292
|
+
return normalizeUsageEntry(entry);
|
|
293
|
+
}
|
|
294
|
+
|
|
266
295
|
function normalizeUsageEntry(entry: PersistedUsageEntry): PersistedUsageEntry {
|
|
267
296
|
const attempts = normalizedAttempts(entry.attempts);
|
|
268
297
|
return {
|
|
@@ -271,6 +300,15 @@ function normalizeUsageEntry(entry: PersistedUsageEntry): PersistedUsageEntry {
|
|
|
271
300
|
provider: entry.provider,
|
|
272
301
|
model: entry.model,
|
|
273
302
|
...(isKnownUsageSurface(entry.surface) ? { surface: entry.surface } : {}),
|
|
303
|
+
...(typeof entry.apiKeyId === "string" && entry.apiKeyId.trim()
|
|
304
|
+
// Deliberately NOT capped. `capMetadataString` protects free-form metadata
|
|
305
|
+
// from unbounded growth, but this is a lookup key: truncating it makes the
|
|
306
|
+
// persisted id stop matching the configured one, and the rollup silently
|
|
307
|
+
// reports zero for a key that is very much in use.
|
|
308
|
+
? { apiKeyId: entry.apiKeyId }
|
|
309
|
+
: {}),
|
|
310
|
+
...(isKnownAdmissionKind(entry.admissionKind) ? { admissionKind: entry.admissionKind } : {}),
|
|
311
|
+
...(isKnownInboundProtocol(entry.inboundProtocol) ? { inboundProtocol: entry.inboundProtocol } : {}),
|
|
274
312
|
...(typeof entry.conversationId === "string" && entry.conversationId.trim()
|
|
275
313
|
? { conversationId: entry.conversationId.trim().slice(0, 128) }
|
|
276
314
|
: {}),
|
|
@@ -349,9 +387,22 @@ export type UsageLogRevision = {
|
|
|
349
387
|
};
|
|
350
388
|
|
|
351
389
|
let usageReadCacheStats = { fullReads: 0, tailReads: 0, parsedLines: 0 };
|
|
390
|
+
const MANAGEMENT_USAGE_MAX_READ_BYTES = 64 * 1024 * 1024;
|
|
391
|
+
const MANAGEMENT_USAGE_READ_CHUNK_BYTES = 1024 * 1024;
|
|
392
|
+
const MANAGEMENT_USAGE_MAX_ENTRIES = 200_000;
|
|
393
|
+
const MANAGEMENT_USAGE_FLIGHT_STALE_MS = 30_000;
|
|
394
|
+
export interface ManagementUsageSnapshot {
|
|
395
|
+
entries: PersistedUsageEntry[];
|
|
396
|
+
revision: UsageLogRevision;
|
|
397
|
+
truncatedPrefixBytes: number;
|
|
398
|
+
entriesTruncated: boolean;
|
|
399
|
+
entriesDropped: number;
|
|
400
|
+
}
|
|
352
401
|
let managementUsageReadInflight: {
|
|
353
402
|
key: string;
|
|
354
|
-
promise: Promise<
|
|
403
|
+
promise: Promise<ManagementUsageSnapshot>;
|
|
404
|
+
startedAt: number;
|
|
405
|
+
abort: AbortController;
|
|
355
406
|
} | null = null;
|
|
356
407
|
|
|
357
408
|
/** Test-only observability for proving that unchanged prefixes are not reparsed. */
|
|
@@ -361,6 +412,7 @@ export function usageReadCacheStatsForTests(): Readonly<typeof usageReadCacheSta
|
|
|
361
412
|
|
|
362
413
|
export function resetUsageReadCacheForTests(): void {
|
|
363
414
|
usageReadCacheStats = { fullReads: 0, tailReads: 0, parsedLines: 0 };
|
|
415
|
+
managementUsageReadInflight?.abort.abort();
|
|
364
416
|
managementUsageReadInflight = null;
|
|
365
417
|
}
|
|
366
418
|
|
|
@@ -413,12 +465,16 @@ export function currentUsageLogRevision(): UsageLogRevision | null {
|
|
|
413
465
|
}
|
|
414
466
|
}
|
|
415
467
|
|
|
416
|
-
async function parseUsageTextCooperatively(text: string): Promise<
|
|
468
|
+
async function parseUsageTextCooperatively(text: string, signal: AbortSignal): Promise<{
|
|
469
|
+
entries: PersistedUsageEntry[];
|
|
470
|
+
entriesDropped: number;
|
|
471
|
+
}> {
|
|
417
472
|
const lines = text.split(/\r?\n/);
|
|
418
473
|
usageReadCacheStats.parsedLines += lines.filter(line => line.trim()).length;
|
|
419
474
|
const entries: PersistedUsageEntry[] = [];
|
|
420
475
|
const batchSize = 1_000;
|
|
421
476
|
for (let offset = 0; offset < lines.length; offset += batchSize) {
|
|
477
|
+
if (signal.aborted) throw signal.reason;
|
|
422
478
|
entries.push(...parseUsageLines(lines.slice(offset, offset + batchSize)));
|
|
423
479
|
if (offset + batchSize < lines.length) {
|
|
424
480
|
// JSON parsing dominates large-log startup. Yield between bounded batches so
|
|
@@ -426,22 +482,56 @@ async function parseUsageTextCooperatively(text: string): Promise<PersistedUsage
|
|
|
426
482
|
await new Promise<void>(resolve => setTimeout(resolve, 0));
|
|
427
483
|
}
|
|
428
484
|
}
|
|
429
|
-
return entries;
|
|
485
|
+
if (entries.length <= MANAGEMENT_USAGE_MAX_ENTRIES) return { entries, entriesDropped: 0 };
|
|
486
|
+
const entriesDropped = entries.length - MANAGEMENT_USAGE_MAX_ENTRIES;
|
|
487
|
+
return { entries: entries.slice(-MANAGEMENT_USAGE_MAX_ENTRIES), entriesDropped };
|
|
430
488
|
}
|
|
431
489
|
|
|
432
490
|
async function readUsageEntriesFullCooperatively(
|
|
433
491
|
path: string,
|
|
434
|
-
|
|
492
|
+
signal: AbortSignal,
|
|
493
|
+
maxReadBytes: number,
|
|
494
|
+
): Promise<ManagementUsageSnapshot> {
|
|
435
495
|
let fd: number | undefined;
|
|
436
496
|
try {
|
|
437
497
|
fd = openSync(path, "r");
|
|
438
498
|
const stat = fstatSync(fd);
|
|
439
499
|
const size = Number(stat.size);
|
|
440
|
-
const
|
|
441
|
-
|
|
442
|
-
|
|
500
|
+
const start = Math.max(0, size - maxReadBytes);
|
|
501
|
+
const chunks: Buffer[] = [];
|
|
502
|
+
for (let position = start; position < size;) {
|
|
503
|
+
if (signal.aborted) throw signal.reason;
|
|
504
|
+
const length = Math.min(MANAGEMENT_USAGE_READ_CHUNK_BYTES, size - position);
|
|
505
|
+
const chunk = readExactly(fd, length, position);
|
|
506
|
+
if (chunk === null) throw new Error("usage log changed while it was being read");
|
|
507
|
+
chunks.push(chunk);
|
|
508
|
+
position += length;
|
|
509
|
+
}
|
|
510
|
+
let bytes = Buffer.concat(chunks);
|
|
511
|
+
let truncatedPrefixBytes = start;
|
|
512
|
+
if (start > 0) {
|
|
513
|
+
const preceding = readExactly(fd, 1, start - 1);
|
|
514
|
+
if (preceding === null) throw new Error("usage log changed while it was being read");
|
|
515
|
+
if (preceding[0] !== 0x0a) {
|
|
516
|
+
const newline = bytes.indexOf(0x0a);
|
|
517
|
+
if (newline < 0) {
|
|
518
|
+
truncatedPrefixBytes += bytes.byteLength;
|
|
519
|
+
bytes = Buffer.alloc(0);
|
|
520
|
+
} else {
|
|
521
|
+
truncatedPrefixBytes += newline + 1;
|
|
522
|
+
bytes = bytes.subarray(newline + 1);
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
const parsed = await parseUsageTextCooperatively(bytes.toString("utf-8"), signal);
|
|
443
527
|
usageReadCacheStats.fullReads += 1;
|
|
444
|
-
return {
|
|
528
|
+
return {
|
|
529
|
+
entries: parsed.entries,
|
|
530
|
+
revision: usageLogRevision(path, stat),
|
|
531
|
+
truncatedPrefixBytes,
|
|
532
|
+
entriesTruncated: parsed.entriesDropped > 0,
|
|
533
|
+
entriesDropped: parsed.entriesDropped,
|
|
534
|
+
};
|
|
445
535
|
} finally {
|
|
446
536
|
if (fd !== undefined) closeSync(fd);
|
|
447
537
|
}
|
|
@@ -452,22 +542,30 @@ async function readUsageEntriesFullCooperatively(
|
|
|
452
542
|
* callers share work only when they observed the same exact file revision. Parsed rows
|
|
453
543
|
* are returned to the request and never retained in module state.
|
|
454
544
|
*/
|
|
455
|
-
export async function readUsageSnapshotForManagement(): Promise<{
|
|
545
|
+
export async function readUsageSnapshotForManagement(maxReadBytes = MANAGEMENT_USAGE_MAX_READ_BYTES): Promise<{
|
|
456
546
|
entries: PersistedUsageEntry[];
|
|
457
547
|
revision: UsageLogRevision | null;
|
|
548
|
+
truncatedPrefixBytes: number;
|
|
549
|
+
entriesTruncated: boolean;
|
|
550
|
+
entriesDropped: number;
|
|
458
551
|
}> {
|
|
552
|
+
if (!Number.isSafeInteger(maxReadBytes) || maxReadBytes <= 0) throw new RangeError("management usage max read bytes must be positive");
|
|
459
553
|
const path = usageLogPath();
|
|
460
|
-
if (!existsSync(path)) return { entries: [], revision: null };
|
|
554
|
+
if (!existsSync(path)) return { entries: [], revision: null, truncatedPrefixBytes: 0, entriesTruncated: false, entriesDropped: 0 };
|
|
461
555
|
const observed = currentUsageLogRevision();
|
|
462
|
-
const key = usageLogRevisionKey(observed)
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
556
|
+
const key = `${usageLogRevisionKey(observed)}\0${maxReadBytes}`;
|
|
557
|
+
const existing = managementUsageReadInflight;
|
|
558
|
+
if (existing?.key === key && Date.now() - existing.startedAt <= MANAGEMENT_USAGE_FLIGHT_STALE_MS) {
|
|
559
|
+
const shared = await existing.promise;
|
|
560
|
+
return { ...shared, entries: shared.entries.slice() };
|
|
466
561
|
}
|
|
467
|
-
|
|
468
|
-
|
|
562
|
+
existing?.abort.abort(new Error("management usage read superseded"));
|
|
563
|
+
const abort = new AbortController();
|
|
564
|
+
const promise = readUsageEntriesFullCooperatively(path, abort.signal, maxReadBytes);
|
|
565
|
+
managementUsageReadInflight = { key, promise, startedAt: Date.now(), abort };
|
|
469
566
|
try {
|
|
470
|
-
|
|
567
|
+
const snapshot = await promise;
|
|
568
|
+
return { ...snapshot, entries: snapshot.entries.slice() };
|
|
471
569
|
} finally {
|
|
472
570
|
if (managementUsageReadInflight?.promise === promise) managementUsageReadInflight = null;
|
|
473
571
|
}
|
package/src/usage/summary.ts
CHANGED
|
@@ -91,6 +91,17 @@ export interface UsageSummary {
|
|
|
91
91
|
}
|
|
92
92
|
|
|
93
93
|
const DAY_MS = 86_400_000;
|
|
94
|
+
export const MAX_USAGE_MODEL_BREAKDOWN_ROWS = 256;
|
|
95
|
+
|
|
96
|
+
function retainedBreakdownRows<T>(
|
|
97
|
+
rows: T[],
|
|
98
|
+
aggregateOverflow: (overflow: T[]) => T,
|
|
99
|
+
): T[] {
|
|
100
|
+
if (rows.length <= MAX_USAGE_MODEL_BREAKDOWN_ROWS) return rows;
|
|
101
|
+
const keep = rows.slice(0, MAX_USAGE_MODEL_BREAKDOWN_ROWS - 1);
|
|
102
|
+
keep.push(aggregateOverflow(rows.slice(MAX_USAGE_MODEL_BREAKDOWN_ROWS - 1)));
|
|
103
|
+
return keep;
|
|
104
|
+
}
|
|
94
105
|
|
|
95
106
|
export function parseRange(input: string | null | undefined): UsageRange {
|
|
96
107
|
if (input === "7d" || input === "30d" || input === "all") return input;
|
|
@@ -334,7 +345,21 @@ function buildDayGrid(range: UsageRange, since: number | null, now: number, entr
|
|
|
334
345
|
const out = [...grid.values()].sort((a, b) => a.date.localeCompare(b.date));
|
|
335
346
|
for (const day of out) {
|
|
336
347
|
const models = dayModels.get(day.date);
|
|
337
|
-
if (models)
|
|
348
|
+
if (models) {
|
|
349
|
+
const sorted = [...models.values()].sort((a, b) => b.requests - a.requests);
|
|
350
|
+
day.models = retainedBreakdownRows(sorted, overflow => {
|
|
351
|
+
const requests = new Set<string>();
|
|
352
|
+
let attemptCount = 0;
|
|
353
|
+
let totalTokens = 0;
|
|
354
|
+
for (const model of overflow) {
|
|
355
|
+
attemptCount += model.attemptCount;
|
|
356
|
+
totalTokens += model.totalTokens;
|
|
357
|
+
const requestKey = `${day.date}\0${usageModelKey(model.provider, model.model)}`;
|
|
358
|
+
for (const requestId of dayModelRequests.get(requestKey) ?? []) requests.add(requestId);
|
|
359
|
+
}
|
|
360
|
+
return { model: "other", provider: "other", requests: requests.size, attemptCount, totalTokens };
|
|
361
|
+
});
|
|
362
|
+
}
|
|
338
363
|
}
|
|
339
364
|
return out;
|
|
340
365
|
}
|
|
@@ -414,7 +439,47 @@ function buildModels(entries: PersistedUsageEntry[], totalTokens: number): Usage
|
|
|
414
439
|
}
|
|
415
440
|
const models = [...byKey.values()];
|
|
416
441
|
for (const m of models) m.shareRatio = totalTokens === 0 ? 0 : m.totalTokens / totalTokens;
|
|
417
|
-
|
|
442
|
+
const sorted = models.sort((a, b) => b.requests - a.requests);
|
|
443
|
+
return retainedBreakdownRows(sorted, overflow => {
|
|
444
|
+
const statusesByRequest = new Map<string, UsageStatus[]>();
|
|
445
|
+
const other: UsageModel = {
|
|
446
|
+
provider: "other",
|
|
447
|
+
model: "other",
|
|
448
|
+
requests: 0,
|
|
449
|
+
attemptCount: 0,
|
|
450
|
+
measuredRequests: 0,
|
|
451
|
+
reportedRequests: 0,
|
|
452
|
+
estimatedRequests: 0,
|
|
453
|
+
totalTokens: 0,
|
|
454
|
+
inputTokens: 0,
|
|
455
|
+
outputTokens: 0,
|
|
456
|
+
shareRatio: 0,
|
|
457
|
+
};
|
|
458
|
+
for (const model of overflow) {
|
|
459
|
+
other.attemptCount += model.attemptCount;
|
|
460
|
+
other.totalTokens += model.totalTokens;
|
|
461
|
+
other.inputTokens += model.inputTokens;
|
|
462
|
+
other.outputTokens += model.outputTokens;
|
|
463
|
+
if (model.estimatedCostUsd !== undefined) {
|
|
464
|
+
other.estimatedCostUsd = (other.estimatedCostUsd ?? 0) + model.estimatedCostUsd;
|
|
465
|
+
}
|
|
466
|
+
const key = usageModelKey(model.provider, model.model);
|
|
467
|
+
for (const [requestId, statuses] of statusesByKey.get(key) ?? []) {
|
|
468
|
+
const combined = statusesByRequest.get(requestId) ?? [];
|
|
469
|
+
combined.push(...statuses);
|
|
470
|
+
statusesByRequest.set(requestId, combined);
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
other.requests = statusesByRequest.size;
|
|
474
|
+
for (const statuses of statusesByRequest.values()) {
|
|
475
|
+
const status = foldAttributionStatuses(statuses);
|
|
476
|
+
if (isMeasuredStatus(status)) other.measuredRequests += 1;
|
|
477
|
+
if (status === "reported") other.reportedRequests += 1;
|
|
478
|
+
else if (status === "estimated") other.estimatedRequests += 1;
|
|
479
|
+
}
|
|
480
|
+
other.shareRatio = totalTokens === 0 ? 0 : other.totalTokens / totalTokens;
|
|
481
|
+
return other;
|
|
482
|
+
});
|
|
418
483
|
}
|
|
419
484
|
|
|
420
485
|
function buildProviders(entries: PersistedUsageEntry[], totalTokens: number): UsageProvider[] {
|
package/src/vision/index.ts
CHANGED
|
@@ -7,6 +7,8 @@ import type { CodexAuthContext } from "../codex/auth-context";
|
|
|
7
7
|
import { getAccountSet } from "../oauth/store";
|
|
8
8
|
import type { ResolvedOpenAiForwardSidecar } from "../providers/openai-sidecar";
|
|
9
9
|
import type { SidecarOutcomeRecorder } from "../web-search/executor";
|
|
10
|
+
import { enforceAppOwnedMemoryBudget } from "../lib/app-owned-memory";
|
|
11
|
+
import type { TranslatorBudget } from "../lib/translator-budget";
|
|
10
12
|
|
|
11
13
|
export { describeImage } from "./describe";
|
|
12
14
|
export { describeImageAnthropic, parseAnthropicVisionSSE } from "./anthropic-describe";
|
|
@@ -16,6 +18,8 @@ const DEFAULT_ANTHROPIC_VISION_MODEL = "claude-sonnet-5";
|
|
|
16
18
|
const DEFAULT_TIMEOUT_MS = 45_000;
|
|
17
19
|
const DEFAULT_MAX_DESCRIPTIONS_PER_TURN = 8;
|
|
18
20
|
const DESCRIPTION_CACHE_MAX_ENTRIES = 256;
|
|
21
|
+
export const VISION_DESCRIPTION_CACHE_MAX_BYTES = 1024 * 1024;
|
|
22
|
+
const descriptionEncoder = new TextEncoder();
|
|
19
23
|
/** Max images described in parallel — keeps first-token latency bounded without flooding the backend. */
|
|
20
24
|
const VISION_CONCURRENCY = 3;
|
|
21
25
|
/** Per-image description hard cap (chars) so multi-image turns can't blow the main model's context. */
|
|
@@ -27,47 +31,109 @@ export interface VisionDescriptionCache {
|
|
|
27
31
|
get(key: string): string | undefined;
|
|
28
32
|
set(key: string, value: string): void;
|
|
29
33
|
clear(): void;
|
|
34
|
+
snapshot?(): { count: number; bytes: number; oldestAt: number | null };
|
|
35
|
+
evictOldest?(): number;
|
|
30
36
|
}
|
|
31
37
|
|
|
32
38
|
class BoundedLruDescriptionCache implements VisionDescriptionCache {
|
|
33
|
-
private readonly entries = new Map<string, string>();
|
|
39
|
+
private readonly entries = new Map<string, { value: string; sizeBytes: number; storedAt: number }>();
|
|
40
|
+
private bytes = 0;
|
|
34
41
|
|
|
35
|
-
constructor(private readonly maxEntries: number) {}
|
|
42
|
+
constructor(private readonly maxEntries: number, private readonly maxBytes: number) {}
|
|
36
43
|
|
|
37
44
|
get(key: string): string | undefined {
|
|
38
|
-
const
|
|
39
|
-
if (
|
|
45
|
+
const entry = this.entries.get(key);
|
|
46
|
+
if (entry === undefined) return undefined;
|
|
40
47
|
this.entries.delete(key);
|
|
41
|
-
|
|
42
|
-
|
|
48
|
+
entry.storedAt = Date.now();
|
|
49
|
+
this.entries.set(key, entry);
|
|
50
|
+
return entry.value;
|
|
43
51
|
}
|
|
44
52
|
|
|
45
53
|
set(key: string, value: string): void {
|
|
46
|
-
this.entries.
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
54
|
+
const existing = this.entries.get(key);
|
|
55
|
+
if (existing) {
|
|
56
|
+
this.entries.delete(key);
|
|
57
|
+
this.bytes -= existing.sizeBytes;
|
|
58
|
+
}
|
|
59
|
+
const sizeBytes = descriptionEncoder.encode(key).byteLength + descriptionEncoder.encode(value).byteLength;
|
|
60
|
+
if (sizeBytes > this.maxBytes || this.maxEntries <= 0) return;
|
|
61
|
+
while (this.entries.size + 1 > this.maxEntries || this.bytes + sizeBytes > this.maxBytes) {
|
|
62
|
+
if (this.evictOldest() === 0) return;
|
|
52
63
|
}
|
|
64
|
+
this.entries.set(key, { value, sizeBytes, storedAt: Date.now() });
|
|
65
|
+
this.bytes += sizeBytes;
|
|
53
66
|
}
|
|
54
67
|
|
|
55
68
|
clear(): void {
|
|
56
69
|
this.entries.clear();
|
|
70
|
+
this.bytes = 0;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
snapshot(): { count: number; bytes: number; oldestAt: number | null } {
|
|
74
|
+
return {
|
|
75
|
+
count: this.entries.size,
|
|
76
|
+
bytes: this.bytes,
|
|
77
|
+
oldestAt: this.entries.values().next().value?.storedAt ?? null,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
evictOldest(): number {
|
|
82
|
+
const oldest = this.entries.keys().next().value;
|
|
83
|
+
if (oldest === undefined) return 0;
|
|
84
|
+
const entry = this.entries.get(oldest);
|
|
85
|
+
if (!entry) return 0;
|
|
86
|
+
this.entries.delete(oldest);
|
|
87
|
+
this.bytes -= entry.sizeBytes;
|
|
88
|
+
return entry.sizeBytes;
|
|
57
89
|
}
|
|
58
90
|
}
|
|
59
91
|
|
|
60
|
-
let
|
|
92
|
+
let descriptionCacheLimits = {
|
|
93
|
+
maxEntries: DESCRIPTION_CACHE_MAX_ENTRIES,
|
|
94
|
+
maxBytes: VISION_DESCRIPTION_CACHE_MAX_BYTES,
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
function defaultDescriptionCache(): VisionDescriptionCache {
|
|
98
|
+
return new BoundedLruDescriptionCache(descriptionCacheLimits.maxEntries, descriptionCacheLimits.maxBytes);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
let descriptionCache: VisionDescriptionCache = defaultDescriptionCache();
|
|
61
102
|
|
|
62
103
|
/** Replace the process cache (primarily for deterministic tests). Passing undefined restores the default LRU. */
|
|
63
104
|
export function setVisionDescriptionCache(cache?: VisionDescriptionCache): void {
|
|
64
|
-
descriptionCache = cache ??
|
|
105
|
+
descriptionCache = cache ?? defaultDescriptionCache();
|
|
65
106
|
}
|
|
66
107
|
|
|
67
108
|
export function resetVisionDescriptionCache(): void {
|
|
68
109
|
descriptionCache.clear();
|
|
69
110
|
}
|
|
70
111
|
|
|
112
|
+
export function setVisionDescriptionCacheLimitsForTests(
|
|
113
|
+
limits?: { maxEntries?: number; maxBytes?: number },
|
|
114
|
+
): void {
|
|
115
|
+
descriptionCacheLimits = limits
|
|
116
|
+
? { maxEntries: limits.maxEntries ?? DESCRIPTION_CACHE_MAX_ENTRIES, maxBytes: limits.maxBytes ?? VISION_DESCRIPTION_CACHE_MAX_BYTES }
|
|
117
|
+
: { maxEntries: DESCRIPTION_CACHE_MAX_ENTRIES, maxBytes: VISION_DESCRIPTION_CACHE_MAX_BYTES };
|
|
118
|
+
descriptionCache = defaultDescriptionCache();
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export function visionDescriptionRetainedStoreSnapshot(): {
|
|
122
|
+
count: number;
|
|
123
|
+
bytes: number;
|
|
124
|
+
evictableBytes: number;
|
|
125
|
+
pinnedBytes: number;
|
|
126
|
+
oldestAt: number | null;
|
|
127
|
+
} {
|
|
128
|
+
const snapshot = descriptionCache.snapshot?.();
|
|
129
|
+
if (!snapshot) return { count: 0, bytes: 0, evictableBytes: 0, pinnedBytes: 0, oldestAt: null };
|
|
130
|
+
return { ...snapshot, evictableBytes: snapshot.bytes, pinnedBytes: 0 };
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export function evictOldestVisionDescriptionForBudget(): number {
|
|
134
|
+
return descriptionCache.evictOldest?.() ?? 0;
|
|
135
|
+
}
|
|
136
|
+
|
|
71
137
|
/** Runtime config is permissive: zero is intentional; malformed values fall back to the bounded default. */
|
|
72
138
|
export function resolveMaxDescriptionsPerTurn(value: unknown): number {
|
|
73
139
|
if (value === 0) return 0;
|
|
@@ -279,6 +345,7 @@ export async function describeImagesInPlace(
|
|
|
279
345
|
selectedForwardHeaders: Headers,
|
|
280
346
|
abortSignal?: AbortSignal,
|
|
281
347
|
recordSidecarOutcome?: SidecarOutcomeRecorder,
|
|
348
|
+
translatorBudget?: TranslatorBudget,
|
|
282
349
|
): Promise<void> {
|
|
283
350
|
// 1. Gather every image part across messages, each with its own message's text as context.
|
|
284
351
|
const jobs: ImageJob[] = [];
|
|
@@ -338,9 +405,13 @@ export async function describeImagesInPlace(
|
|
|
338
405
|
} catch (error) {
|
|
339
406
|
outcome = { text: "", error: error instanceof Error ? error.message : String(error) };
|
|
340
407
|
}
|
|
341
|
-
const successfulText = outcome.error ? "" : outcome.text.trim();
|
|
342
|
-
if (identity.persistent && successfulText)
|
|
343
|
-
|
|
408
|
+
const successfulText = outcome.error ? "" : clamp(outcome.text.trim(), DESC_MAX_CHARS);
|
|
409
|
+
if (identity.persistent && successfulText) {
|
|
410
|
+
descriptionCache.set(identity.key, successfulText);
|
|
411
|
+
enforceAppOwnedMemoryBudget();
|
|
412
|
+
}
|
|
413
|
+
const resolvedOutcome = outcome.error ? outcome : { ...outcome, text: successfulText };
|
|
414
|
+
resolveOutcome(resolvedOutcome);
|
|
344
415
|
});
|
|
345
416
|
}
|
|
346
417
|
|
|
@@ -351,7 +422,19 @@ export async function describeImagesInPlace(
|
|
|
351
422
|
let oi = 0;
|
|
352
423
|
for (const { msg, parts } of targets) {
|
|
353
424
|
const newParts: OcxContentPart[] = [];
|
|
354
|
-
for (const p of parts)
|
|
425
|
+
for (const p of parts) {
|
|
426
|
+
if (p.type !== "image") {
|
|
427
|
+
newParts.push(p);
|
|
428
|
+
continue;
|
|
429
|
+
}
|
|
430
|
+
const replacement = renderDescription(outcomes[oi++]);
|
|
431
|
+
const reservation = translatorBudget?.reserveTransient(
|
|
432
|
+
descriptionEncoder.encode(replacement.text).byteLength,
|
|
433
|
+
{ kind: "request_copies" },
|
|
434
|
+
);
|
|
435
|
+
newParts.push(replacement);
|
|
436
|
+
reservation?.commitRetained();
|
|
437
|
+
}
|
|
355
438
|
msg.content = newParts;
|
|
356
439
|
}
|
|
357
440
|
}
|
|
@@ -362,15 +445,22 @@ export async function describeImagesInPlace(
|
|
|
362
445
|
* raw images would 400 or silently confuse it. Replace each image with an explicit marker so the
|
|
363
446
|
* model (and the user, via its reply) knows the image was dropped rather than ignored.
|
|
364
447
|
*/
|
|
365
|
-
export function stripImagesInPlace(parsed: OcxParsedRequest): boolean {
|
|
448
|
+
export function stripImagesInPlace(parsed: OcxParsedRequest, translatorBudget?: TranslatorBudget): boolean {
|
|
366
449
|
let stripped = false;
|
|
367
450
|
for (const msg of parsed.context.messages) {
|
|
368
451
|
if (!carriesImages(msg.role) || !Array.isArray(msg.content)) continue;
|
|
369
452
|
const parts = msg.content as OcxContentPart[];
|
|
370
453
|
if (!parts.some(p => p.type === "image")) continue;
|
|
371
|
-
msg.content = parts.map(p =>
|
|
372
|
-
|
|
373
|
-
:
|
|
454
|
+
msg.content = parts.map(p => {
|
|
455
|
+
if (p.type !== "image") return p;
|
|
456
|
+
const replacement = { type: "text", text: "[image omitted: this model is text-only and the vision sidecar is unavailable (no ChatGPT login)]" } as OcxContentPart;
|
|
457
|
+
const reservation = translatorBudget?.reserveTransient(
|
|
458
|
+
descriptionEncoder.encode((replacement as OcxTextContent).text).byteLength,
|
|
459
|
+
{ kind: "request_copies" },
|
|
460
|
+
);
|
|
461
|
+
reservation?.commitRetained();
|
|
462
|
+
return replacement;
|
|
463
|
+
});
|
|
374
464
|
stripped = true;
|
|
375
465
|
}
|
|
376
466
|
return stripped;
|
package/src/web-search/loop.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { AdapterRequest, ProviderAdapter } from "../adapters/base";
|
|
1
|
+
import type { AdapterRequest, IncomingMeta, ProviderAdapter } from "../adapters/base";
|
|
2
2
|
import type { AdapterEvent, OcxMessage, OcxParsedRequest, OcxProviderConfig, OcxThinkingContent, OcxUsage } from "../types";
|
|
3
3
|
import { namespacedToolName } from "../types";
|
|
4
4
|
import { bridgeToResponsesSSE } from "../bridge";
|
|
@@ -8,6 +8,11 @@ import { clearableDeadline } from "../lib/abort";
|
|
|
8
8
|
import { redactSecretString } from "../lib/redact";
|
|
9
9
|
import { readBoundedResponseBody } from "../lib/bounded-body";
|
|
10
10
|
import { fetchWithResetRetry } from "../lib/upstream-retry";
|
|
11
|
+
import {
|
|
12
|
+
isTranslatorBudgetExceededError,
|
|
13
|
+
TRANSLATOR_MAX_TURN_BYTES,
|
|
14
|
+
TranslatorBudgetExceededError,
|
|
15
|
+
} from "../lib/translator-budget";
|
|
11
16
|
import { formatWebSearchResults } from "./format-result";
|
|
12
17
|
import { parseStreamWithProgress, RoutedModelInactivityError, WebSearchStreamProtocolError } from "./progress-stream";
|
|
13
18
|
import { WEB_SEARCH_TOOL_NAME } from "./synthetic-tool";
|
|
@@ -204,6 +209,7 @@ class LoopError extends Error {
|
|
|
204
209
|
export interface WebSearchLoopDeps {
|
|
205
210
|
parsed: OcxParsedRequest;
|
|
206
211
|
adapter: ProviderAdapter;
|
|
212
|
+
incomingMeta: IncomingMeta;
|
|
207
213
|
/** Which executor runs searches. Defaults to "openai" so existing callers keep the ChatGPT path (audit F4). */
|
|
208
214
|
backend?: "openai" | "anthropic";
|
|
209
215
|
/** Required for the openai backend; unused (and typically undefined) for the anthropic backend. */
|
|
@@ -247,6 +253,7 @@ export interface WebSearchLoopDeps {
|
|
|
247
253
|
* inject the answer as a tool_result, and loop (bounded by `maxSearches`).
|
|
248
254
|
*/
|
|
249
255
|
export async function runWithWebSearch(deps: WebSearchLoopDeps): Promise<Response> {
|
|
256
|
+
const translatorBudget = deps.incomingMeta.translatorBudget;
|
|
250
257
|
const { parsed, selectedForwardHeaders, forwardProvider, hostedTool, settings, maxSearches, abortSignal, recordSidecarOutcome } = deps;
|
|
251
258
|
const backend = deps.backend ?? "openai";
|
|
252
259
|
const anthropicSidecar = deps.anthropicSidecar;
|
|
@@ -312,14 +319,17 @@ export async function runWithWebSearch(deps: WebSearchLoopDeps): Promise<Respons
|
|
|
312
319
|
const request = await requestAdapter.buildRequest(iterParsed, {
|
|
313
320
|
headers: selectedForwardHeaders,
|
|
314
321
|
abortSignal: headerDeadline.signal,
|
|
322
|
+
translatorBudget,
|
|
315
323
|
});
|
|
316
324
|
try {
|
|
317
325
|
deps.onRequestBuilt?.(request);
|
|
318
326
|
} catch {
|
|
319
327
|
// Diagnostics are best-effort and must never abort a web-search iteration.
|
|
320
328
|
}
|
|
321
|
-
|
|
322
|
-
|
|
329
|
+
let response: Response;
|
|
330
|
+
try {
|
|
331
|
+
response = requestAdapter.fetchResponse
|
|
332
|
+
? await requestAdapter.fetchResponse(request, {
|
|
323
333
|
abortSignal: headerDeadline.signal,
|
|
324
334
|
timeoutMs: connectTimeoutMs,
|
|
325
335
|
returnRawErrors: true,
|
|
@@ -337,7 +347,10 @@ export async function runWithWebSearch(deps: WebSearchLoopDeps): Promise<Respons
|
|
|
337
347
|
});
|
|
338
348
|
},
|
|
339
349
|
{ abortSignal: headerDeadline.signal, label: "web-search-loop" },
|
|
340
|
-
|
|
350
|
+
);
|
|
351
|
+
} finally {
|
|
352
|
+
request.releaseBodyObservation?.();
|
|
353
|
+
}
|
|
341
354
|
return { response, responseAdapter: requestAdapter };
|
|
342
355
|
};
|
|
343
356
|
|
|
@@ -385,6 +398,7 @@ export async function runWithWebSearch(deps: WebSearchLoopDeps): Promise<Respons
|
|
|
385
398
|
}
|
|
386
399
|
return prepared;
|
|
387
400
|
} catch (error) {
|
|
401
|
+
if (isTranslatorBudgetExceededError(error)) throw error;
|
|
388
402
|
if (headerDeadline.didExpire()) {
|
|
389
403
|
throw new LoopError(504, `Provider response-header timeout after ${connectTimeoutMs}ms during web-search`);
|
|
390
404
|
}
|
|
@@ -412,6 +426,7 @@ export async function runWithWebSearch(deps: WebSearchLoopDeps): Promise<Respons
|
|
|
412
426
|
for await (const event of parseStreamWithProgress(prepared.response, parse, {
|
|
413
427
|
signal,
|
|
414
428
|
inactivityTimeoutMs: routedModelStallTimeoutMs,
|
|
429
|
+
translatorBudget,
|
|
415
430
|
})) {
|
|
416
431
|
if (event.type === "heartbeat") yield event;
|
|
417
432
|
// Kiro's explicit-completion protocol marks ordinary assistant text as commentary while
|
|
@@ -424,6 +439,7 @@ export async function runWithWebSearch(deps: WebSearchLoopDeps): Promise<Respons
|
|
|
424
439
|
else events.push(event);
|
|
425
440
|
}
|
|
426
441
|
} catch (error) {
|
|
442
|
+
if (isTranslatorBudgetExceededError(error)) throw error;
|
|
427
443
|
if (signal.aborted) throw new LoopError(499, "client closed request during web-search");
|
|
428
444
|
if (error instanceof RoutedModelInactivityError) throw new LoopError(504, error.message);
|
|
429
445
|
if (error instanceof WebSearchStreamProtocolError) throw new LoopError(502, error.message);
|
|
@@ -436,7 +452,12 @@ export async function runWithWebSearch(deps: WebSearchLoopDeps): Promise<Respons
|
|
|
436
452
|
throw new LoopError(502, `Web-search adapter stream protocol error: expected one final terminal event, received ${terminalIndexes.length}`);
|
|
437
453
|
}
|
|
438
454
|
const terminal = events[terminalIndexes[0]!];
|
|
439
|
-
if (terminal.type === "error")
|
|
455
|
+
if (terminal.type === "error") {
|
|
456
|
+
if (terminal.code === "translation_buffer_limit") {
|
|
457
|
+
throw new TranslatorBudgetExceededError("retained_collectors", TRANSLATOR_MAX_TURN_BYTES);
|
|
458
|
+
}
|
|
459
|
+
throw new LoopError(502, terminal.message);
|
|
460
|
+
}
|
|
440
461
|
return scanEventsForWebSearch(events);
|
|
441
462
|
};
|
|
442
463
|
|
|
@@ -603,7 +624,17 @@ export async function runWithWebSearch(deps: WebSearchLoopDeps): Promise<Respons
|
|
|
603
624
|
yield* runSearchCall(call, callIndex === 0 ? iterationThinking : []);
|
|
604
625
|
}
|
|
605
626
|
} catch (e) {
|
|
606
|
-
|
|
627
|
+
if (isTranslatorBudgetExceededError(e)) {
|
|
628
|
+
yield {
|
|
629
|
+
type: "error",
|
|
630
|
+
status: 502,
|
|
631
|
+
errorType: "upstream_error",
|
|
632
|
+
code: e.code,
|
|
633
|
+
message: "upstream translation buffer exceeded the safe limit",
|
|
634
|
+
};
|
|
635
|
+
} else {
|
|
636
|
+
yield { type: "error", message: e instanceof LoopError ? e.message : (e instanceof Error ? e.message : String(e)) };
|
|
637
|
+
}
|
|
607
638
|
return;
|
|
608
639
|
}
|
|
609
640
|
}
|
|
@@ -621,6 +652,7 @@ export async function runWithWebSearch(deps: WebSearchLoopDeps): Promise<Respons
|
|
|
621
652
|
internalAbort.abort("client closed responses stream");
|
|
622
653
|
}, undefined,
|
|
623
654
|
{
|
|
655
|
+
translatorBudget,
|
|
624
656
|
...(deps.forceEmptyResponseId ? { responseId: "" } : {}),
|
|
625
657
|
hideThinkingSummary: parsed.options.hideThinkingSummary,
|
|
626
658
|
...(deps.stallTimeoutSec !== undefined ? { stallTimeoutSec: deps.stallTimeoutSec } : {}),
|