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
@@ -1,306 +1,541 @@
1
- import { offloadRequestImagesWithPolicy } from '@deepseek-ai/dsh-llm'
2
- import { convertResponsesMessages, convertResponsesTools } from '@earendil-works/pi-ai/api/openai-responses-shared'
3
- import { createGrammarToolInputProperties } from '@earendil-works/pi-ai/api/constrained-sampling'
4
- import { getBuiltinModels } from '@earendil-works/pi-ai/providers/all'
5
- const DEFAULT_MAX_REQUEST_IMAGE_BYTES = 20 * 1024 * 1024
6
- const DEFAULT_REQUEST_IMAGE_PIXEL_BUDGET = 2048 * 2048
7
- const DEFAULT_REQUEST_IMAGE_MAX_BYTES = 1024 * 1024
8
- export { DEFAULT_MAX_REQUEST_IMAGE_BYTES, DEFAULT_REQUEST_IMAGE_PIXEL_BUDGET, DEFAULT_REQUEST_IMAGE_MAX_BYTES }
9
-
10
- function isObject(value) { return value !== null && typeof value === 'object' && !Array.isArray(value) }
11
-
1
+ import { offloadedImageText, offloadRequestImagesWithPolicy, requestImageHandleText, resolveImageAttachmentAccess, } from "@deepseek-ai/dsh-llm";
2
+ import { convertResponsesMessages, convertResponsesTools, createGrammarToolInputProperties, getBuiltinModels, } from "./pi-responses-runtime.js";
3
+ const DEFAULT_MAX_REQUEST_IMAGE_BYTES = 20 * 1024 * 1024;
4
+ const DEFAULT_REQUEST_IMAGE_PIXEL_BUDGET = 2048 * 2048;
5
+ const DEFAULT_REQUEST_IMAGE_MAX_BYTES = 1024 * 1024;
6
+ export { DEFAULT_MAX_REQUEST_IMAGE_BYTES, DEFAULT_REQUEST_IMAGE_PIXEL_BUDGET, DEFAULT_REQUEST_IMAGE_MAX_BYTES, };
7
+ function isObject(value) {
8
+ return value !== null && typeof value === "object" && !Array.isArray(value);
9
+ }
10
+ function isResponseInputItem(value) {
11
+ if (!isObject(value))
12
+ return false;
13
+ if (value.type === "compaction")
14
+ return typeof value.encrypted_content === "string";
15
+ if (value.type === "message" || value.type === undefined)
16
+ return ((value.role === "developer" ||
17
+ value.role === "user" ||
18
+ value.role === "system" ||
19
+ value.role === "assistant") &&
20
+ (typeof value.content === "string" || Array.isArray(value.content)));
21
+ if (value.type === "function_call")
22
+ return (typeof value.call_id === "string" &&
23
+ typeof value.name === "string" &&
24
+ typeof value.arguments === "string");
25
+ if (value.type === "function_call_output")
26
+ return typeof value.call_id === "string" && value.output !== undefined;
27
+ return false;
28
+ }
29
+ export function responseInputItems(value) {
30
+ if (!Array.isArray(value) || !value.every(isResponseInputItem))
31
+ throw error("native checkpoint contains an unsupported Responses input item", "LCX_COMPACT_INVALID_RESPONSE");
32
+ return value;
33
+ }
34
+ function asArray(value) {
35
+ return Array.isArray(value) ? value : [];
36
+ }
37
+ function stringValue(value, fallback = "") {
38
+ return typeof value === "string" ? value : fallback;
39
+ }
40
+ function positiveSafeInteger(value, fallback) {
41
+ return Number.isSafeInteger(value) && value > 0
42
+ ? value
43
+ : fallback;
44
+ }
12
45
  function error(message, code) {
13
- const value = new Error(message)
14
- value.code = code
15
- return value
46
+ const value = new Error(message);
47
+ value.code = code;
48
+ return value;
49
+ }
50
+ function errorCode(value) {
51
+ return isObject(value) && typeof value.code === "string" ? value.code : undefined;
52
+ }
53
+ function errorMessage(value) {
54
+ return value instanceof Error ? value.message : String(value);
55
+ }
56
+ function attachmentServices(store) {
57
+ if (store === undefined)
58
+ return undefined;
59
+ return {
60
+ store,
61
+ resolveAccess: (mapper, ref) => resolveImageAttachmentAccess(store, mapper, ref),
62
+ };
63
+ }
64
+ function hasImageAttachment(value) {
65
+ return isObject(value) && typeof value.attachmentId === "string" && typeof value.bytes === "number";
66
+ }
67
+ function imageAccess(ctx, attachments, ref) {
68
+ const mapper = ctx?.fs
69
+ ? (hostPath) => ctx.fs.processPathFromHostPath(hostPath)
70
+ : () => undefined;
71
+ return attachments.resolveAccess(mapper, ref);
16
72
  }
17
-
18
73
  export async function resolveModelImageSupport(ctx, route, signal) {
19
- const llm = ctx?.get?.('llm') ?? ctx?.llm
20
- if (typeof llm?.resolveModelInfo !== 'function') return 'unknown'
21
- try {
22
- const info = await llm.resolveModelInfo(route.provider, route.model, signal)
23
- const modalities = info?.inputModalities
24
- if (!Array.isArray(modalities)) return 'unknown'
25
- return modalities.includes('image') ? 'supported' : 'unsupported'
26
- } catch {
27
- return 'unknown'
28
- }
29
- }
30
-
31
- function attachmentResolver(ctx, options) {
32
- const attachments = ctx?.get?.('attachments') ?? ctx?.attachments
33
- return async (block, signal) => {
34
- if (typeof attachments?.readImageRequest !== 'function') {
35
- throw error('LCX requires the DSH 0.1.1-rc.2 request-image attachment API', 'LCX_COMPACT_IMAGE_API_UNAVAILABLE')
74
+ const llm = ctx?.llm;
75
+ if (llm === undefined)
76
+ return "unknown";
77
+ try {
78
+ const info = await llm.resolveModelInfo(stringValue(route.provider), stringValue(route.model), signal);
79
+ const modalities = info.inputModalities;
80
+ return modalities?.includes("image")
81
+ ? "supported"
82
+ : modalities !== undefined
83
+ ? "unsupported"
84
+ : "unknown";
85
+ }
86
+ catch {
87
+ return "unknown";
36
88
  }
37
- const request = await attachments.readImageRequest(block?.attachment, {
38
- maxPixels: options.requestImagePixelBudget,
39
- maxBytes: options.requestImageMaxBytes,
40
- }, signal)
41
- const data = request?.data
42
- const mediaType = request?.mediaType ?? block?.attachment?.mediaType
43
- if (!(data instanceof Uint8Array) && !Buffer.isBuffer(data)) throw error('DSH attachment returned no request-image bytes', 'LCX_COMPACT_IMAGE_UNAVAILABLE')
44
- if (typeof mediaType !== 'string' || !mediaType.startsWith('image/')) throw error('DSH attachment returned an invalid request-image media type', 'LCX_COMPACT_IMAGE_UNAVAILABLE')
45
- return { data: Buffer.from(data), mediaType, ref: request?.attachment ?? block.attachment }
46
- }
47
- }
48
-
89
+ }
90
+ function attachmentResolver(ctx, options) {
91
+ const attachments = attachmentServices(ctx?.attachments);
92
+ return async (block, signal) => {
93
+ if (attachments === undefined)
94
+ throw error("LCX requires the DSH request-image attachment API", "LCX_COMPACT_IMAGE_API_UNAVAILABLE");
95
+ const request = await attachments.store.readImageRequest(block.attachment, { maxPixels: options.requestImagePixelBudget, maxBytes: options.requestImageMaxBytes }, signal);
96
+ const data = request.data;
97
+ const mediaType = request.mediaType ?? block.attachment.mediaType;
98
+ if (!(data instanceof Uint8Array) && !Buffer.isBuffer(data))
99
+ throw error("DSH attachment returned no request-image bytes", "LCX_COMPACT_IMAGE_UNAVAILABLE");
100
+ if (typeof mediaType !== "string" || !mediaType.startsWith("image/"))
101
+ throw error("DSH attachment returned an invalid request-image media type", "LCX_COMPACT_IMAGE_UNAVAILABLE");
102
+ const ref = request.attachment ?? block.attachment;
103
+ return {
104
+ data: Buffer.from(data),
105
+ mediaType,
106
+ ref,
107
+ request,
108
+ access: imageAccess(ctx, attachments, ref),
109
+ };
110
+ };
111
+ }
49
112
  async function imagePart(block, ctx, options, imageMap) {
50
- if (options.imageSupport === 'unsupported') return { type: 'input_text', text: '[image omitted because the target model does not support image input]' }
51
- const image = await attachmentResolver(ctx, options)(block, options.signal)
52
- const encodedBytes = Math.ceil(image.data.byteLength / 3) * 4
53
- if (encodedBytes > options.maxRequestImageBytes) throw error('one image exceeds the configured LCX request image bound', 'LCX_COMPACT_IMAGE_TOO_LARGE')
54
- const imageUrl = `data:${image.mediaType};base64,${image.data.toString('base64')}`
55
- imageMap.set(imageUrl, structuredClone(image.ref))
56
- return { type: 'input_image', detail: 'auto', image_url: imageUrl }
57
- }
58
-
59
- async function piImagePart(block, ctx, options, imageMap) {
60
- if (options.imageSupport === 'unsupported') return { type: 'text', text: '[image omitted because the target model does not support image input]' }
61
- const image = await attachmentResolver(ctx, options)(block, options.signal)
62
- const encodedBytes = Math.ceil(image.data.byteLength / 3) * 4
63
- if (encodedBytes > options.maxRequestImageBytes) throw error('one image exceeds the configured LCX request image bound', 'LCX_COMPACT_IMAGE_TOO_LARGE')
64
- const data = image.data.toString('base64')
65
- imageMap.set(`data:${image.mediaType};base64,${data}`, structuredClone(image.ref))
66
- return { type: 'image', data, mimeType: image.mediaType }
67
- }
68
-
113
+ if (options.imageSupport === "unsupported")
114
+ return { type: "input_text", text: "[image omitted because the target model does not support image input]" };
115
+ const image = await attachmentResolver(ctx, options)(block, options.signal);
116
+ if (Math.ceil(image.data.byteLength / 3) * 4 > options.maxRequestImageBytes)
117
+ throw error("one image exceeds the configured LCX request image bound", "LCX_COMPACT_IMAGE_TOO_LARGE");
118
+ const imageUrl = `data:${image.mediaType};base64,${image.data.toString("base64")}`;
119
+ imageMap.set(imageUrl, structuredClone(image.ref));
120
+ return { type: "input_image", detail: "auto", image_url: imageUrl };
121
+ }
122
+ async function piImageParts(block, ctx, options, imageMap) {
123
+ if (options.imageSupport === "unsupported")
124
+ return [{ type: "text", text: "[image omitted because the target model does not support image input]" }];
125
+ const image = await attachmentResolver(ctx, options)(block, options.signal);
126
+ if (Math.ceil(image.data.byteLength / 3) * 4 > options.maxRequestImageBytes)
127
+ throw error("one image exceeds the configured LCX request image bound", "LCX_COMPACT_IMAGE_TOO_LARGE");
128
+ const data = image.data.toString("base64");
129
+ imageMap.set(`data:${image.mediaType};base64,${data}`, structuredClone(image.ref));
130
+ return [
131
+ { type: "text", text: requestImageHandleText(image.ref, image.request, image.access) },
132
+ { type: "image", data, mimeType: image.mediaType },
133
+ ];
134
+ }
69
135
  function parseArguments(value) {
70
- if (typeof value !== 'string') return value && typeof value === 'object' ? structuredClone(value) : {}
71
- try {
72
- const parsed = JSON.parse(value)
73
- return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : {}
74
- } catch { return {} }
136
+ if (isObject(value))
137
+ return structuredClone(value);
138
+ if (typeof value !== "string")
139
+ return {};
140
+ try {
141
+ const parsed = JSON.parse(value);
142
+ return isObject(parsed) ? parsed : {};
143
+ }
144
+ catch {
145
+ return {};
146
+ }
75
147
  }
76
-
77
148
  function emptyUsage() {
78
- return { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, totalTokens: 0, cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 } }
149
+ return { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, totalTokens: 0, cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 } };
150
+ }
151
+ function invalidReplay(message) {
152
+ return error(`invalid pi-ai replay state: ${message}`, "LCX_COMPACT_INVALID_REPLAY_STATE");
153
+ }
154
+ function unsupportedContent(type) {
155
+ return error(`LCX Compact cannot safely serialize DSH message content type: ${String(type)}`, "LCX_CHECKPOINT_PORTABLE_UNSUPPORTED_CONTENT");
156
+ }
157
+ function replayBlockType(type) {
158
+ return type === "text" || type === "reasoning" || type === "tool-call" ? type : undefined;
159
+ }
160
+ function replayTextPhase(signature) {
161
+ if (typeof signature !== "string" || !signature.startsWith("{"))
162
+ return undefined;
163
+ try {
164
+ const parsed = JSON.parse(signature);
165
+ if (!isObject(parsed) || parsed.v !== 1 || typeof parsed.id !== "string")
166
+ return undefined;
167
+ return parsed.phase === "commentary" || parsed.phase === "final_answer"
168
+ ? parsed.phase
169
+ : undefined;
170
+ }
171
+ catch {
172
+ return undefined;
173
+ }
174
+ }
175
+ function restorePortableAssistantPhases(input, messages, target) {
176
+ const slots = [];
177
+ for (const message of messages) {
178
+ if (message.role !== "assistant" || message.stopReason === "error" || message.stopReason === "aborted")
179
+ continue;
180
+ const sameProviderAndApi = message.provider === target.provider && message.api === target.api;
181
+ const sameModel = sameProviderAndApi && message.model === target.id;
182
+ for (const block of message.content) {
183
+ if (block.type === "text") {
184
+ const phase = sameProviderAndApi && !sameModel
185
+ ? replayTextPhase(block.textSignature)
186
+ : undefined;
187
+ slots.push({ text: block.text, ...(phase === undefined ? {} : { phase }) });
188
+ }
189
+ else if (block.type === "thinking" && !sameModel && !block.redacted && block.thinking.trim()) {
190
+ slots.push({ text: block.thinking });
191
+ }
192
+ }
193
+ }
194
+ const output = structuredClone(input);
195
+ let slotIndex = 0;
196
+ for (const [index, candidate] of output.entries()) {
197
+ if (!isObject(candidate) || candidate.type !== "message" || candidate.role !== "assistant" ||
198
+ !Array.isArray(candidate.content))
199
+ continue;
200
+ const slot = slots[slotIndex++];
201
+ if (slot?.phase === undefined)
202
+ continue;
203
+ const text = candidate.content
204
+ .map((part) => isObject(part) && part.type === "output_text" ? String(part.text ?? "") : "")
205
+ .join("");
206
+ if (text === slot.text)
207
+ output[index] = { ...candidate, phase: slot.phase };
208
+ }
209
+ return output;
79
210
  }
80
-
81
- function invalidReplay(message) { return error(`invalid pi-ai replay state: ${message}`, 'LCX_COMPACT_INVALID_REPLAY_STATE') }
82
- function unsupportedContent(type) { return error(`LCX Compact cannot safely serialize DSH message content type: ${String(type)}`, 'LCX_CHECKPOINT_PORTABLE_UNSUPPORTED_CONTENT') }
83
- function replayBlockType(type) { if (type === 'text') return 'text'; if (type === 'reasoning') return 'reasoning'; if (type === 'tool-call') return 'tool-call'; return undefined }
84
-
85
211
  export function readDshPiReplayState(value) {
86
- if (!isObject(value)) throw invalidReplay('expected a replay envelope')
87
- const response = value.response
88
- if (!isObject(response)) throw invalidReplay('expected a response object')
89
- if (response.kind !== 'pi-ai') throw invalidReplay('unknown state kind')
90
- if (response.version !== 2) throw invalidReplay(`unsupported version ${String(response.version)}`)
91
- for (const key of ['api', 'provider', 'model']) if (typeof response[key] !== 'string' || response[key].length === 0) throw invalidReplay(`${key} must be a non-empty string`)
92
- if (!['stop', 'length', 'toolUse', 'error', 'aborted'].includes(response.stopReason)) throw invalidReplay('unknown stopReason')
93
- if (response.responseModel !== undefined && typeof response.responseModel !== 'string') throw invalidReplay('responseModel must be a string')
94
- if (response.responseId !== undefined && typeof response.responseId !== 'string') throw invalidReplay('responseId must be a string')
95
- if (!Array.isArray(value.blocks)) throw invalidReplay('blocks must be an array')
96
- for (const [index, block] of value.blocks.entries()) {
97
- if (!isObject(block)) throw invalidReplay(`block ${index} must be an object`)
98
- if (!['text', 'reasoning', 'tool-call'].includes(block.type)) throw invalidReplay(`block ${index} has an unknown type`)
99
- for (const signature of ['textSignature', 'thinkingSignature', 'thoughtSignature']) if (block[signature] !== undefined && typeof block[signature] !== 'string') throw invalidReplay(`block ${index} ${signature} must be a string`)
100
- if (block.redacted !== undefined && typeof block.redacted !== 'boolean') throw invalidReplay(`block ${index} redacted must be boolean`)
101
- if (block.namespace !== undefined && typeof block.namespace !== 'string') throw invalidReplay(`block ${index} namespace must be a string`)
102
- }
103
- return { response, blocks: value.blocks }
104
- }
105
-
212
+ if (!isObject(value))
213
+ throw invalidReplay("expected a replay envelope");
214
+ const responseValue = value.response;
215
+ if (!isObject(responseValue) || responseValue.kind !== "pi-ai" || responseValue.version !== 2)
216
+ throw invalidReplay("expected a pi-ai version 2 response object");
217
+ const required = ["api", "provider", "model"];
218
+ for (const key of required)
219
+ if (typeof responseValue[key] !== "string" || responseValue[key].length === 0)
220
+ throw invalidReplay(`${key} must be a non-empty string`);
221
+ if (!["stop", "length", "toolUse", "error", "aborted"].includes(responseValue.stopReason))
222
+ throw invalidReplay("unknown stopReason");
223
+ if (responseValue.responseModel !== undefined && typeof responseValue.responseModel !== "string")
224
+ throw invalidReplay("responseModel must be a string");
225
+ if (responseValue.responseId !== undefined && typeof responseValue.responseId !== "string")
226
+ throw invalidReplay("responseId must be a string");
227
+ if (!Array.isArray(value.blocks))
228
+ throw invalidReplay("blocks must be an array");
229
+ const blocks = value.blocks.map((block, index) => {
230
+ if (!isObject(block) || replayBlockType(block.type) === undefined)
231
+ throw invalidReplay(`block ${index} has an unknown type`);
232
+ for (const signature of ["textSignature", "thinkingSignature", "thoughtSignature", "namespace"])
233
+ if (block[signature] !== undefined && typeof block[signature] !== "string")
234
+ throw invalidReplay(`block ${index} ${signature} must be a string`);
235
+ if (block.redacted !== undefined && typeof block.redacted !== "boolean")
236
+ throw invalidReplay(`block ${index} redacted must be boolean`);
237
+ return { ...block, type: replayBlockType(block.type) };
238
+ });
239
+ return { response: responseValue, blocks };
240
+ }
106
241
  function foreignAssistant(message) {
107
- const source = message?.source?.kind === 'model' ? message.source : undefined
108
- const content = []
109
- for (const block of message?.content ?? []) {
110
- if (block?.type === 'text') content.push({ type: 'text', text: String(block.text ?? '') })
111
- else if (block?.type === 'reasoning') content.push({ type: 'thinking', thinking: String(block.text ?? '') })
112
- else if (block?.type === 'tool-call') content.push({ type: 'toolCall', id: String(block.id), name: String(block.name), arguments: parseArguments(block.arguments) })
113
- else if (block?.type === 'image') throw unsupportedContent('assistant image')
114
- else throw unsupportedContent(block?.type)
115
- }
116
- return { role: 'assistant', content, api: 'dsh-foreign', provider: source?.provider ?? 'dsh-foreign', model: source?.model ?? 'dsh-foreign', usage: emptyUsage(), stopReason: content.some((block) => block.type === 'toolCall') ? 'toolUse' : 'stop', timestamp: 0 }
117
- }
118
-
242
+ const source = message.source.kind === "model" ? message.source : undefined;
243
+ const content = [];
244
+ for (const block of message.content) {
245
+ if (block.type === "text")
246
+ content.push({ type: "text", text: block.text });
247
+ else if (block.type === "reasoning")
248
+ content.push({ type: "thinking", thinking: block.text });
249
+ else if (block.type === "tool-call")
250
+ content.push({ type: "toolCall", id: String(block.id), name: block.name, arguments: parseArguments(block.arguments) });
251
+ else
252
+ throw unsupportedContent(block.type);
253
+ }
254
+ return { role: "assistant", content, api: "dsh-foreign", provider: source?.provider ?? "dsh-foreign", model: source?.model ?? "dsh-foreign", usage: emptyUsage(), stopReason: content.some((block) => block.type === "toolCall") ? "toolUse" : "stop", timestamp: 0 };
255
+ }
119
256
  function replayedAssistant(message, source) {
120
- const state = readDshPiReplayState(source.replayState)
121
- if (state.response.provider !== source.provider) throw invalidReplay('provider does not match assistant source')
122
- if (state.response.model !== source.model) throw invalidReplay('model does not match assistant source')
123
- if (state.blocks.length !== (message?.content ?? []).length) throw invalidReplay('block count does not match assistant content')
124
- const content = (message.content ?? []).map((block, index) => {
125
- const replay = state.blocks[index]
126
- if (replayBlockType(block?.type) !== replay?.type) throw invalidReplay(`block ${index} does not match assistant content`)
127
- if (block.type === 'text') return { type: 'text', text: String(block.text ?? ''), ...(replay.textSignature === undefined ? {} : { textSignature: replay.textSignature }) }
128
- if (block.type === 'reasoning') return { type: 'thinking', thinking: String(block.text ?? ''), ...(replay.thinkingSignature === undefined ? {} : { thinkingSignature: replay.thinkingSignature }), ...(replay.redacted === undefined ? {} : { redacted: replay.redacted }) }
129
- return { type: 'toolCall', id: String(block.id), name: String(block.name), arguments: parseArguments(block.arguments), ...(replay.thoughtSignature === undefined ? {} : { thoughtSignature: replay.thoughtSignature }), ...(replay.namespace === undefined ? {} : { namespace: replay.namespace }) }
130
- })
131
- return { role: 'assistant', content, api: state.response.api, provider: state.response.provider, model: state.response.model, ...(state.response.responseModel === undefined ? {} : { responseModel: state.response.responseModel }), ...(state.response.responseId === undefined ? {} : { responseId: state.response.responseId }), usage: emptyUsage(), stopReason: state.response.stopReason, timestamp: 0 }
132
- }
133
-
257
+ if (source.replayState === undefined)
258
+ throw invalidReplay("assistant source has no replay state");
259
+ const state = readDshPiReplayState(source.replayState);
260
+ if (state.response.provider !== source.provider)
261
+ throw invalidReplay("provider does not match assistant source");
262
+ if (state.response.model !== source.model)
263
+ throw invalidReplay("model does not match assistant source");
264
+ if (state.blocks.length !== message.content.length)
265
+ throw invalidReplay("block count does not match assistant content");
266
+ const content = message.content.map((block, index) => {
267
+ const replay = state.blocks[index];
268
+ if (replayBlockType(block.type) !== replay.type)
269
+ throw invalidReplay(`block ${index} does not match assistant content`);
270
+ if (block.type === "text")
271
+ return { type: "text", text: block.text, ...(typeof replay.textSignature === "string" ? { textSignature: replay.textSignature } : {}) };
272
+ if (block.type === "reasoning")
273
+ return { type: "thinking", thinking: block.text, ...(typeof replay.thinkingSignature === "string" ? { thinkingSignature: replay.thinkingSignature } : {}), ...(typeof replay.redacted === "boolean" ? { redacted: replay.redacted } : {}) };
274
+ if (block.type === "tool-call")
275
+ return { type: "toolCall", id: String(block.id), name: block.name, arguments: parseArguments(block.arguments), ...(typeof replay.thoughtSignature === "string" ? { thoughtSignature: replay.thoughtSignature } : {}), ...(typeof replay.namespace === "string" ? { namespace: replay.namespace } : {}) };
276
+ throw invalidReplay(`block ${index} has an unsupported type`);
277
+ });
278
+ return { role: "assistant", content, api: state.response.api, provider: state.response.provider, model: state.response.model, ...(typeof state.response.responseModel === "string" ? { responseModel: state.response.responseModel } : {}), ...(typeof state.response.responseId === "string" ? { responseId: state.response.responseId } : {}), usage: emptyUsage(), stopReason: state.response.stopReason, timestamp: 0 };
279
+ }
134
280
  function toPiAssistant(message, onReplayDegrade) {
135
- const source = message?.source
136
- if (source?.kind !== 'model' || source.replayState === undefined) return foreignAssistant(message)
137
- try { return replayedAssistant(message, source) }
138
- catch (cause) {
139
- if (cause?.code !== 'LCX_COMPACT_INVALID_REPLAY_STATE') throw cause
140
- onReplayDegrade?.(cause.message)
141
- return foreignAssistant(message)
142
- }
143
- }
144
-
281
+ const source = message.source;
282
+ if (source.kind !== "model" || source.replayState === undefined)
283
+ return foreignAssistant(message);
284
+ try {
285
+ return replayedAssistant(message, source);
286
+ }
287
+ catch (cause) {
288
+ if (errorCode(cause) !== "LCX_COMPACT_INVALID_REPLAY_STATE")
289
+ throw cause;
290
+ if (typeof onReplayDegrade === "function")
291
+ onReplayDegrade(errorMessage(cause));
292
+ return foreignAssistant(message);
293
+ }
294
+ }
145
295
  async function piToolContent(blocks, ctx, options, imageMap) {
146
- const content = []
147
- for (const block of blocks ?? []) {
148
- if (block?.type === 'text') content.push({ type: 'text', text: String(block.text ?? '') })
149
- else if (block?.type === 'image') content.push(await piImagePart(block, ctx, options, imageMap))
150
- else if (block?.type === 'tool-result') content.push(...await piToolContent(block.content, ctx, options, imageMap))
151
- }
152
- return content.length > 0 ? content : [{ type: 'text', text: '(no output)' }]
153
- }
154
-
296
+ const content = [];
297
+ for (const block of blocks) {
298
+ if (block.type === "text")
299
+ content.push({ type: "text", text: block.text });
300
+ else if (block.type === "image")
301
+ content.push(...(await piImageParts(block, ctx, options, imageMap)));
302
+ else if (block.type === "reasoning")
303
+ continue;
304
+ else if (block.type === "tool-result")
305
+ content.push(...(await piToolContent(block.content, ctx, options, imageMap)));
306
+ else
307
+ throw unsupportedContent(block.type);
308
+ }
309
+ return content.length > 0 ? content : [{ type: "text", text: "(no output)" }];
310
+ }
311
+ function projectFileContent(blocks, fileRequestText) {
312
+ return blocks.map((block) => {
313
+ if (block.type === "file")
314
+ return { type: "text", text: fileRequestText(block.attachment) };
315
+ if (block.type === "tool-result")
316
+ return { ...block, content: projectFileContent(block.content, fileRequestText) };
317
+ return block;
318
+ });
319
+ }
320
+ function projectFiles(messages, ctx) {
321
+ const fileRequestText = ctx?.llm?.fileRequestText.bind(ctx.llm);
322
+ if (fileRequestText === undefined) {
323
+ for (const message of messages)
324
+ for (const block of message.content)
325
+ if (block.type === "file")
326
+ throw unsupportedContent("file (DSH fileRequestText API unavailable)");
327
+ return messages;
328
+ }
329
+ return messages.map((message) => ({
330
+ ...message,
331
+ content: projectFileContent(message.content, fileRequestText),
332
+ }));
333
+ }
334
+ function flattenMessageText(message) {
335
+ return message.content
336
+ .map((block) => (block.type === "text" ? block.text : ""))
337
+ .join("");
338
+ }
155
339
  async function dshToPiMessages(messages, ctx, options, imageMap) {
156
- const result = []
157
- for (const message of messages ?? []) {
158
- if (message?.role === 'system') continue
159
- if (message?.role === 'assistant') { result.push(toPiAssistant(message, options.onReplayDegrade)); continue }
160
- if (message?.role !== 'user') continue
161
- const ordinary = (message.content ?? []).filter((block) => block?.type === 'text' || block?.type === 'image')
162
- if (ordinary.length > 0) {
163
- const content = []
164
- for (const block of ordinary) content.push(block.type === 'text' ? { type: 'text', text: String(block.text ?? '') } : await piImagePart(block, ctx, options, imageMap))
165
- if (content.length > 0) result.push({ role: 'user', content, timestamp: 0 })
340
+ const result = [];
341
+ const toolNames = new Map();
342
+ for (const message of messages) {
343
+ if (message.role === "system") {
344
+ result.push({ role: "user", content: flattenMessageText(message), timestamp: 0 });
345
+ continue;
346
+ }
347
+ if (message.role === "assistant") {
348
+ const assistant = toPiAssistant(message, options.onReplayDegrade);
349
+ for (const block of assistant.content)
350
+ if (block.type === "toolCall")
351
+ toolNames.set(String(block.id), block.name);
352
+ result.push(assistant);
353
+ continue;
354
+ }
355
+ const ordinary = [];
356
+ const toolResults = [];
357
+ for (const block of message.content) {
358
+ if (block.type === "text" || block.type === "image")
359
+ ordinary.push(block);
360
+ else if (block.type === "reasoning")
361
+ continue;
362
+ else if (block.type === "tool-result")
363
+ toolResults.push(block);
364
+ else
365
+ throw unsupportedContent(block.type);
366
+ }
367
+ if (ordinary.length > 0) {
368
+ const content = [];
369
+ for (const block of ordinary) {
370
+ if (block.type === "text")
371
+ content.push({ type: "text", text: block.text });
372
+ else if (block.type === "image")
373
+ content.push(...(await piImageParts(block, ctx, options, imageMap)));
374
+ }
375
+ if (content.length > 0)
376
+ result.push({ role: "user", content, timestamp: 0 });
377
+ }
378
+ for (const block of toolResults) {
379
+ if (block.type !== "tool-result")
380
+ continue;
381
+ const toolCallId = String(block.toolCallId);
382
+ result.push({ role: "toolResult", toolCallId, toolName: toolNames.get(toolCallId) ?? "unknown", content: await piToolContent(block.content, ctx, options, imageMap), addedToolNames: [], isError: block.isError === true, timestamp: 0 });
383
+ }
166
384
  }
167
- for (const block of (message.content ?? []).filter((value) => value?.type === 'tool-result')) result.push({ role: 'toolResult', toolCallId: String(block.toolCallId), toolName: String(block.toolName ?? block.name ?? 'unknown'), content: await piToolContent(block.content, ctx, options, imageMap), addedToolNames: block.addedToolNames ?? message.addedToolNames ?? [], isError: block.isError === true, timestamp: 0 })
168
- }
169
- return result
385
+ return result;
170
386
  }
171
-
172
387
  function builtinResponsesModel(provider, modelId) {
173
- try { return getBuiltinModels(String(provider ?? '')).find((model) => model?.id === modelId && model?.api === 'openai-responses') }
174
- catch { return undefined }
388
+ try {
389
+ return getBuiltinModels(provider).find((model) => model.id === modelId && model.api === "openai-responses");
390
+ }
391
+ catch {
392
+ return undefined;
393
+ }
175
394
  }
176
-
177
395
  export function resolvePiResponsesModel(options) {
178
- const route = options.route ?? {}
179
- const explicit = options.model && typeof options.model === 'object' ? options.model : undefined
180
- const provider = String(explicit?.provider ?? route.provider ?? 'dsh-lcx-codex')
181
- const id = String(explicit?.id ?? route.model ?? 'unknown')
182
- const builtin = builtinResponsesModel(provider, id)
183
- const compat = { ...(builtin?.compat ?? {}), ...(options.responsesCompat ?? {}), ...(explicit?.compat ?? {}) }
184
- const input = options.imageSupport === 'supported' ? ['text', 'image'] : options.imageSupport === 'unsupported' ? ['text'] : (explicit?.input ?? builtin?.input ?? ['text'])
185
- return {
186
- ...(builtin ?? {}), ...(explicit ?? {}), id, name: String(explicit?.name ?? builtin?.name ?? id), api: 'openai-responses', provider,
187
- baseUrl: String(explicit?.baseUrl ?? route.baseURL ?? builtin?.baseUrl ?? ''), reasoning: typeof explicit?.reasoning === 'boolean' ? explicit.reasoning : (builtin?.reasoning ?? true), input,
188
- cost: explicit?.cost ?? builtin?.cost ?? { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, contextWindow: Number(explicit?.contextWindow ?? builtin?.contextWindow) > 0 ? Number(explicit?.contextWindow ?? builtin?.contextWindow) : 262144,
189
- maxTokens: Number(explicit?.maxTokens ?? builtin?.maxTokens) > 0 ? Number(explicit?.maxTokens ?? builtin?.maxTokens) : 32768,
190
- ...(Object.keys(compat).length > 0 ? { compat } : {}),
191
- }
192
- }
193
-
194
- function splitDeferredTools(context, enabled) {
195
- const unique = new Map()
196
- for (const tool of context.tools ?? []) if (tool?.name) unique.set(tool.name, tool)
197
- if (!enabled) return { immediate: [...unique.values()], deferred: new Map() }
198
- const deferredNames = new Set(); const usedNames = new Set()
199
- for (const message of context.messages ?? []) {
200
- if (message.role === 'assistant') {
201
- for (const block of message.content ?? []) if (block.type === 'toolCall') usedNames.add(block.name)
202
- } else if (message.role === 'toolResult') {
203
- for (const name of message.addedToolNames ?? []) if (!usedNames.has(name)) deferredNames.add(name)
204
- }
205
- }
206
- const immediate = []; const deferred = new Map()
207
- for (const [name, tool] of unique) { if (deferredNames.has(name)) deferred.set(name, tool); else immediate.push(tool) }
208
- return { immediate, deferred }
396
+ const route = isObject(options.route) ? options.route : {};
397
+ const explicit = isObject(options.model) ? options.model : {};
398
+ const provider = stringValue(explicit.provider, stringValue(route.provider, "dsh-lcx-codex"));
399
+ const id = stringValue(explicit.id, stringValue(route.model, "unknown"));
400
+ const builtin = builtinResponsesModel(provider, id);
401
+ const builtinRecord = isObject(builtin) ? builtin : {};
402
+ const compat = { ...(isObject(builtinRecord.compat) ? builtinRecord.compat : {}), ...(isObject(options.responsesCompat) ? options.responsesCompat : {}), ...(isObject(explicit.compat) ? explicit.compat : {}) };
403
+ const imageSupport = options.imageSupport === "supported" || options.imageSupport === "unsupported" ? options.imageSupport : "unknown";
404
+ const input = imageSupport === "supported" ? ["text", "image"] : imageSupport === "unsupported" ? ["text"] : Array.isArray(explicit.input) ? explicit.input : Array.isArray(builtinRecord.input) ? builtinRecord.input : ["text"];
405
+ return { ...builtinRecord, ...explicit, id, name: stringValue(explicit.name, stringValue(builtinRecord.name, id)), api: "openai-responses", provider, baseUrl: stringValue(explicit.baseUrl, stringValue(route.baseURL, stringValue(builtinRecord.baseUrl))), reasoning: typeof explicit.reasoning === "boolean" ? explicit.reasoning : typeof builtinRecord.reasoning === "boolean" ? builtinRecord.reasoning : true, input, cost: explicit.cost ?? builtinRecord.cost ?? { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, contextWindow: positiveSafeInteger(explicit.contextWindow ?? builtinRecord.contextWindow, 262144), maxTokens: positiveSafeInteger(explicit.maxTokens ?? builtinRecord.maxTokens, 32768), ...(Object.keys(compat).length > 0 ? { compat } : {}) };
209
406
  }
210
-
211
407
  export async function serializeDshMessages(messages, ctx, options = {}) {
212
- const imageMap = new Map()
213
- const normalized = {
214
- imageSupport: options.imageSupport ?? 'unknown', signal: options.signal, route: options.route ?? {}, model: options.model, responsesCompat: options.responsesCompat,
215
- systemPrompt: typeof options.systemPrompt === 'string' ? options.systemPrompt : undefined, includeSystemPrompt: options.includeSystemPrompt === true, onReplayDegrade: options.onReplayDegrade,
216
- maxRequestImageBytes: Number.isSafeInteger(options.maxRequestImageBytes) && options.maxRequestImageBytes > 0 ? options.maxRequestImageBytes : DEFAULT_MAX_REQUEST_IMAGE_BYTES,
217
- requestImagePixelBudget: Number.isSafeInteger(options.requestImagePixelBudget) && options.requestImagePixelBudget > 0 ? options.requestImagePixelBudget : DEFAULT_REQUEST_IMAGE_PIXEL_BUDGET,
218
- requestImageMaxBytes: Number.isSafeInteger(options.requestImageMaxBytes) && options.requestImageMaxBytes > 0 ? options.requestImageMaxBytes : DEFAULT_REQUEST_IMAGE_MAX_BYTES,
219
- }
220
- const projected = offloadRequestImagesWithPolicy(messages ?? [], { representation: 'base64', maxBytes: normalized.maxRequestImageBytes, byteQuantum: 1, byteLength: ref => Math.min(ref.bytes, normalized.requestImageMaxBytes) })
221
- const model = resolvePiResponsesModel(normalized)
222
- const context = { systemPrompt: normalized.systemPrompt, messages: await dshToPiMessages(projected, ctx, normalized, imageMap), tools: options.tools ?? [] }
223
- const supportsStrictMode = model.compat?.supportsStrictMode ?? false
224
- const supportsOpenAIGrammarTools = model.compat?.supportsOpenAIGrammarTools ?? false
225
- const supportsAdditionalTools = model.compat?.supportsAdditionalTools ?? false
226
- const supportsToolSearch = model.compat?.supportsToolSearch ?? false
227
- const deferredToolsMode = supportsAdditionalTools ? 'additional-tools' : supportsToolSearch ? 'tool-search' : undefined
228
- const grammarToolInputProperties = createGrammarToolInputProperties(context.tools, supportsOpenAIGrammarTools)
229
- const placement = splitDeferredTools(context, deferredToolsMode !== undefined)
230
- const toolOptions = { supportsStrictMode, supportsOpenAIGrammarTools }
231
- const input = convertResponsesMessages(model, context, new Set(['openai', 'openai-codex', 'opencode']), {
232
- includeSystemPrompt: normalized.includeSystemPrompt,
233
- grammarToolInputProperties,
234
- deferredTools: placement.deferred,
235
- deferredToolsMode,
236
- toolOptions,
237
- })
238
- const tools = options.tools === undefined ? undefined : convertResponsesTools(placement.immediate, toolOptions)
239
- return { input, imageMap, tools, model, grammarToolInputProperties, deferredToolsMode }
240
- }
241
-
408
+ const normalized = {
409
+ imageSupport: options.imageSupport === "supported" || options.imageSupport === "unsupported" ? options.imageSupport : "unknown",
410
+ signal: options.signal instanceof AbortSignal ? options.signal : undefined,
411
+ route: options.route ?? {}, model: options.model, responsesCompat: options.responsesCompat,
412
+ systemPrompt: typeof options.systemPrompt === "string" ? options.systemPrompt : undefined,
413
+ includeSystemPrompt: options.includeSystemPrompt === true, onReplayDegrade: options.onReplayDegrade,
414
+ maxRequestImageBytes: positiveSafeInteger(options.maxRequestImageBytes, DEFAULT_MAX_REQUEST_IMAGE_BYTES),
415
+ requestImagePixelBudget: positiveSafeInteger(options.requestImagePixelBudget, DEFAULT_REQUEST_IMAGE_PIXEL_BUDGET),
416
+ requestImageMaxBytes: positiveSafeInteger(options.requestImageMaxBytes, DEFAULT_REQUEST_IMAGE_MAX_BYTES),
417
+ tools: Array.isArray(options.tools) ? options.tools : [],
418
+ };
419
+ const imageMap = new Map();
420
+ const fileProjected = projectFiles(messages, ctx);
421
+ const attachments = attachmentServices(ctx?.attachments);
422
+ const projected = offloadRequestImagesWithPolicy(fileProjected, {
423
+ representation: "base64", maxBytes: normalized.maxRequestImageBytes, byteQuantum: 1,
424
+ byteLength: (ref) => Math.min(ref.bytes, normalized.requestImageMaxBytes),
425
+ placeholder: (ref) => offloadedImageText(ref, attachments === undefined ? undefined : imageAccess(ctx, attachments, ref)),
426
+ });
427
+ const model = resolvePiResponsesModel(normalized);
428
+ const context = { systemPrompt: normalized.systemPrompt, messages: await dshToPiMessages(projected, ctx, normalized, imageMap), tools: [...normalized.tools] };
429
+ const compat = isObject(model.compat) ? model.compat : {};
430
+ const supportsStrictMode = compat.supportsStrictMode === true;
431
+ const supportsOpenAIGrammarTools = compat.supportsOpenAIGrammarTools === true;
432
+ const supportsAdditionalTools = compat.supportsAdditionalTools === true;
433
+ const supportsToolSearch = compat.supportsToolSearch === true;
434
+ const deferredToolsMode = supportsAdditionalTools ? "additional-tools" : supportsToolSearch ? "tool-search" : undefined;
435
+ const grammarToolInputProperties = createGrammarToolInputProperties(context.tools, supportsOpenAIGrammarTools);
436
+ // DSH has no authoritative added-tool provenance; keep its full catalog immediate.
437
+ const immediateTools = new Map();
438
+ for (const tool of context.tools)
439
+ if (tool.name)
440
+ immediateTools.set(tool.name, tool);
441
+ const toolOptions = { supportsStrictMode, supportsOpenAIGrammarTools };
442
+ const piContext = {
443
+ systemPrompt: context.systemPrompt,
444
+ messages: context.messages,
445
+ tools: context.tools,
446
+ };
447
+ const piModel = model;
448
+ const convertedInput = convertResponsesMessages(piModel, piContext, new Set(["openai", "openai-codex", "opencode"]), { includeSystemPrompt: normalized.includeSystemPrompt, grammarToolInputProperties, deferredTools: new Map(), deferredToolsMode, toolOptions });
449
+ const input = restorePortableAssistantPhases(convertedInput, context.messages, { provider: model.provider, id: model.id, api: model.api });
450
+ const tools = options.tools === undefined ? undefined : convertResponsesTools([...immediateTools.values()], toolOptions);
451
+ return { input, imageMap, tools, model: piModel, grammarToolInputProperties, deferredToolsMode };
452
+ }
242
453
  export function responsesTools(tools) {
243
- if (tools === undefined) return undefined
244
- if (!Array.isArray(tools)) throw error('LCX Responses tools must be an array', 'LCX_COMPACT_INVALID_TOOLS')
245
- if (tools.every((tool) => isObject(tool) && typeof tool.type === 'string')) return structuredClone(tools)
246
- for (const tool of tools) if (!isObject(tool) || typeof tool.name !== 'string' || !tool.name) throw error('LCX Responses tool has no name', 'LCX_COMPACT_INVALID_TOOLS')
247
- return convertResponsesTools(tools, { supportsStrictMode: false, supportsOpenAIGrammarTools: false })
248
- }
249
-
250
- function isResponsesMessageItem(item) { return isObject(item) && typeof item.role === 'string' && (item.type === undefined || item.type === 'message') }
251
-
454
+ if (tools === undefined)
455
+ return undefined;
456
+ if (!Array.isArray(tools))
457
+ throw error("LCX Responses tools must be an array", "LCX_COMPACT_INVALID_TOOLS");
458
+ if (tools.every((tool) => isObject(tool) && typeof tool.type === "string"))
459
+ return structuredClone(tools);
460
+ for (const tool of tools)
461
+ if (!isObject(tool) || typeof tool.name !== "string" || !tool.name)
462
+ throw error("LCX Responses tool has no name", "LCX_COMPACT_INVALID_TOOLS");
463
+ return convertResponsesTools(tools, { supportsStrictMode: false, supportsOpenAIGrammarTools: false });
464
+ }
465
+ function isResponsesMessageItem(item) {
466
+ return isObject(item) && typeof item.role === "string" && (item.type === undefined || item.type === "message");
467
+ }
252
468
  function mapImageParts(value, mapper) {
253
- if (!Array.isArray(value)) return value
254
- return value.map((part) => {
255
- if (part?.type === 'input_image' || part?.type === 'dsh_image_attachment') return mapper(part)
256
- if (part?.type === 'function_call_output' && Array.isArray(part.output)) return { ...structuredClone(part), output: mapImageParts(part.output, mapper) }
257
- return structuredClone(part)
258
- })
259
- }
260
-
469
+ return value.map((part) => {
470
+ if (!isObject(part))
471
+ return structuredClone(part);
472
+ if (part.type === "input_image" || part.type === "dsh_image_attachment")
473
+ return mapper(part);
474
+ if (part.type === "function_call_output" && Array.isArray(part.output))
475
+ return { ...structuredClone(part), output: mapImageParts(part.output, mapper) };
476
+ return structuredClone(part);
477
+ });
478
+ }
261
479
  export function persistNativeImageReferences(output, imageMap) {
262
- const map = imageMap instanceof Map ? imageMap : new Map()
263
- const persist = (part) => {
264
- if (part.type !== 'input_image' || typeof part.image_url !== 'string') throw error('native compact returned an invalid image item', 'LCX_COMPACT_INVALID_RESPONSE')
265
- const ref = map.get(part.image_url)
266
- if (!ref) throw error('native compact returned an untracked image item', 'LCX_COMPACT_INVALID_RESPONSE')
267
- return { type: 'dsh_image_attachment', attachment: structuredClone(ref) }
268
- }
269
- return (output ?? []).map((item) => {
270
- if (isResponsesMessageItem(item) && Array.isArray(item.content)) return { ...structuredClone(item), content: mapImageParts(item.content, persist) }
271
- if (item?.type === 'function_call_output' && Array.isArray(item.output)) return { ...structuredClone(item), output: mapImageParts(item.output, persist) }
272
- return structuredClone(item)
273
- })
274
- }
275
-
480
+ const persist = (part) => {
481
+ if (part.type === "dsh_image_attachment" && isObject(part.attachment))
482
+ return structuredClone(part);
483
+ if (part.type !== "input_image" || typeof part.image_url !== "string")
484
+ throw error("native compact returned an invalid image item", "LCX_COMPACT_INVALID_RESPONSE");
485
+ const ref = imageMap.get(part.image_url);
486
+ if (ref === undefined)
487
+ throw error("native compact returned an untracked image item", "LCX_COMPACT_INVALID_RESPONSE");
488
+ return { type: "dsh_image_attachment", attachment: structuredClone(ref) };
489
+ };
490
+ return output.map((item) => {
491
+ if (!isObject(item))
492
+ return structuredClone(item);
493
+ if (isResponsesMessageItem(item) && Array.isArray(item.content))
494
+ return { ...structuredClone(item), content: mapImageParts(item.content, persist) };
495
+ if (item.type === "function_call_output" && Array.isArray(item.output))
496
+ return { ...structuredClone(item), output: mapImageParts(item.output, persist) };
497
+ return structuredClone(item);
498
+ });
499
+ }
276
500
  async function hydratePart(part, ctx, options) {
277
- if (part?.type !== 'dsh_image_attachment') return structuredClone(part)
278
- if (options.imageSupport === 'unsupported') return { type: 'input_text', text: '[image omitted because the target model does not support image input]' }
279
- const block = { type: 'image', attachment: part.attachment }
280
- const map = options.imageMap instanceof Map ? options.imageMap : new Map()
281
- return imagePart(block, ctx, options, map)
501
+ if (!isObject(part) || part.type !== "dsh_image_attachment")
502
+ return structuredClone(part);
503
+ const attachment = part.attachment;
504
+ if (!hasImageAttachment(attachment))
505
+ throw error("native compact returned an invalid image attachment", "LCX_COMPACT_INVALID_RESPONSE");
506
+ if (options.imageSupport === "unsupported")
507
+ return { type: "input_text", text: "[image omitted because the target model does not support image input]" };
508
+ return imagePart({ attachment }, ctx, options, options.imageMap);
282
509
  }
283
-
284
510
  export async function hydrateNativeImageReferences(output, ctx, options = {}) {
285
- const normalized = {
286
- imageSupport: options.imageSupport ?? 'unknown',
287
- signal: options.signal,
288
- maxRequestImageBytes: options.maxRequestImageBytes ?? DEFAULT_MAX_REQUEST_IMAGE_BYTES,
289
- requestImagePixelBudget: options.requestImagePixelBudget ?? DEFAULT_REQUEST_IMAGE_PIXEL_BUDGET,
290
- requestImageMaxBytes: options.requestImageMaxBytes ?? DEFAULT_REQUEST_IMAGE_MAX_BYTES,
291
- imageMap: options.imageMap,
292
- }
293
- const result = []
294
- for (const item of output ?? []) {
295
- if (isResponsesMessageItem(item) && Array.isArray(item.content)) {
296
- const content = []
297
- for (const part of item.content) content.push(await hydratePart(part, ctx, normalized))
298
- result.push({ ...structuredClone(item), content })
299
- } else if (item?.type === 'function_call_output' && Array.isArray(item.output)) {
300
- const value = []
301
- for (const part of item.output) value.push(await hydratePart(part, ctx, normalized))
302
- result.push({ ...structuredClone(item), output: value })
303
- } else result.push(structuredClone(item))
304
- }
305
- return result
511
+ const normalized = {
512
+ imageSupport: options.imageSupport === "supported" || options.imageSupport === "unsupported" ? options.imageSupport : "unknown",
513
+ signal: options.signal instanceof AbortSignal ? options.signal : undefined,
514
+ maxRequestImageBytes: positiveSafeInteger(options.maxRequestImageBytes, DEFAULT_MAX_REQUEST_IMAGE_BYTES),
515
+ requestImagePixelBudget: positiveSafeInteger(options.requestImagePixelBudget, DEFAULT_REQUEST_IMAGE_PIXEL_BUDGET),
516
+ requestImageMaxBytes: positiveSafeInteger(options.requestImageMaxBytes, DEFAULT_REQUEST_IMAGE_MAX_BYTES),
517
+ imageMap: options.imageMap instanceof Map ? options.imageMap : new Map(),
518
+ };
519
+ const result = [];
520
+ for (const item of asArray(output)) {
521
+ if (!isObject(item)) {
522
+ result.push(structuredClone(item));
523
+ continue;
524
+ }
525
+ if (isResponsesMessageItem(item) && Array.isArray(item.content)) {
526
+ const content = [];
527
+ for (const part of item.content)
528
+ content.push(await hydratePart(part, ctx, normalized));
529
+ result.push({ ...structuredClone(item), content });
530
+ }
531
+ else if (item.type === "function_call_output" && Array.isArray(item.output)) {
532
+ const value = [];
533
+ for (const part of item.output)
534
+ value.push(await hydratePart(part, ctx, normalized));
535
+ result.push({ ...structuredClone(item), output: value });
536
+ }
537
+ else
538
+ result.push(structuredClone(item));
539
+ }
540
+ return result;
306
541
  }