dsh-lcx-codex 0.3.3 → 0.4.0-rc.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.
- package/ARCHITECTURE.md +93 -0
- package/CHANGELOG.md +143 -41
- package/README.md +208 -100
- package/README_EN.md +133 -101
- package/assets/dsh-lcx-codex-banner.jpg +0 -0
- package/cordis.patch.yml +10 -4
- package/lib/client.js +117 -194
- package/lib/compact-v2.js +124 -273
- package/lib/dsh-responses.js +299 -0
- package/lib/index.js +548 -1593
- package/lib/json-store.js +34 -0
- package/lib/legacy-v3.js +20 -0
- package/lib/native-checkpoint.js +93 -0
- package/lib/responses-replay.js +281 -0
- package/lib/route.js +222 -0
- package/lib/service-mutex.js +72 -0
- package/lib/transport.js +59 -380
- package/lib/web-run-output.js +13 -152
- package/lib/web-search-alpha.js +26 -448
- package/lib/web-search-capability.js +8 -10
- package/lib/web-search-hosted.js +22 -372
- package/lib/web-search-ref-store.js +7 -7
- package/package.json +20 -14
- package/scripts/probe-alpha.mjs +28 -76
- package/scripts/validate-dsh-schema.mjs +31 -0
- package/lib/checkpoint-store-v3.js +0 -459
- package/lib/compact.js +0 -752
- package/lib/dsh-pi-responses.js +0 -339
- package/lib/private-file.js +0 -18
- package/lib/session-lease.js +0 -70
- package/lib/web-search-store.js +0 -181
|
@@ -0,0 +1,299 @@
|
|
|
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
|
+
|
|
12
|
+
function error(message, code) {
|
|
13
|
+
const value = new Error(message)
|
|
14
|
+
value.code = code
|
|
15
|
+
return value
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
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')
|
|
36
|
+
}
|
|
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
|
+
|
|
49
|
+
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
|
+
|
|
69
|
+
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 {} }
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
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 } }
|
|
79
|
+
}
|
|
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
|
+
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
|
+
}
|
|
102
|
+
return { response, blocks: value.blocks }
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function foreignAssistant(message) {
|
|
106
|
+
const source = message?.source?.kind === 'model' ? message.source : undefined
|
|
107
|
+
const content = []
|
|
108
|
+
for (const block of message?.content ?? []) {
|
|
109
|
+
if (block?.type === 'text') content.push({ type: 'text', text: String(block.text ?? '') })
|
|
110
|
+
else if (block?.type === 'reasoning') content.push({ type: 'thinking', thinking: String(block.text ?? '') })
|
|
111
|
+
else if (block?.type === 'tool-call') content.push({ type: 'toolCall', id: String(block.id), name: String(block.name), arguments: parseArguments(block.arguments) })
|
|
112
|
+
else if (block?.type === 'image') throw unsupportedContent('assistant image')
|
|
113
|
+
else throw unsupportedContent(block?.type)
|
|
114
|
+
}
|
|
115
|
+
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 }
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function replayedAssistant(message, source) {
|
|
119
|
+
const state = readDshPiReplayState(source.replayState)
|
|
120
|
+
if (state.response.provider !== source.provider) throw invalidReplay('provider does not match assistant source')
|
|
121
|
+
if (state.response.model !== source.model) throw invalidReplay('model does not match assistant source')
|
|
122
|
+
if (state.blocks.length !== (message?.content ?? []).length) throw invalidReplay('block count does not match assistant content')
|
|
123
|
+
const content = (message.content ?? []).map((block, index) => {
|
|
124
|
+
const replay = state.blocks[index]
|
|
125
|
+
if (replayBlockType(block?.type) !== replay?.type) throw invalidReplay(`block ${index} does not match assistant content`)
|
|
126
|
+
if (block.type === 'text') return { type: 'text', text: String(block.text ?? ''), ...(replay.textSignature === undefined ? {} : { textSignature: replay.textSignature }) }
|
|
127
|
+
if (block.type === 'reasoning') return { type: 'thinking', thinking: String(block.text ?? ''), ...(replay.thinkingSignature === undefined ? {} : { thinkingSignature: replay.thinkingSignature }), ...(replay.redacted === undefined ? {} : { redacted: replay.redacted }) }
|
|
128
|
+
return { type: 'toolCall', id: String(block.id), name: String(block.name), arguments: parseArguments(block.arguments), ...(replay.thoughtSignature === undefined ? {} : { thoughtSignature: replay.thoughtSignature }) }
|
|
129
|
+
})
|
|
130
|
+
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 }
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function toPiAssistant(message, onReplayDegrade) {
|
|
134
|
+
const source = message?.source
|
|
135
|
+
if (source?.kind !== 'model' || source.replayState === undefined) return foreignAssistant(message)
|
|
136
|
+
try { return replayedAssistant(message, source) }
|
|
137
|
+
catch (cause) {
|
|
138
|
+
if (cause?.code !== 'LCX_COMPACT_INVALID_REPLAY_STATE') throw cause
|
|
139
|
+
onReplayDegrade?.(cause.message)
|
|
140
|
+
return foreignAssistant(message)
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
async function piToolContent(blocks, ctx, options, imageMap) {
|
|
145
|
+
const content = []
|
|
146
|
+
for (const block of blocks ?? []) {
|
|
147
|
+
if (block?.type === 'text') content.push({ type: 'text', text: String(block.text ?? '') })
|
|
148
|
+
else if (block?.type === 'image') content.push(await piImagePart(block, ctx, options, imageMap))
|
|
149
|
+
else if (block?.type === 'tool-result') content.push(...await piToolContent(block.content, ctx, options, imageMap))
|
|
150
|
+
}
|
|
151
|
+
return content.length > 0 ? content : [{ type: 'text', text: '(no output)' }]
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
async function dshToPiMessages(messages, ctx, options, imageMap) {
|
|
155
|
+
const result = []
|
|
156
|
+
for (const message of messages ?? []) {
|
|
157
|
+
if (message?.role === 'system') continue
|
|
158
|
+
if (message?.role === 'assistant') { result.push(toPiAssistant(message, options.onReplayDegrade)); continue }
|
|
159
|
+
if (message?.role !== 'user') continue
|
|
160
|
+
const ordinary = (message.content ?? []).filter((block) => block?.type === 'text' || block?.type === 'image')
|
|
161
|
+
if (ordinary.length > 0) {
|
|
162
|
+
const content = []
|
|
163
|
+
for (const block of ordinary) content.push(block.type === 'text' ? { type: 'text', text: String(block.text ?? '') } : await piImagePart(block, ctx, options, imageMap))
|
|
164
|
+
if (content.length > 0) result.push({ role: 'user', content, timestamp: 0 })
|
|
165
|
+
}
|
|
166
|
+
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 })
|
|
167
|
+
}
|
|
168
|
+
return result
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function builtinResponsesModel(provider, modelId) {
|
|
172
|
+
try { return getBuiltinModels(String(provider ?? '')).find((model) => model?.id === modelId && model?.api === 'openai-responses') }
|
|
173
|
+
catch { return undefined }
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function piModel(options) {
|
|
177
|
+
const route = options.route ?? {}
|
|
178
|
+
const explicit = options.model && typeof options.model === 'object' ? options.model : undefined
|
|
179
|
+
const provider = String(explicit?.provider ?? route.provider ?? 'dsh-lcx-codex')
|
|
180
|
+
const id = String(explicit?.id ?? route.model ?? 'unknown')
|
|
181
|
+
const builtin = builtinResponsesModel(provider, id)
|
|
182
|
+
const compat = { ...(builtin?.compat ?? {}), ...(options.responsesCompat ?? {}), ...(explicit?.compat ?? {}) }
|
|
183
|
+
const input = options.imageSupport === 'supported' ? ['text', 'image'] : options.imageSupport === 'unsupported' ? ['text'] : (explicit?.input ?? builtin?.input ?? ['text'])
|
|
184
|
+
return {
|
|
185
|
+
...(builtin ?? {}), ...(explicit ?? {}), id, name: String(explicit?.name ?? builtin?.name ?? id), api: 'openai-responses', provider,
|
|
186
|
+
baseUrl: String(explicit?.baseUrl ?? route.baseURL ?? builtin?.baseUrl ?? ''), reasoning: typeof explicit?.reasoning === 'boolean' ? explicit.reasoning : (builtin?.reasoning ?? true), input,
|
|
187
|
+
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,
|
|
188
|
+
maxTokens: Number(explicit?.maxTokens ?? builtin?.maxTokens) > 0 ? Number(explicit?.maxTokens ?? builtin?.maxTokens) : 32768,
|
|
189
|
+
...(Object.keys(compat).length > 0 ? { compat } : {}),
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function splitDeferredTools(context, enabled) {
|
|
194
|
+
const unique = new Map()
|
|
195
|
+
for (const tool of context.tools ?? []) if (tool?.name) unique.set(tool.name, tool)
|
|
196
|
+
if (!enabled) return { immediate: [...unique.values()], deferred: new Map() }
|
|
197
|
+
const deferredNames = new Set(); const usedNames = new Set()
|
|
198
|
+
for (const message of context.messages ?? []) {
|
|
199
|
+
if (message.role === 'assistant') {
|
|
200
|
+
for (const block of message.content ?? []) if (block.type === 'toolCall') usedNames.add(block.name)
|
|
201
|
+
} else if (message.role === 'toolResult') {
|
|
202
|
+
for (const name of message.addedToolNames ?? []) if (!usedNames.has(name)) deferredNames.add(name)
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
const immediate = []; const deferred = new Map()
|
|
206
|
+
for (const [name, tool] of unique) { if (deferredNames.has(name)) deferred.set(name, tool); else immediate.push(tool) }
|
|
207
|
+
return { immediate, deferred }
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
export async function serializeDshMessages(messages, ctx, options = {}) {
|
|
211
|
+
const imageMap = new Map()
|
|
212
|
+
const normalized = {
|
|
213
|
+
imageSupport: options.imageSupport ?? 'unknown', signal: options.signal, route: options.route ?? {}, model: options.model, responsesCompat: options.responsesCompat,
|
|
214
|
+
systemPrompt: typeof options.systemPrompt === 'string' ? options.systemPrompt : undefined, includeSystemPrompt: options.includeSystemPrompt === true, onReplayDegrade: options.onReplayDegrade,
|
|
215
|
+
maxRequestImageBytes: Number.isSafeInteger(options.maxRequestImageBytes) && options.maxRequestImageBytes > 0 ? options.maxRequestImageBytes : DEFAULT_MAX_REQUEST_IMAGE_BYTES,
|
|
216
|
+
requestImagePixelBudget: Number.isSafeInteger(options.requestImagePixelBudget) && options.requestImagePixelBudget > 0 ? options.requestImagePixelBudget : DEFAULT_REQUEST_IMAGE_PIXEL_BUDGET,
|
|
217
|
+
requestImageMaxBytes: Number.isSafeInteger(options.requestImageMaxBytes) && options.requestImageMaxBytes > 0 ? options.requestImageMaxBytes : DEFAULT_REQUEST_IMAGE_MAX_BYTES,
|
|
218
|
+
}
|
|
219
|
+
const projected = offloadRequestImagesWithPolicy(messages ?? [], { representation: 'base64', maxBytes: normalized.maxRequestImageBytes, byteQuantum: 1, byteLength: ref => Math.min(ref.bytes, normalized.requestImageMaxBytes) })
|
|
220
|
+
const model = piModel(normalized)
|
|
221
|
+
const context = { systemPrompt: normalized.systemPrompt, messages: await dshToPiMessages(projected, ctx, normalized, imageMap), tools: options.tools ?? [] }
|
|
222
|
+
const supportsStrictMode = model.compat?.supportsStrictMode ?? false
|
|
223
|
+
const supportsOpenAIGrammarTools = model.compat?.supportsOpenAIGrammarTools ?? false
|
|
224
|
+
const supportsToolSearch = model.compat?.supportsToolSearch ?? false
|
|
225
|
+
const grammarToolInputProperties = createGrammarToolInputProperties(context.tools, supportsOpenAIGrammarTools)
|
|
226
|
+
const placement = splitDeferredTools(context, supportsToolSearch)
|
|
227
|
+
const toolOptions = { supportsStrictMode, supportsOpenAIGrammarTools }
|
|
228
|
+
const input = convertResponsesMessages(model, context, new Set(['openai', 'openai-codex', 'opencode']), {
|
|
229
|
+
includeSystemPrompt: normalized.includeSystemPrompt, grammarToolInputProperties, deferredTools: placement.deferred, toolOptions,
|
|
230
|
+
})
|
|
231
|
+
const tools = options.tools === undefined ? undefined : convertResponsesTools(placement.immediate, toolOptions)
|
|
232
|
+
return { input, imageMap, tools }
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
export function responsesTools(tools) {
|
|
236
|
+
if (tools === undefined) return undefined
|
|
237
|
+
if (!Array.isArray(tools)) throw error('LCX Responses tools must be an array', 'LCX_COMPACT_INVALID_TOOLS')
|
|
238
|
+
if (tools.every((tool) => isObject(tool) && typeof tool.type === 'string')) return structuredClone(tools)
|
|
239
|
+
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')
|
|
240
|
+
return convertResponsesTools(tools, { supportsStrictMode: false, supportsOpenAIGrammarTools: false })
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function isResponsesMessageItem(item) { return isObject(item) && typeof item.role === 'string' && (item.type === undefined || item.type === 'message') }
|
|
244
|
+
|
|
245
|
+
function mapImageParts(value, mapper) {
|
|
246
|
+
if (!Array.isArray(value)) return value
|
|
247
|
+
return value.map((part) => {
|
|
248
|
+
if (part?.type === 'input_image' || part?.type === 'dsh_image_attachment') return mapper(part)
|
|
249
|
+
if (part?.type === 'function_call_output' && Array.isArray(part.output)) return { ...structuredClone(part), output: mapImageParts(part.output, mapper) }
|
|
250
|
+
return structuredClone(part)
|
|
251
|
+
})
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
export function persistNativeImageReferences(output, imageMap) {
|
|
255
|
+
const map = imageMap instanceof Map ? imageMap : new Map()
|
|
256
|
+
const persist = (part) => {
|
|
257
|
+
if (part.type !== 'input_image' || typeof part.image_url !== 'string') throw error('native compact returned an invalid image item', 'LCX_COMPACT_INVALID_RESPONSE')
|
|
258
|
+
const ref = map.get(part.image_url)
|
|
259
|
+
if (!ref) throw error('native compact returned an untracked image item', 'LCX_COMPACT_INVALID_RESPONSE')
|
|
260
|
+
return { type: 'dsh_image_attachment', attachment: structuredClone(ref) }
|
|
261
|
+
}
|
|
262
|
+
return (output ?? []).map((item) => {
|
|
263
|
+
if (isResponsesMessageItem(item) && Array.isArray(item.content)) return { ...structuredClone(item), content: mapImageParts(item.content, persist) }
|
|
264
|
+
if (item?.type === 'function_call_output' && Array.isArray(item.output)) return { ...structuredClone(item), output: mapImageParts(item.output, persist) }
|
|
265
|
+
return structuredClone(item)
|
|
266
|
+
})
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
async function hydratePart(part, ctx, options) {
|
|
270
|
+
if (part?.type !== 'dsh_image_attachment') return structuredClone(part)
|
|
271
|
+
if (options.imageSupport === 'unsupported') return { type: 'input_text', text: '[image omitted because the target model does not support image input]' }
|
|
272
|
+
const block = { type: 'image', attachment: part.attachment }
|
|
273
|
+
const map = options.imageMap instanceof Map ? options.imageMap : new Map()
|
|
274
|
+
return imagePart(block, ctx, options, map)
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
export async function hydrateNativeImageReferences(output, ctx, options = {}) {
|
|
278
|
+
const normalized = {
|
|
279
|
+
imageSupport: options.imageSupport ?? 'unknown',
|
|
280
|
+
signal: options.signal,
|
|
281
|
+
maxRequestImageBytes: options.maxRequestImageBytes ?? DEFAULT_MAX_REQUEST_IMAGE_BYTES,
|
|
282
|
+
requestImagePixelBudget: options.requestImagePixelBudget ?? DEFAULT_REQUEST_IMAGE_PIXEL_BUDGET,
|
|
283
|
+
requestImageMaxBytes: options.requestImageMaxBytes ?? DEFAULT_REQUEST_IMAGE_MAX_BYTES,
|
|
284
|
+
imageMap: options.imageMap,
|
|
285
|
+
}
|
|
286
|
+
const result = []
|
|
287
|
+
for (const item of output ?? []) {
|
|
288
|
+
if (isResponsesMessageItem(item) && Array.isArray(item.content)) {
|
|
289
|
+
const content = []
|
|
290
|
+
for (const part of item.content) content.push(await hydratePart(part, ctx, normalized))
|
|
291
|
+
result.push({ ...structuredClone(item), content })
|
|
292
|
+
} else if (item?.type === 'function_call_output' && Array.isArray(item.output)) {
|
|
293
|
+
const value = []
|
|
294
|
+
for (const part of item.output) value.push(await hydratePart(part, ctx, normalized))
|
|
295
|
+
result.push({ ...structuredClone(item), output: value })
|
|
296
|
+
} else result.push(structuredClone(item))
|
|
297
|
+
}
|
|
298
|
+
return result
|
|
299
|
+
}
|