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

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