dsh-lcx-codex 0.4.2 → 0.4.3-pre.13

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.
Files changed (39) hide show
  1. package/README.md +75 -224
  2. package/THIRD_PARTY_NOTICES.md +64 -0
  3. package/cordis.patch.yml +3 -20
  4. package/lib/auxiliary-usage.js +63 -0
  5. package/lib/client.js +1398 -167
  6. package/lib/compact-v2.js +218 -199
  7. package/lib/dsh-compat.js +294 -100
  8. package/lib/dsh-responses.js +512 -277
  9. package/lib/grok-native-search.js +391 -0
  10. package/lib/index.js +1066 -758
  11. package/lib/invocation-policy-scope.js +261 -0
  12. package/lib/json-store.js +57 -31
  13. package/lib/native-checkpoint.js +520 -194
  14. package/lib/pi-responses-runtime.js +1571 -0
  15. package/lib/responses-request.js +109 -121
  16. package/lib/responses-stream.js +1280 -447
  17. package/lib/route.js +425 -369
  18. package/lib/search-accounting.js +86 -0
  19. package/lib/search-usage.js +86 -0
  20. package/lib/service-mutex.js +73 -64
  21. package/lib/token-budget.js +176 -108
  22. package/lib/transport.js +308 -68
  23. package/lib/types/client/index.d.ts +18 -0
  24. package/lib/types/client/search-media.d.ts +16 -0
  25. package/lib/types/index.d.ts +83 -0
  26. package/lib/web-run-output.js +189 -18
  27. package/lib/web-search-alpha.js +1067 -163
  28. package/lib/web-search-capability.js +80 -65
  29. package/lib/web-search-hosted.js +321 -33
  30. package/lib/web-search-ref-store.js +145 -60
  31. package/package.json +112 -32
  32. package/ARCHITECTURE.md +0 -117
  33. package/CHANGELOG.md +0 -224
  34. package/README_EN.md +0 -277
  35. package/assets/dsh-lcx-codex-banner.jpg +0 -0
  36. package/lib/legacy-v3.js +0 -20
  37. package/lib/responses-replay.js +0 -68
  38. package/scripts/probe-alpha.mjs +0 -43
  39. package/scripts/validate-dsh-schema.mjs +0 -31
@@ -0,0 +1,86 @@
1
+ export const object = (v) => typeof v === 'object' && v !== null && !Array.isArray(v);
2
+ const count = (v) => typeof v === 'number' && Number.isSafeInteger(v) && v >= 0;
3
+ export const zeroBuckets = () => ({ uncachedInputTokens: 0, outputTokens: 0, cacheReadTokens: 0, cacheWriteTokens: 0 });
4
+ export const isSearchTool = (name) => name === 'web_search' || name === 'websearch_gpt_advanced';
5
+ export function auxiliaryUsageOf(event, toolName) {
6
+ if (!object(event) || event.type !== 'tool/result' || !object(event.data?.meta))
7
+ return [];
8
+ // Only our owned search results may contribute auxiliary billing.
9
+ // Official ToolMessageSource carries only callId; resolve its name from tool/call.
10
+ if (!isSearchTool(toolName))
11
+ return [];
12
+ const raw = event.data.meta.auxiliaryUsage;
13
+ if (!Array.isArray(raw))
14
+ return [];
15
+ const ids = new Set(), result = [];
16
+ for (const item of raw) {
17
+ if (!object(item) || typeof item.requestId !== 'string' || !item.requestId
18
+ || typeof item.provider !== 'string' || !item.provider || typeof item.model !== 'string' || !item.model
19
+ || !object(item.usage) || ids.has(item.requestId))
20
+ continue;
21
+ const u = item.usage;
22
+ if (![u.inputTokens, u.outputTokens, u.totalTokens, u.cacheReadTokens, u.cacheWriteTokens].every(count)
23
+ || u.inputTokens + u.outputTokens + u.cacheReadTokens + u.cacheWriteTokens !== u.totalTokens)
24
+ continue;
25
+ ids.add(item.requestId);
26
+ result.push(item);
27
+ }
28
+ return result;
29
+ }
30
+ export function addUsage(base, records) {
31
+ if (!records.length)
32
+ return base;
33
+ const next = { ...base };
34
+ for (const { usage: u } of records) {
35
+ next.uncachedInputTokens += u.inputTokens;
36
+ next.outputTokens += u.outputTokens;
37
+ next.cacheReadTokens += u.cacheReadTokens;
38
+ next.cacheWriteTokens += u.cacheWriteTokens;
39
+ }
40
+ if (!Object.values(next).every(count))
41
+ throw new Error('LCX search usage exceeds safe counters');
42
+ return next;
43
+ }
44
+ export function mergeBuckets(base, extra) {
45
+ if (!object(base) || Object.values(extra).every(n => n === 0))
46
+ return base;
47
+ const next = { ...base };
48
+ for (const key of Object.keys(extra)) {
49
+ // Keep unavailable host buckets unavailable rather than inventing zero.
50
+ if (count(base[key]))
51
+ next[key] = base[key] + extra[key];
52
+ }
53
+ return next;
54
+ }
55
+ export function addTurnUsage(base, records) {
56
+ if (!object(base) || !records.length || !count(base.totalTokens))
57
+ return base;
58
+ const next = mergeBuckets(base, addUsage(zeroBuckets(), records));
59
+ next.totalTokens = base.totalTokens + records.reduce((n, r) => n + r.usage.totalTokens, 0);
60
+ // Auxiliary responses do not always disclose a reasoning subset.
61
+ delete next.reasoningTokens;
62
+ if (Array.isArray(base.routes)) {
63
+ const routes = new Map();
64
+ for (const r of [...base.routes, ...records])
65
+ routes.set(`${r.provider}\0${r.model}`, { provider: r.provider, model: r.model });
66
+ next.routes = [...routes.values()];
67
+ }
68
+ return next;
69
+ }
70
+ /** The metadata is presentation/accounting state; it never changes request messages. */
71
+ export function aggregateContextOf(event) {
72
+ if (!object(event) || !['assistant/message', 'assistant/attempt'].includes(event.type) || !object(event.data))
73
+ return;
74
+ const chunks = Array.isArray(event.data.stream) ? event.data.stream.filter((e) => e.type === 'chunk').map((e) => e.chunk) : [];
75
+ const sample = event.data.usage ?? chunks.findLast((c) => c?.type === 'usage')?.usage;
76
+ if (!object(sample))
77
+ return;
78
+ const replay = chunks.findLast((c) => c?.type === 'finish')?.replayState;
79
+ const mark = replay?.response?.lcxUsage;
80
+ if (mark?.version === 1 && ['request', 'aggregate'].includes(mark.inputTokenScope))
81
+ return mark.inputTokenScope === 'aggregate';
82
+ // Read the already-installed local candidate without rewriting its logs.
83
+ if (sample.inputTokenScope === 'aggregate' || sample.inputTokenScope === 'request')
84
+ return sample.inputTokenScope === 'aggregate';
85
+ return replay?.grokNative?.kind === 'xai-responses-native-search' && replay.grokNative.version === 3;
86
+ }
@@ -0,0 +1,86 @@
1
+ import { z } from 'zod';
2
+ import { SessionSeq } from '@deepseek-ai/dsh-session';
3
+ import { addUsage, aggregateContextOf, auxiliaryUsageOf, isSearchTool, zeroBuckets } from './search-accounting.js';
4
+ const n = z.number().int().nonnegative();
5
+ const schema = z.object({ auxiliary: z.object({ uncachedInputTokens: n, outputTokens: n, cacheReadTokens: n, cacheWriteTokens: n }).strict(), aggregateContext: z.boolean() }).strict();
6
+ /** Separate, durable extension: the host's own usage projection remains primary-call billing. */
7
+ export const searchUsageProjection = {
8
+ key: 'lcxSearchUsage', stateVersion: 2, stateSchema: schema.extend({ pending: z.record(z.string(), z.string()) }),
9
+ init: () => ({ auxiliary: zeroBuckets(), aggregateContext: false, pending: {} }),
10
+ apply(state, event) {
11
+ if (event.type === 'tool/call' && isSearchTool(event.data.name))
12
+ return { ...state, pending: { ...state.pending, [event.data.callId]: event.data.name } };
13
+ const callId = event.type === 'tool/result' ? event.data.message.source.callId : undefined;
14
+ const auxiliary = addUsage(state.auxiliary, auxiliaryUsageOf(event, callId ? state.pending[callId] : undefined));
15
+ const aggregateContext = aggregateContextOf(event) ?? state.aggregateContext;
16
+ if (callId && Object.hasOwn(state.pending, callId)) {
17
+ const pending = { ...state.pending };
18
+ delete pending[callId];
19
+ return { auxiliary, aggregateContext, pending };
20
+ }
21
+ if (event.type === 'turn/end' && Object.keys(state.pending).length)
22
+ return { auxiliary, aggregateContext, pending: {} };
23
+ return auxiliary === state.auxiliary && aggregateContext === state.aggregateContext ? state : { ...state, auxiliary, aggregateContext };
24
+ },
25
+ wire: { viewSchema: schema, view: ({ auxiliary, aggregateContext }) => ({ auxiliary, aggregateContext }) },
26
+ };
27
+ const installed = new WeakMap();
28
+ /** Own a reversible adapter on the public measure method, never a DSH file or private fold. */
29
+ export function installSearchMeasurement(meter) {
30
+ const existing = installed.get(meter);
31
+ if (existing) {
32
+ existing.refs++;
33
+ let active = true;
34
+ return () => { if (active) {
35
+ active = false;
36
+ release(meter);
37
+ } };
38
+ }
39
+ const original = meter.measure, descriptor = Object.getOwnPropertyDescriptor(meter, 'measure');
40
+ const cursors = new WeakMap();
41
+ function measure(session, header) {
42
+ const value = original.call(this, session, header);
43
+ let state = cursors.get(session) ?? { seq: 0, aggregate: false };
44
+ while (state.seq < session.seq) {
45
+ const e = session.eventAt(SessionSeq(state.seq++));
46
+ if (e?.type === 'assistant/message')
47
+ state.aggregate = aggregateContextOf(e) ?? false;
48
+ }
49
+ cursors.set(session, state);
50
+ if (!state.aggregate || value.baseline.kind !== 'usage')
51
+ return value;
52
+ // The official measure already prices retained images/files and surface replacements.
53
+ // Only discard its unsuitable aggregate anchor; keep its current surface and node prices.
54
+ const tools = (header ?? session.requestHeader())?.tools;
55
+ const toolTokens = !tools?.length ? 0 : Math.ceil(JSON.stringify(tools).length / 4) + 4;
56
+ const tokens = value.surfaceTokens + toolTokens;
57
+ return Object.freeze({ ...value, baseline: Object.freeze({ kind: 'estimated', tokens }), surfaceDeltaTokens: 0, totalTokens: tokens });
58
+ }
59
+ Object.defineProperty(meter, 'measure', { configurable: true, writable: true, value: measure });
60
+ installed.set(meter, { refs: 1, release() {
61
+ if (Object.getOwnPropertyDescriptor(meter, 'measure')?.value !== measure)
62
+ return;
63
+ if (descriptor)
64
+ Object.defineProperty(meter, 'measure', descriptor);
65
+ else
66
+ delete meter.measure;
67
+ } });
68
+ let active = true;
69
+ return () => { if (active) {
70
+ active = false;
71
+ release(meter);
72
+ } };
73
+ }
74
+ function release(meter) {
75
+ const record = installed.get(meter);
76
+ if (record && --record.refs === 0) {
77
+ record.release();
78
+ installed.delete(meter);
79
+ }
80
+ }
81
+ export function installSearchUsage(ctx) {
82
+ ctx.inject(['sessionProjections'], c => { c.sessionProjections.register(searchUsageProjection); });
83
+ ctx.inject(['tokenMeter'], c => {
84
+ c.effect(() => installSearchMeasurement(c.tokenMeter), 'lcx search context measurement');
85
+ });
86
+ }
@@ -1,72 +1,81 @@
1
1
  function abortReason(signal) {
2
- if (signal?.reason instanceof Error) return signal.reason
3
- return new DOMException('The operation was aborted', 'AbortError')
2
+ if (signal?.reason instanceof Error)
3
+ return signal.reason;
4
+ return new DOMException("The operation was aborted", "AbortError");
4
5
  }
5
-
6
6
  export class ServiceMutex {
7
- constructor() {
8
- this.locked = false
9
- this.closed = false
10
- this.closeReason = undefined
11
- this.queue = []
12
- this.idleWaiters = []
13
- }
14
-
15
- acquire(signal) {
16
- if (this.closed) return Promise.reject(this.closeReason ?? new Error('service mutex closed'))
17
- if (signal?.aborted) return Promise.reject(abortReason(signal))
18
- if (!this.locked) {
19
- this.locked = true
20
- return Promise.resolve(() => this.release())
7
+ locked = false;
8
+ closed = false;
9
+ closeReason;
10
+ queue = [];
11
+ idleWaiters = [];
12
+ constructor() { }
13
+ acquire(signal) {
14
+ if (this.closed)
15
+ return Promise.reject(this.closeReason ?? new Error("service mutex closed"));
16
+ if (signal?.aborted)
17
+ return Promise.reject(abortReason(signal));
18
+ if (!this.locked) {
19
+ this.locked = true;
20
+ return Promise.resolve(() => this.release());
21
+ }
22
+ return new Promise((resolve, reject) => {
23
+ const waiter = { resolve, reject, signal };
24
+ if (signal) {
25
+ const onAbort = () => {
26
+ const index = this.queue.indexOf(waiter);
27
+ if (index >= 0)
28
+ this.queue.splice(index, 1);
29
+ signal.removeEventListener("abort", onAbort);
30
+ reject(abortReason(signal));
31
+ };
32
+ waiter.onAbort = onAbort;
33
+ signal.addEventListener("abort", onAbort, { once: true });
34
+ }
35
+ this.queue.push(waiter);
36
+ });
21
37
  }
22
- return new Promise((resolve, reject) => {
23
- const waiter = { resolve, reject, signal, onAbort: undefined }
24
- if (signal) {
25
- waiter.onAbort = () => {
26
- const index = this.queue.indexOf(waiter)
27
- if (index >= 0) this.queue.splice(index, 1)
28
- signal.removeEventListener('abort', waiter.onAbort)
29
- reject(abortReason(signal))
38
+ release() {
39
+ while (this.queue.length > 0) {
40
+ const waiter = this.queue.shift();
41
+ if (!waiter)
42
+ continue;
43
+ if (waiter.signal && waiter.onAbort)
44
+ waiter.signal.removeEventListener("abort", waiter.onAbort);
45
+ if (waiter.signal?.aborted) {
46
+ waiter.reject(abortReason(waiter.signal));
47
+ continue;
48
+ }
49
+ waiter.resolve(() => this.release());
50
+ return;
30
51
  }
31
- signal.addEventListener('abort', waiter.onAbort, { once: true })
32
- }
33
- this.queue.push(waiter)
34
- })
35
- }
36
-
37
- release() {
38
- while (this.queue.length > 0) {
39
- const waiter = this.queue.shift()
40
- if (waiter.signal && waiter.onAbort) waiter.signal.removeEventListener('abort', waiter.onAbort)
41
- if (waiter.signal?.aborted) {
42
- waiter.reject(abortReason(waiter.signal))
43
- continue
44
- }
45
- waiter.resolve(() => this.release())
46
- return
52
+ this.locked = false;
53
+ const idle = this.idleWaiters.splice(0);
54
+ for (const resolve of idle)
55
+ resolve();
47
56
  }
48
- this.locked = false
49
- const idle = this.idleWaiters.splice(0)
50
- for (const resolve of idle) resolve()
51
- }
52
-
53
- async run(signal, task) {
54
- const release = await this.acquire(signal)
55
- try { return await task() }
56
- finally { release() }
57
- }
58
-
59
- close(reason = new Error('service mutex closing')) {
60
- if (!this.closed) {
61
- this.closed = true
62
- this.closeReason = reason
63
- const queued = this.queue.splice(0)
64
- for (const waiter of queued) {
65
- if (waiter.signal && waiter.onAbort) waiter.signal.removeEventListener('abort', waiter.onAbort)
66
- waiter.reject(reason)
67
- }
57
+ async run(signal, task) {
58
+ const release = await this.acquire(signal);
59
+ try {
60
+ return await task();
61
+ }
62
+ finally {
63
+ release();
64
+ }
65
+ }
66
+ close(reason = new Error("service mutex closing")) {
67
+ if (!this.closed) {
68
+ this.closed = true;
69
+ this.closeReason = reason;
70
+ const queued = this.queue.splice(0);
71
+ for (const waiter of queued) {
72
+ if (waiter.signal && waiter.onAbort)
73
+ waiter.signal.removeEventListener("abort", waiter.onAbort);
74
+ waiter.reject(reason);
75
+ }
76
+ }
77
+ if (!this.locked)
78
+ return Promise.resolve();
79
+ return new Promise((resolve) => this.idleWaiters.push(resolve));
68
80
  }
69
- if (!this.locked) return Promise.resolve()
70
- return new Promise((resolve) => this.idleWaiters.push(resolve))
71
- }
72
81
  }
@@ -1,19 +1,15 @@
1
- // @ts-check
2
-
3
- /** @typedef {Record<string, unknown>} UnknownRecord */
4
-
5
- export const PORTABLE_BUDGET_ERROR_CODE = 'LCX_PORTABLE_BUDGET_EXCEEDED'
6
- const MAX_BUDGET_INPUT_CHARS = 2_000_000
7
- const CONSERVATIVE_IMAGE_TOKEN_COST = 2_048
8
- const CJK = /[\p{Script=Han}\p{Script=Hiragana}\p{Script=Katakana}\p{Script=Hangul}]/u
9
- const ASCII_WORD = /[A-Za-z0-9]/u
10
- const STRUCTURAL = /[\p{P}\p{S}]/u
11
- const DATA_URI = /^data:[^;,\s]+(?:;[^,\s]*)?;base64,[A-Za-z0-9+/\s]+={0,2}$/u
12
- const ENCODED_TEXT = /^[A-Za-z0-9+/_-]{512,}={0,2}$/u
13
-
1
+ export const PORTABLE_BUDGET_ERROR_CODE = "LCX_PORTABLE_BUDGET_EXCEEDED";
2
+ const MAX_BUDGET_INPUT_CHARS = 2_000_000;
3
+ const CONSERVATIVE_IMAGE_TOKEN_COST = 2_048;
4
+ const CJK = /[\p{Script=Han}\p{Script=Hiragana}\p{Script=Katakana}\p{Script=Hangul}]/u;
5
+ const ASCII_WORD = /[A-Za-z0-9]/u;
6
+ const STRUCTURAL = /[\p{P}\p{S}]/u;
7
+ const DATA_URI = /^data:[^;,\s]+(?:;[^,\s]*)?;base64,[A-Za-z0-9+/\s]+={0,2}$/u;
8
+ const ENCODED_TEXT = /^[A-Za-z0-9+/_-]{512,}={0,2}$/u;
14
9
  /** @param {string} value */
15
- function looksEncodedText(value) { return DATA_URI.test(value) || ENCODED_TEXT.test(value) }
16
-
10
+ function looksEncodedText(value) {
11
+ return DATA_URI.test(value) || ENCODED_TEXT.test(value);
12
+ }
17
13
  /**
18
14
  * A small conservative fallback, deliberately not a tokenizer. The baseline
19
15
  * preserves legacy /4 while CJK and structural characters cost more.
@@ -21,84 +17,133 @@ function looksEncodedText(value) { return DATA_URI.test(value) || ENCODED_TEXT.t
21
17
  * @returns {number | undefined}
22
18
  */
23
19
  export function estimateTextTokens(value) {
24
- if (typeof value !== 'string') return undefined
25
- if (value.length > MAX_BUDGET_INPUT_CHARS || looksEncodedText(value)) return Math.max(1, value.length)
26
- let weighted = 0
27
- let asciiRun = 0
28
- const flushAscii = () => { if (asciiRun > 0) weighted += Math.ceil(asciiRun / 4); asciiRun = 0 }
29
- for (const char of value) {
30
- if (ASCII_WORD.test(char)) { asciiRun += 1; continue }
31
- flushAscii()
32
- if (/\s/u.test(char)) continue
33
- if (CJK.test(char) || STRUCTURAL.test(char)) weighted += 1
34
- else weighted += Math.ceil(char.length / 2)
35
- }
36
- flushAscii()
37
- return Math.max(1, Math.ceil(value.length / 4), weighted)
20
+ if (typeof value !== "string")
21
+ return undefined;
22
+ if (value.length > MAX_BUDGET_INPUT_CHARS || looksEncodedText(value))
23
+ return Math.max(1, value.length);
24
+ let weighted = 0;
25
+ let asciiRun = 0;
26
+ const flushAscii = () => {
27
+ if (asciiRun > 0)
28
+ weighted += Math.ceil(asciiRun / 4);
29
+ asciiRun = 0;
30
+ };
31
+ for (const char of value) {
32
+ if (ASCII_WORD.test(char)) {
33
+ asciiRun += 1;
34
+ continue;
35
+ }
36
+ flushAscii();
37
+ if (/\s/u.test(char))
38
+ continue;
39
+ if (CJK.test(char) || STRUCTURAL.test(char))
40
+ weighted += 1;
41
+ else
42
+ weighted += Math.ceil(char.length / 2);
43
+ }
44
+ flushAscii();
45
+ return Math.max(1, Math.ceil(value.length / 4), weighted);
38
46
  }
39
-
40
47
  /**
41
48
  * @param {unknown} value
42
49
  * @returns {value is UnknownRecord}
43
50
  */
44
- function isObject(value) { return value !== null && typeof value === 'object' && !Array.isArray(value) }
51
+ function isObject(value) {
52
+ return value !== null && typeof value === "object" && !Array.isArray(value);
53
+ }
45
54
  /**
46
55
  * @param {unknown} value
47
56
  * @param {Set<object>} [seen]
48
57
  * @returns {boolean}
49
58
  */
50
59
  function containsOpaqueValue(value, seen = new Set()) {
51
- if (value === null || typeof value !== 'object') return false
52
- if (ArrayBuffer.isView(value) || value instanceof ArrayBuffer || seen.has(value)) return true
53
- seen.add(value)
54
- return Object.values(value).some((entry) => containsOpaqueValue(entry, seen))
60
+ if (value === null || typeof value !== "object")
61
+ return false;
62
+ if (ArrayBuffer.isView(value) ||
63
+ value instanceof ArrayBuffer ||
64
+ seen.has(value))
65
+ return true;
66
+ seen.add(value);
67
+ return Object.values(value).some((entry) => containsOpaqueValue(entry, seen));
55
68
  }
56
69
  /** @param {unknown} value */
57
- function safeJson(value) { try { if (containsOpaqueValue(value)) return undefined; const encoded = JSON.stringify(value); return typeof encoded === 'string' ? encoded : undefined } catch { return undefined } }
70
+ function safeJson(value) {
71
+ try {
72
+ if (containsOpaqueValue(value))
73
+ return undefined;
74
+ const encoded = JSON.stringify(value);
75
+ return typeof encoded === "string" ? encoded : undefined;
76
+ }
77
+ catch {
78
+ return undefined;
79
+ }
80
+ }
58
81
  /** @param {unknown} value */
59
- function safeScalar(value) { return typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean' ? String(value) : undefined }
60
-
82
+ function safeScalar(value) {
83
+ return typeof value === "string" ||
84
+ typeof value === "number" ||
85
+ typeof value === "boolean"
86
+ ? String(value)
87
+ : undefined;
88
+ }
61
89
  /**
62
90
  * @param {unknown} content
63
91
  * @returns {unknown[] | undefined}
64
92
  */
65
93
  function visibleContent(content) {
66
- if (typeof content === 'string') return [{ type: 'text', text: content }]
67
- if (!Array.isArray(content)) return undefined
68
- const parts = []
69
- for (const part of content) {
70
- if (!isObject(part) || typeof part.type !== 'string') return undefined
71
- if (['text', 'input_text', 'output_text'].includes(part.type)) {
72
- if (typeof part.text !== 'string') return undefined
73
- parts.push({ type: part.type, text: part.text })
74
- continue
94
+ if (typeof content === "string")
95
+ return [{ type: "text", text: content }];
96
+ if (!Array.isArray(content))
97
+ return undefined;
98
+ const parts = [];
99
+ for (const part of content) {
100
+ if (!isObject(part) || typeof part.type !== "string")
101
+ return undefined;
102
+ if (["text", "input_text", "output_text"].includes(part.type)) {
103
+ if (typeof part.text !== "string")
104
+ return undefined;
105
+ parts.push({ type: part.type, text: part.text });
106
+ continue;
107
+ }
108
+ if (part.type === "reasoning") {
109
+ if (typeof part.text !== "string")
110
+ return undefined;
111
+ parts.push({ type: "reasoning", text: part.text });
112
+ continue;
113
+ }
114
+ if (part.type === "tool-call") {
115
+ const id = safeScalar(part.id);
116
+ const name = safeScalar(part.name);
117
+ const argumentsText = safeJson(part.arguments);
118
+ if (id === undefined || name === undefined || argumentsText === undefined)
119
+ return undefined;
120
+ parts.push({ type: "tool-call", id, name, arguments: argumentsText });
121
+ continue;
122
+ }
123
+ if (part.type === "tool-result") {
124
+ const toolCallId = safeScalar(part.toolCallId);
125
+ const toolName = safeScalar(part.toolName ?? part.name ?? "unknown");
126
+ const nested = visibleContent(part.content);
127
+ if (toolCallId === undefined ||
128
+ toolName === undefined ||
129
+ nested === undefined)
130
+ return undefined;
131
+ parts.push({
132
+ type: "tool-result",
133
+ toolCallId,
134
+ toolName,
135
+ content: nested,
136
+ });
137
+ continue;
138
+ }
139
+ if (["image", "input_image", "output_image", "dsh_image_attachment"].includes(part.type)) {
140
+ parts.push({ type: part.type, image: true });
141
+ continue;
142
+ }
143
+ return undefined;
75
144
  }
76
- if (part.type === 'reasoning') {
77
- if (typeof part.text !== 'string') return undefined
78
- parts.push({ type: 'reasoning', text: part.text })
79
- continue
80
- }
81
- if (part.type === 'tool-call') {
82
- const id = safeScalar(part.id); const name = safeScalar(part.name); const argumentsText = safeJson(part.arguments)
83
- if (id === undefined || name === undefined || argumentsText === undefined) return undefined
84
- parts.push({ type: 'tool-call', id, name, arguments: argumentsText })
85
- continue
86
- }
87
- if (part.type === 'tool-result') {
88
- const toolCallId = safeScalar(part.toolCallId); const toolName = safeScalar(part.toolName ?? part.name ?? 'unknown'); const nested = visibleContent(part.content)
89
- if (toolCallId === undefined || toolName === undefined || nested === undefined) return undefined
90
- parts.push({ type: 'tool-result', toolCallId, toolName, content: nested })
91
- continue
92
- }
93
- if (['image', 'input_image', 'output_image', 'dsh_image_attachment'].includes(part.type)) {
94
- parts.push({ type: part.type, image: true })
95
- continue
96
- }
97
- return undefined
98
- }
99
- return parts
145
+ return parts;
100
146
  }
101
-
102
147
  /**
103
148
  * Project an item to model-visible fields only. Opaque provider state, raw
104
149
  * binary, and replay/session metadata are intentionally excluded.
@@ -106,21 +151,39 @@ function visibleContent(content) {
106
151
  * @returns {unknown | undefined}
107
152
  */
108
153
  export function modelVisibleBudgetView(item) {
109
- if (!isObject(item)) return undefined
110
- if (item.type === 'function_call') {
111
- const callId = safeScalar(item.call_id); const name = safeScalar(item.name); const argumentsText = safeJson(item.arguments)
112
- return callId === undefined || name === undefined || argumentsText === undefined ? undefined : { type: 'function_call', call_id: callId, name, arguments: argumentsText }
113
- }
114
- if (item.type === 'function_call_output') {
115
- const callId = safeScalar(item.call_id); const output = typeof item.output === 'string' ? item.output : safeJson(item.output)
116
- return callId === undefined || output === undefined ? undefined : { type: 'function_call_output', call_id: callId, output }
117
- }
118
- if (item.type !== undefined && item.type !== 'message') return undefined
119
- if (!['developer', 'system', 'user', 'assistant'].includes(String(item.role ?? ''))) return undefined
120
- const content = visibleContent(item.content)
121
- return content === undefined ? undefined : { role: String(item.role), content }
154
+ if (!isObject(item))
155
+ return undefined;
156
+ if (item.type === "function_call") {
157
+ const callId = safeScalar(item.call_id);
158
+ const name = safeScalar(item.name);
159
+ const argumentsText = safeJson(item.arguments);
160
+ return callId === undefined ||
161
+ name === undefined ||
162
+ argumentsText === undefined
163
+ ? undefined
164
+ : {
165
+ type: "function_call",
166
+ call_id: callId,
167
+ name,
168
+ arguments: argumentsText,
169
+ };
170
+ }
171
+ if (item.type === "function_call_output") {
172
+ const callId = safeScalar(item.call_id);
173
+ const output = typeof item.output === "string" ? item.output : safeJson(item.output);
174
+ return callId === undefined || output === undefined
175
+ ? undefined
176
+ : { type: "function_call_output", call_id: callId, output };
177
+ }
178
+ if (item.type !== undefined && item.type !== "message")
179
+ return undefined;
180
+ if (!["developer", "system", "user", "assistant"].includes(String(item.role ?? "")))
181
+ return undefined;
182
+ const content = visibleContent(item.content);
183
+ return content === undefined
184
+ ? undefined
185
+ : { role: String(item.role), content };
122
186
  }
123
-
124
187
  /**
125
188
  * Images have provider/model-dependent costs. Count each known image block with
126
189
  * a fixed conservative surcharge without inspecting base64 or attachment data.
@@ -128,38 +191,43 @@ export function modelVisibleBudgetView(item) {
128
191
  * @returns {number}
129
192
  */
130
193
  function imageTokenCost(value) {
131
- if (Array.isArray(value)) {
132
- let total = 0
133
- for (const entry of value) total += imageTokenCost(entry)
134
- return total
135
- }
136
- if (!isObject(value)) return 0
137
- let total = value.image === true ? CONSERVATIVE_IMAGE_TOKEN_COST : 0
138
- for (const entry of Object.values(value)) total += imageTokenCost(entry)
139
- return total
194
+ if (Array.isArray(value)) {
195
+ let total = 0;
196
+ for (const entry of value)
197
+ total += imageTokenCost(entry);
198
+ return total;
199
+ }
200
+ if (!isObject(value))
201
+ return 0;
202
+ let total = value.image === true ? CONSERVATIVE_IMAGE_TOKEN_COST : 0;
203
+ for (const entry of Object.values(value))
204
+ total += imageTokenCost(entry);
205
+ return total;
140
206
  }
141
-
142
207
  /**
143
208
  * @param {unknown} item
144
209
  * @returns {number | undefined}
145
210
  */
146
211
  export function estimateBudgetItem(item) {
147
- const view = modelVisibleBudgetView(item)
148
- if (view === undefined) return undefined
149
- const encoded = safeJson(view)
150
- const textCost = estimateTextTokens(encoded)
151
- return textCost === undefined ? undefined : textCost + imageTokenCost(view)
212
+ const view = modelVisibleBudgetView(item);
213
+ if (view === undefined)
214
+ return undefined;
215
+ const encoded = safeJson(view);
216
+ const textCost = estimateTextTokens(encoded);
217
+ return textCost === undefined ? undefined : textCost + imageTokenCost(view);
152
218
  }
153
-
154
219
  /** @param {unknown} maxChars */
155
220
  export function portableTokenCeiling(maxChars) {
156
- return Number.isSafeInteger(maxChars) && /** @type {number} */ (maxChars) > 0 ? Math.ceil(/** @type {number} */ (maxChars) / 4) : undefined
221
+ return typeof maxChars === "number" &&
222
+ Number.isSafeInteger(maxChars) &&
223
+ maxChars > 0
224
+ ? Math.ceil(maxChars / 4)
225
+ : undefined;
157
226
  }
158
-
159
227
  /** @param {string} message */
160
228
  export function portableBudgetError(message) {
161
- /** @type {Error & { code?: string }} */
162
- const error = new Error(message)
163
- error.code = PORTABLE_BUDGET_ERROR_CODE
164
- return error
229
+ /** @type {Error & { code?: string }} */
230
+ const error = new Error(message);
231
+ error.code = PORTABLE_BUDGET_ERROR_CODE;
232
+ return error;
165
233
  }