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.
@@ -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
+ }