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/lib/compact.js DELETED
@@ -1,752 +0,0 @@
1
- import { createHash } from 'node:crypto'
2
- import { offloadDshRequestImages, resolveDshImage, serializeDshResponsesInput } from './dsh-pi-responses.js'
3
-
4
- const UNSUPPORTED_CHECKPOINT_PATTERN = /\[dsh-lcx-codex-checkpoint:[0-9a-f-]{36}\]/iu
5
- const PORTABLE_CHECKPOINT_PATTERN = /\[dsh-lcx-codex-v3-checkpoint:([0-9a-f-]{36})\]/giu
6
- const PORTABLE_HISTORY_TOKEN_BUDGET = 20_000
7
- export const NATIVE_RETAINED_MESSAGE_TOKEN_BUDGET = 64_000
8
- export const PORTABLE_HISTORY_BYTE_BUDGET = 2 * 1024 * 1024
9
- const UNSUPPORTED_IMAGE_PLACEHOLDER = '[image omitted because the target model does not support image input]'
10
- const NATIVE_IMAGE_TOKEN_ESTIMATE = 765
11
- const CONTEXTUAL_USER_PREFIXES = [
12
- '<environment_context>',
13
- '<user_instructions>',
14
- '<additional_context>',
15
- '<skills',
16
- '<token_budget>',
17
- '<model_switch>',
18
- ]
19
-
20
- function nativeContentPartTokens(part) {
21
- if (part?.type === 'input_image' || part?.type === 'dsh_image_attachment') return NATIVE_IMAGE_TOKEN_ESTIMATE
22
- if (typeof part?.text === 'string') return Math.ceil(part.text.length / 4)
23
- return Math.ceil(JSON.stringify(part ?? null).length / 4)
24
- }
25
-
26
- function nativeMessageTokens(item) {
27
- return Math.max(1, (item?.content ?? []).reduce((total, part) => total + nativeContentPartTokens(part), 0))
28
- }
29
-
30
- function truncatedNativeText(text, maxTokens) {
31
- const maxChars = maxTokens * 4
32
- if (maxChars <= 0) return ''
33
- if (text.length <= maxChars) return text
34
- const omittedTokens = Math.max(1, Math.ceil((text.length - maxChars) / 4))
35
- const marker = `[...${omittedTokens} tokens truncated...]`
36
- if (maxChars <= marker.length + 2) return text.slice(-maxChars)
37
- const sideChars = Math.max(1, Math.floor((maxChars - marker.length) / 2))
38
- return `${text.slice(0, sideChars)}${marker}${text.slice(-sideChars)}`
39
- }
40
-
41
- function truncateNativeUserMessage(item, maxTokens) {
42
- let remaining = maxTokens
43
- const content = []
44
- for (const part of item?.content ?? []) {
45
- if (remaining <= 0) break
46
- const tokens = Math.max(1, nativeContentPartTokens(part))
47
- if (tokens <= remaining) {
48
- content.push(structuredClone(part))
49
- remaining -= tokens
50
- continue
51
- }
52
- if (typeof part?.text === 'string') {
53
- const text = truncatedNativeText(part.text, remaining)
54
- if (text) content.push({ ...structuredClone(part), text })
55
- }
56
- remaining = 0
57
- }
58
- return content.length > 0 ? { ...structuredClone(item), content } : undefined
59
- }
60
-
61
- function isNativeRetainedUserMessage(item) {
62
- if (item?.type !== 'message' || item.role !== 'user' || !Array.isArray(item.content)) return false
63
- return !item.content.some((part) => {
64
- if (part?.type !== 'input_text' || typeof part.text !== 'string') return false
65
- const text = part.text.trimStart().toLowerCase()
66
- return CONTEXTUAL_USER_PREFIXES.some((prefix) => text.startsWith(prefix))
67
- })
68
- }
69
-
70
- export function buildNativeReplacementHistory(input, compaction, options = {}) {
71
- if (compaction?.type !== 'compaction' || typeof compaction.encrypted_content !== 'string' || compaction.encrypted_content.length === 0) {
72
- const error = new Error('LCX Native replacement history requires one non-empty compaction item')
73
- error.code = 'LCX_COMPACT_INVALID_RESPONSE'
74
- throw error
75
- }
76
- let remaining = options.tokenBudget ?? NATIVE_RETAINED_MESSAGE_TOKEN_BUDGET
77
- if (!Number.isSafeInteger(remaining) || remaining <= 0) {
78
- const error = new Error('LCX Native retained message token budget must be a positive integer')
79
- error.code = 'LCX_COMPACT_INVALID_INPUT'
80
- throw error
81
- }
82
- const retained = []
83
- const candidates = (input ?? []).filter(isNativeRetainedUserMessage)
84
- for (let index = candidates.length - 1; index >= 0 && remaining > 0; index -= 1) {
85
- const item = candidates[index]
86
- const tokens = nativeMessageTokens(item)
87
- if (tokens <= remaining) {
88
- retained.push(structuredClone(item))
89
- remaining -= tokens
90
- continue
91
- }
92
- const truncated = truncateNativeUserMessage(item, remaining)
93
- if (truncated) retained.push(truncated)
94
- remaining = 0
95
- }
96
- retained.reverse()
97
- retained.push(structuredClone(compaction))
98
- return retained
99
- }
100
-
101
- export function checkpointNativeReplacementHistory(record) {
102
- const nativeOutput = Array.isArray(record?.nativeOutput) ? record.nativeOutput : []
103
- if (nativeOutput.some(isNativeRetainedUserMessage)) return structuredClone(nativeOutput)
104
- const compaction = nativeOutput.find((item) => item?.type === 'compaction') ?? record?.nativeCompaction
105
- if (!compaction) return structuredClone(nativeOutput)
106
- return buildNativeReplacementHistory(record?.portableHistory ?? [], compaction)
107
- }
108
-
109
- export function portableCheckpointIds(text) {
110
- return [...String(text ?? '').matchAll(PORTABLE_CHECKPOINT_PATTERN)].map((match) => match[1].toLowerCase())
111
- }
112
-
113
- export function textOfContent(content) {
114
- return (Array.isArray(content) ? content : [])
115
- .filter((block) => block?.type === 'text' && typeof block.text === 'string')
116
- .map((block) => block.text)
117
- .join('')
118
- }
119
-
120
- export function textOfMessage(message) {
121
- return textOfContent(message?.content)
122
- }
123
-
124
- function toolResultText(block) {
125
- return textOfContent(block?.content) || '(no output)'
126
- }
127
-
128
- function unsupportedPortableContentError(type) {
129
- const error = new Error(`LCX Compact cannot safely portable-replay message content type: ${String(type)}`)
130
- error.code = 'LCX_CHECKPOINT_PORTABLE_UNSUPPORTED_CONTENT'
131
- return error
132
- }
133
-
134
- function compactImageError(message, code, cause) {
135
- const error = new Error(message, cause === undefined ? undefined : { cause })
136
- error.code = code
137
- return error
138
- }
139
-
140
- function containsImage(content) {
141
- return (Array.isArray(content) ? content : []).some((block) =>
142
- block?.type === 'image' || (block?.type === 'tool-result' && containsImage(block.content)))
143
- }
144
-
145
- export function inputImageCount(input) {
146
- return (input ?? []).reduce((count, item) => {
147
- const content = item?.type === 'message' ? item.content : item?.type === 'function_call_output' ? item.output : undefined
148
- return count + (Array.isArray(content)
149
- ? content.filter((part) => part?.type === 'input_image' || part?.type === 'dsh_image_attachment').length
150
- : 0)
151
- }, 0)
152
- }
153
-
154
- async function inputImagePart(block, options = {}) {
155
- const image = await resolveDshImage(block, options)
156
- return { type: 'input_image', image_url: `data:${image.mimeType};base64,${image.data}` }
157
- }
158
-
159
- function assertSupportedBlocks(blocks, { allowImage }) {
160
- const supported = new Set(['text', 'tool-call', 'tool-result', 'reasoning', ...(allowImage ? ['image'] : [])])
161
- for (const block of Array.isArray(blocks) ? blocks : []) {
162
- if (!supported.has(block?.type)) throw unsupportedPortableContentError(block?.type)
163
- if (block.type === 'tool-result') assertSupportedBlocks(block.content, { allowImage })
164
- }
165
- }
166
-
167
- async function toolResultOutputWithImages(block, options) {
168
- const parts = []
169
- let text = ''
170
- const flushText = () => {
171
- if (text) parts.push({ type: 'input_text', text })
172
- text = ''
173
- }
174
- for (const part of block?.content ?? []) {
175
- if (part?.type === 'text') text += part.text
176
- else if (part?.type === 'image') {
177
- flushText()
178
- parts.push(await inputImagePart(part, options))
179
- } else if (part?.type === 'tool-result') {
180
- const nested = await toolResultOutputWithImages(part, options)
181
- if (typeof nested === 'string') text += nested === '(no output)' ? '' : nested
182
- else {
183
- flushText()
184
- parts.push(...nested)
185
- }
186
- }
187
- }
188
- flushText()
189
- return parts.some((part) => part.type === 'input_image') ? parts : parts.map((part) => part.text).join('') || '(no output)'
190
- }
191
-
192
- async function messageInputItemsWithImages(message, options = {}) {
193
- const blocks = Array.isArray(message?.content) ? message.content : []
194
- if (!containsImage(blocks)) return messageInputItems(message, options)
195
- if (message?.role !== 'user') {
196
- throw compactImageError('LCX Compact only supports image attachments in user messages or tool results', 'LCX_COMPACT_IMAGE_UNSUPPORTED')
197
- }
198
- if (options.strict) assertSupportedBlocks(blocks, { allowImage: true })
199
- const content = []
200
- let pendingText = ''
201
- const flushText = () => {
202
- if (pendingText) content.push({ type: 'input_text', text: pendingText })
203
- pendingText = ''
204
- }
205
- for (const block of blocks) {
206
- if (block?.type === 'text') pendingText += block.text
207
- else if (block?.type === 'image') {
208
- flushText()
209
- content.push(await inputImagePart(block, options))
210
- }
211
- }
212
- flushText()
213
- const items = []
214
- if (content.length > 0) items.push({ type: 'message', role: 'user', content })
215
- for (const call of blocks.filter((block) => block?.type === 'tool-call')) {
216
- items.push({
217
- type: 'function_call',
218
- call_id: String(call.id),
219
- name: String(call.name),
220
- arguments: typeof call.arguments === 'string' ? call.arguments : JSON.stringify(call.arguments ?? {}),
221
- })
222
- }
223
- for (const result of blocks.filter((block) => block?.type === 'tool-result')) {
224
- items.push({
225
- type: 'function_call_output',
226
- call_id: String(result.toolCallId),
227
- output: containsImage(result.content) ? await toolResultOutputWithImages(result, options) : toolResultText(result),
228
- })
229
- }
230
- return items
231
- }
232
-
233
- export async function normalInputWithImages(messages, options = {}) {
234
- const input = []
235
- for (const message of messages ?? []) {
236
- if (message?.role === 'system') continue
237
- input.push(...await messageInputItemsWithImages(message, options))
238
- }
239
- return input
240
- }
241
-
242
- function mapImageContent(value, mapper) {
243
- if (!Array.isArray(value)) return value
244
- return value.map((part) => {
245
- if (part?.type === 'input_image' || part?.type === 'dsh_image_attachment') return mapper(part)
246
- return part?.type === 'function_call_output' && Array.isArray(part.output)
247
- ? { ...part, output: mapImageContent(part.output, mapper) }
248
- : structuredClone(part)
249
- })
250
- }
251
-
252
- export function persistNativeImageReferences(output, references) {
253
- const referenceMap = references instanceof Map ? references : new Map()
254
- const persistPart = (part) => {
255
- if (part.type !== 'input_image' || typeof part.image_url !== 'string') {
256
- throw compactImageError('LCX Compact response contains an invalid image item', 'LCX_COMPACT_INVALID_RESPONSE')
257
- }
258
- const attachment = referenceMap.get(part.image_url)
259
- if (!attachment) {
260
- throw compactImageError('LCX Compact response contains an untracked image item', 'LCX_COMPACT_INVALID_RESPONSE')
261
- }
262
- return { type: 'dsh_image_attachment', attachment: structuredClone(attachment) }
263
- }
264
- return (output ?? []).map((item) => {
265
- if (item?.type === 'message' && Array.isArray(item.content)) {
266
- return { ...structuredClone(item), content: mapImageContent(item.content, persistPart) }
267
- }
268
- if (item?.type === 'function_call_output' && Array.isArray(item.output)) {
269
- return { ...structuredClone(item), output: mapImageContent(item.output, persistPart) }
270
- }
271
- return structuredClone(item)
272
- })
273
- }
274
-
275
- async function hydrateImageContent(content, options) {
276
- const hydrated = []
277
- for (const part of content ?? []) {
278
- if (part?.type === 'dsh_image_attachment') {
279
- hydrated.push(await inputImagePart({ type: 'image', attachment: part.attachment }, options))
280
- } else {
281
- hydrated.push(structuredClone(part))
282
- }
283
- }
284
- return hydrated
285
- }
286
-
287
- export async function hydrateNativeImageReferences(output, options = {}) {
288
- const hydrated = []
289
- for (const item of output ?? []) {
290
- if (item?.type === 'message' && Array.isArray(item.content)) {
291
- hydrated.push({ ...structuredClone(item), content: await hydrateImageContent(item.content, options) })
292
- } else if (item?.type === 'function_call_output' && Array.isArray(item.output)) {
293
- hydrated.push({ ...structuredClone(item), output: await hydrateImageContent(item.output, options) })
294
- } else {
295
- hydrated.push(structuredClone(item))
296
- }
297
- }
298
- return hydrated
299
- }
300
-
301
- function messageInputItems(message, { strict = false } = {}) {
302
- const blocks = Array.isArray(message?.content) ? message.content : []
303
- if (strict) assertSupportedBlocks(blocks, { allowImage: false })
304
- const text = textOfContent(blocks)
305
- const calls = blocks.filter((block) => block?.type === 'tool-call')
306
- const results = blocks.filter((block) => block?.type === 'tool-result')
307
- const items = []
308
-
309
- if (message?.role === 'assistant') {
310
- if (text) items.push({ type: 'message', role: 'assistant', content: [{ type: 'output_text', text }] })
311
- for (const call of calls) {
312
- items.push({
313
- type: 'function_call',
314
- call_id: String(call.id),
315
- name: String(call.name),
316
- arguments: typeof call.arguments === 'string' ? call.arguments : JSON.stringify(call.arguments ?? {}),
317
- })
318
- }
319
- return items
320
- }
321
-
322
- if (text) items.push({ type: 'message', role: 'user', content: [{ type: 'input_text', text }] })
323
- for (const result of results) {
324
- items.push({
325
- type: 'function_call_output',
326
- call_id: String(result.toolCallId),
327
- output: toolResultText(result),
328
- })
329
- }
330
- return items
331
- }
332
-
333
- export function normalInput(messages, options = {}) {
334
- const input = []
335
- for (const message of messages ?? []) {
336
- if (message?.role === 'system') continue
337
- input.push(...messageInputItems(message, options))
338
- }
339
- return input
340
- }
341
-
342
- export function routeFingerprint(route) {
343
- // Keep the final empty slot for compatibility with v3 fingerprints already
344
- // written before the nonexistent GenerateOptions.branchId field was removed.
345
- const value = [route?.provider ?? '', route?.model ?? '', route?.baseURL ?? '', route?.sessionId ?? '', ''].join('\u001f')
346
- return createHash('sha256').update(value, 'utf8').digest('hex')
347
- }
348
-
349
- function checkpointRouteMismatchDimensions(record, route) {
350
- const dimensions = []
351
- if (record.provider !== String(route?.provider ?? '')) dimensions.push('provider')
352
- if (record.model !== String(route?.model ?? '')) dimensions.push('model')
353
- if ((record.sessionId ?? '') !== String(route?.sessionId ?? '')) dimensions.push('session')
354
- if (dimensions.length === 0) dimensions.push('base URL or route configuration')
355
- return dimensions
356
- }
357
-
358
- export function assertCheckpointRoute(record, route) {
359
- const expected = routeFingerprint(route)
360
- const semanticMismatch = record.modelKey !== `${record.provider}:${record.model}` ||
361
- record.provider !== String(route?.provider ?? '') ||
362
- record.model !== String(route?.model ?? '')
363
- if (semanticMismatch || record.routeFingerprint !== expected) {
364
- const dimensions = checkpointRouteMismatchDimensions(record, route)
365
- const guidance = dimensions.includes('session')
366
- ? 'Resume the original DSH session or start a new session without this checkpoint marker.'
367
- : 'Use the original provider/model/base URL or create a new checkpoint.'
368
- const error = new Error(`LCX checkpoint route mismatch (${dimensions.join(', ')}): checkpoint=${record.routeFingerprint ?? 'missing'} current=${expected}. ${guidance}`)
369
- error.code = 'LCX_CHECKPOINT_ROUTE_MISMATCH'
370
- throw error
371
- }
372
- }
373
-
374
- function assertNoUnsupportedCheckpointMarker(messages) {
375
- for (const message of messages ?? []) {
376
- if (message?.role !== 'user') continue
377
- if (!UNSUPPORTED_CHECKPOINT_PATTERN.test(textOfMessage(message))) continue
378
- const error = new Error('LCX checkpoint v2 markers are unsupported; use a v3 checkpoint')
379
- error.code = 'LCX_CHECKPOINT_V2_UNSUPPORTED'
380
- throw error
381
- }
382
- }
383
-
384
- export function baseURLFingerprint(baseURL) {
385
- return createHash('sha256').update(String(baseURL ?? '').replace(/\/+$/u, ''), 'utf8').digest('hex')
386
- }
387
-
388
- export function latestPortableMarker(messages) {
389
- assertNoUnsupportedCheckpointMarker(messages)
390
- let found
391
- for (let index = 0; index < (messages?.length ?? 0); index += 1) {
392
- if (messages[index]?.role !== 'user') continue
393
- const ids = portableCheckpointIds(textOfMessage(messages[index]))
394
- if (ids.length > 1) {
395
- const error = new Error('LCX message contains multiple v3 checkpoint markers')
396
- error.code = 'LCX_CHECKPOINT_V3_CORRUPT'
397
- throw error
398
- }
399
- if (ids.length === 1) found = { id: ids[0], index }
400
- }
401
- return found
402
- }
403
-
404
- export function hasPortableCheckpoint(messages) {
405
- return latestPortableMarker(messages) !== undefined
406
- }
407
-
408
- export function buildPortableHistory(input, options = {}) {
409
- const tokenBudget = options.tokenBudget ?? PORTABLE_HISTORY_TOKEN_BUDGET
410
- const byteBudget = options.byteBudget ?? PORTABLE_HISTORY_BYTE_BUDGET
411
- let remainingChars = tokenBudget * 4
412
- let remainingBytes = byteBudget
413
- const candidates = []
414
- for (const item of input ?? []) {
415
- if (item === null || typeof item !== 'object' || Array.isArray(item)) continue
416
- if (typeof item.type !== 'string') continue
417
- if (item.type === 'compaction' || item.type === 'context_compaction' || item.type === 'compaction_trigger') continue
418
- if (item.type.startsWith('response.')) continue
419
- if (Object.prototype.hasOwnProperty.call(item, 'encrypted_content')) continue
420
- let candidate = item
421
- if (item.type === 'message') {
422
- const expectedType = item.role === 'assistant' ? 'output_text' : item.role === 'user' ? 'input_text' : undefined
423
- const parts = Array.isArray(item.content) ? item.content : []
424
- const imageParts = parts.filter((part) => part?.type === 'input_image')
425
- if (imageParts.length > 0) {
426
- if (!options.omitInputImages) throw unsupportedPortableContentError('input_image')
427
- candidate = { ...item, content: parts.filter((part) => part?.type === expectedType) }
428
- if (candidate.content.length === 0) continue
429
- }
430
- if (!expectedType || !Array.isArray(candidate.content) || candidate.content.some((part) =>
431
- !isObject(part) || (part.type !== 'dsh_image_attachment' &&
432
- (part.type !== expectedType || typeof part.text !== 'string')))) {
433
- throw unsupportedPortableContentError(item.type)
434
- }
435
- } else if (item.type === 'function_call_output' && Array.isArray(item.output)) {
436
- const imageParts = item.output.filter((part) => part?.type === 'input_image')
437
- if (imageParts.length > 0) {
438
- if (!options.omitInputImages) throw unsupportedPortableContentError('input_image')
439
- candidate = { ...item, output: item.output.filter((part) => part?.type !== 'input_image') }
440
- }
441
- if (candidate.output.some((part) => !isObject(part) ||
442
- (part.type !== 'dsh_image_attachment' &&
443
- (!['input_text', 'output_text'].includes(part.type) || typeof part.text !== 'string')))) {
444
- throw unsupportedPortableContentError('function_call_output')
445
- }
446
- } else if (item.type !== 'function_call' && item.type !== 'function_call_output') {
447
- throw unsupportedPortableContentError(item.type)
448
- }
449
- candidates.push(candidate)
450
- }
451
- const callsById = new Map()
452
- const outputsById = new Map()
453
- for (let index = 0; index < candidates.length; index += 1) {
454
- const item = candidates[index]
455
- if (item.type === 'function_call' && typeof item.call_id === 'string' && item.call_id.length > 0 &&
456
- typeof item.name === 'string' && item.name.length > 0 && typeof item.arguments === 'string') {
457
- const indexes = callsById.get(item.call_id) ?? []
458
- indexes.push(index)
459
- callsById.set(item.call_id, indexes)
460
- }
461
- if (item.type === 'function_call_output' && typeof item.call_id === 'string' && item.call_id.length > 0 &&
462
- Object.prototype.hasOwnProperty.call(item, 'output')) {
463
- const indexes = outputsById.get(item.call_id) ?? []
464
- indexes.push(index)
465
- outputsById.set(item.call_id, indexes)
466
- }
467
- }
468
-
469
- const pairedIndexes = new Set()
470
- const units = []
471
- for (const [callId, callIndexes] of callsById) {
472
- const outputIndexes = outputsById.get(callId)
473
- if (callIndexes.length !== 1 || outputIndexes?.length !== 1) continue
474
- const callIndex = callIndexes[0]
475
- const outputIndex = outputIndexes[0]
476
- if (callIndex >= outputIndex) continue
477
- const indexes = [callIndex, outputIndex]
478
- for (const index of indexes) pairedIndexes.add(index)
479
- units.push({ indexes, items: indexes.map((index) => candidates[index]) })
480
- }
481
- for (let index = 0; index < candidates.length; index += 1) {
482
- const item = candidates[index]
483
- if (pairedIndexes.has(index)) continue
484
- if (item.type === 'function_call' || item.type === 'function_call_output') continue
485
- units.push({ indexes: [index], items: [item] })
486
- }
487
- units.sort((left, right) => left.indexes[0] - right.indexes[0])
488
-
489
- const retainedUnits = []
490
- for (let index = units.length - 1; index >= 0; index -= 1) {
491
- const unit = units[index]
492
- let unitBytes = 0
493
- let unitChars = 0
494
- for (const item of unit.items) {
495
- const serialized = JSON.stringify(item)
496
- unitBytes += Buffer.byteLength(serialized, 'utf8')
497
- unitChars += serialized.length
498
- }
499
- if (unitBytes > remainingBytes || unitChars > remainingChars) continue
500
- retainedUnits.push(unit)
501
- remainingBytes -= unitBytes
502
- remainingChars -= unitChars
503
- if (remainingBytes <= 128 || remainingChars <= 32) break
504
- }
505
-
506
- return retainedUnits
507
- .flatMap((unit) => unit.indexes.map((index, itemIndex) => ({ index, item: structuredClone(unit.items[itemIndex]) })))
508
- .sort((left, right) => left.index - right.index)
509
- .map((entry) => entry.item)
510
- }
511
-
512
- function portableCheckpointMismatchDimensions(record, route) {
513
- if (!record || !route) return ['route']
514
- const dimensions = []
515
- if (record.provider !== String(route.provider ?? '')) dimensions.push('provider')
516
- if (record.baseURLFingerprint !== baseURLFingerprint(route.baseURL)) dimensions.push('endpoint')
517
- const sessionId = String(route.sessionId ?? '')
518
- const sameSessionLineage = !sessionId || record.lineageId === sessionId ||
519
- (Array.isArray(route.ancestorSessionIds) && route.ancestorSessionIds.includes(record.lineageId))
520
- if (!sameSessionLineage) dimensions.push('session')
521
- return dimensions.length > 0 ? dimensions : ['route']
522
- }
523
-
524
- export function portableCheckpointState(record, route) {
525
- if (!record || !route) return 'route-mismatch'
526
- const currentSessionId = String(route.sessionId ?? '')
527
- const hasSessionIdentity = currentSessionId.length > 0
528
- const sameLineage = record.lineageId === currentSessionId ||
529
- (Array.isArray(route.ancestorSessionIds) && route.ancestorSessionIds.includes(record.lineageId))
530
- const sameProvider = record.provider === String(route.provider ?? '')
531
- const sameBaseURL = record.baseURLFingerprint === baseURLFingerprint(route.baseURL)
532
- if (hasSessionIdentity && sameLineage && record.routeFingerprint === routeFingerprint(route)) return 'native-compatible'
533
- if (sameProvider && sameBaseURL && (!hasSessionIdentity || sameLineage)) return 'portable-migratable'
534
- return 'route-mismatch'
535
- }
536
-
537
- function portableSummaryItem(summary) {
538
- if (typeof summary !== 'string' || summary.length === 0) return []
539
- return [{
540
- type: 'message',
541
- role: 'assistant',
542
- content: [{ type: 'output_text', text: summary }],
543
- }]
544
- }
545
-
546
- export function buildPortableResponsesInput(messages, store, route) {
547
- const marker = latestPortableMarker(messages)
548
- if (!marker) return normalInput(messages, { strict: true })
549
- const record = store.get(marker.id)
550
- if (!record) {
551
- const error = new Error(`LCX v3 checkpoint ${marker.id} is missing from ${store.file}`)
552
- error.code = 'LCX_CHECKPOINT_V3_CORRUPT'
553
- throw error
554
- }
555
- const state = portableCheckpointState(record, route)
556
- if (state === 'route-mismatch') {
557
- const dimensions = portableCheckpointMismatchDimensions(record, route)
558
- const error = new Error(`LCX v3 checkpoint cannot migrate; route mismatch dimensions: ${dimensions.join(', ')}`)
559
- error.code = 'LCX_CHECKPOINT_ROUTE_MISMATCH'
560
- throw error
561
- }
562
- const prefix = state === 'native-compatible'
563
- ? checkpointNativeReplacementHistory(record)
564
- : [...portableSummaryItem(record.portableSummary), ...buildPortableHistory(record.portableHistory)]
565
- const tail = normalInput((messages ?? []).slice(marker.index + 1), { strict: true })
566
- return [...structuredClone(prefix), ...tail]
567
- }
568
-
569
- export async function buildPortableResponsesInputWithImages(messages, store, route, options = {}) {
570
- const marker = latestPortableMarker(messages)
571
- const imageSupport = options.imageSupport ?? (typeof options.resolveImage === 'function' ? 'supported' : 'unknown')
572
- const requestMessages = offloadDshRequestImages(messages, options.maxRequestImageBytes)
573
- if (!marker) return serializeDshResponsesInput(requestMessages, { ...options, imageSupport, route })
574
- const record = store.get(marker.id)
575
- if (!record) {
576
- const error = new Error(`LCX v3 checkpoint ${marker.id} is missing from ${store.file}`)
577
- error.code = 'LCX_CHECKPOINT_V3_CORRUPT'
578
- throw error
579
- }
580
- const state = portableCheckpointState(record, route)
581
- if (state === 'route-mismatch') {
582
- const dimensions = portableCheckpointMismatchDimensions(record, route)
583
- const error = new Error(`LCX v3 checkpoint cannot migrate; route mismatch dimensions: ${dimensions.join(', ')}`)
584
- error.code = 'LCX_CHECKPOINT_ROUTE_MISMATCH'
585
- throw error
586
- }
587
- const portableImages = Number(record.portableImageCount ?? inputImageCount(record.portableHistory))
588
- const durableImages = inputImageCount(record.portableHistory)
589
- if (state === 'portable-migratable' && portableImages > durableImages) {
590
- throw compactImageError(`LCX checkpoint portable migration cannot restore ${portableImages} image attachment(s)`, 'LCX_CHECKPOINT_PORTABLE_UNSUPPORTED_CONTENT')
591
- }
592
- let portableHistory
593
- if (state === 'portable-migratable') {
594
- portableHistory = buildPortableHistory(record.portableHistory)
595
- if (durableImages > 0) {
596
- if (imageSupport === 'unknown') {
597
- throw compactImageError('LCX Compact cannot determine whether the target model accepts checkpoint images', 'LCX_COMPACT_IMAGE_CAPABILITY_UNKNOWN')
598
- }
599
- portableHistory = imageSupport === 'supported'
600
- ? await hydrateNativeImageReferences(portableHistory, options)
601
- : portableHistory.map((item) => {
602
- const key = item?.type === 'message' ? 'content' : item?.type === 'function_call_output' ? 'output' : undefined
603
- if (!key || !Array.isArray(item[key])) return structuredClone(item)
604
- const textType = item.type === 'message' && item.role === 'assistant' ? 'output_text' : 'input_text'
605
- return {
606
- ...structuredClone(item),
607
- [key]: item[key].map((part) => part?.type === 'dsh_image_attachment'
608
- ? { type: textType, text: UNSUPPORTED_IMAGE_PLACEHOLDER }
609
- : structuredClone(part)),
610
- }
611
- })
612
- }
613
- }
614
- const prefix = state === 'native-compatible'
615
- ? await hydrateNativeImageReferences(checkpointNativeReplacementHistory(record), options)
616
- : [...portableSummaryItem(record.portableSummary), ...portableHistory]
617
- const tail = state === 'native-compatible'
618
- ? await serializeDshResponsesInput(requestMessages.slice(marker.index + 1), { ...options, imageSupport, route })
619
- : await serializeDshResponsesInput(requestMessages.slice(marker.index + 1), { ...options, imageSupport, route })
620
- return [...prefix, ...tail]
621
- }
622
-
623
- function isObject(value) {
624
- return value !== null && typeof value === 'object' && !Array.isArray(value)
625
- }
626
-
627
- function compactOutputShape(response) {
628
- const output = Array.isArray(response?.output) ? response.output : []
629
- const types = output.slice(0, 20).map((item) => {
630
- const type = item && typeof item === 'object' && typeof item.type === 'string' ? item.type : typeof item
631
- return String(type).replace(/[^a-zA-Z0-9._:-]/gu, '').slice(0, 48) || 'unknown'
632
- })
633
- return `object=${String(response?.object ?? 'missing')} outputLength=${output.length} outputTypes=[${types.join(',')}]`
634
- }
635
-
636
- function invalidOutputItem(message) {
637
- const error = new Error(`LCX compact response contains an invalid output item: ${message}`)
638
- error.code = 'LCX_COMPACT_INVALID_RESPONSE'
639
- return error
640
- }
641
-
642
- function validContent(content) {
643
- return Array.isArray(content) && content.every((part) => isObject(part) && typeof part.type === 'string' &&
644
- (part.text === undefined || typeof part.text === 'string'))
645
- }
646
-
647
- function validateOutputItems(output) {
648
- const itemIds = new Set()
649
- const callIds = new Set()
650
- const outputIds = new Set()
651
- const calls = new Set()
652
- const outputs = new Set()
653
- for (const item of output) {
654
- if (!isObject(item) || typeof item.type !== 'string' || item.type.length === 0) throw invalidOutputItem('missing type')
655
- if (item.id !== undefined) {
656
- if (typeof item.id !== 'string' || item.id.length === 0 || itemIds.has(item.id)) throw invalidOutputItem('duplicate or invalid id')
657
- itemIds.add(item.id)
658
- }
659
- if (item.type === 'compaction_trigger' || item.type === 'context_compaction' || item.type.startsWith('response.')) {
660
- throw invalidOutputItem(`invalid trigger/type ${item.type}`)
661
- }
662
- if (item.type === 'message') {
663
- if (!['assistant', 'developer', 'system', 'user'].includes(item.role) || !validContent(item.content)) {
664
- throw invalidOutputItem('message shape')
665
- }
666
- continue
667
- }
668
- if (item.type === 'function_call') {
669
- if (typeof item.call_id !== 'string' || item.call_id.length === 0 || callIds.has(item.call_id) ||
670
- typeof item.name !== 'string' || item.name.length === 0 || typeof item.arguments !== 'string') {
671
- throw invalidOutputItem('function_call shape')
672
- }
673
- callIds.add(item.call_id)
674
- calls.add(item.call_id)
675
- continue
676
- }
677
- if (item.type === 'function_call_output') {
678
- if (typeof item.call_id !== 'string' || item.call_id.length === 0 || outputIds.has(item.call_id) ||
679
- !(typeof item.output === 'string' || Array.isArray(item.output))) {
680
- throw invalidOutputItem('function_call_output shape')
681
- }
682
- outputIds.add(item.call_id)
683
- outputs.add(item.call_id)
684
- continue
685
- }
686
- if (item.type === 'compaction') {
687
- if (typeof item.encrypted_content !== 'string' || item.encrypted_content.length === 0) {
688
- throw invalidOutputItem('compaction encrypted_content')
689
- }
690
- continue
691
- }
692
- if (item.type === 'reasoning') {
693
- if (item.encrypted_content !== undefined && typeof item.encrypted_content !== 'string') {
694
- throw invalidOutputItem('reasoning encrypted_content')
695
- }
696
- if (item.content !== undefined && !validContent(item.content)) throw invalidOutputItem('reasoning content')
697
- if (item.summary !== undefined && !validContent(item.summary)) throw invalidOutputItem('reasoning summary')
698
- continue
699
- }
700
- if (Object.prototype.hasOwnProperty.call(item, 'encrypted_content')) throw invalidOutputItem('encrypted non-compaction item')
701
- }
702
- for (const callId of outputs) {
703
- if (!calls.has(callId)) throw invalidOutputItem(`orphan function_call_output ${callId}`)
704
- }
705
- for (const callId of calls) {
706
- if (!outputs.has(callId)) throw invalidOutputItem(`orphan function_call ${callId}`)
707
- }
708
- }
709
-
710
- export function normalizeCompactionResponse(response) {
711
- if (response?.error) {
712
- const error = new Error(response.error.message ?? 'LCX compact request failed')
713
- error.code = 'LCX_COMPACT_HTTP_ERROR'
714
- throw error
715
- }
716
- if (response?.object !== undefined && !['response.compaction', 'response'].includes(response.object)) {
717
- const error = new Error(`Unexpected compact response object: ${String(response.object)}`)
718
- error.code = 'LCX_COMPACT_INVALID_RESPONSE'
719
- throw error
720
- }
721
- if (!Array.isArray(response?.output)) {
722
- const error = new Error('LCX compact response has no output array')
723
- error.code = 'LCX_COMPACT_INVALID_RESPONSE'
724
- throw error
725
- }
726
- if (response.output.some((item) => item?.type === 'compaction_trigger')) {
727
- throw invalidOutputItem('invalid trigger compaction_trigger')
728
- }
729
- const compactions = response.output.filter((item) => item?.type === 'compaction')
730
- if (compactions.length === 0) {
731
- const error = new Error(`LCX compact response has no compaction item (${compactOutputShape(response)})`)
732
- error.code = 'LCX_COMPACT_MISSING_ITEM'
733
- throw error
734
- }
735
- if (compactions.length !== 1) {
736
- const error = new Error(`LCX compact response has ${compactions.length} compaction items`)
737
- error.code = 'LCX_COMPACT_MULTIPLE_ITEMS'
738
- throw error
739
- }
740
- if (typeof compactions[0].encrypted_content !== 'string' || compactions[0].encrypted_content.length === 0) {
741
- const error = new Error('LCX compact response has empty encrypted_content')
742
- error.code = 'LCX_COMPACT_EMPTY_ENCRYPTED_CONTENT'
743
- throw error
744
- }
745
- validateOutputItems(response.output)
746
- const compaction = compactions[0]
747
- return {
748
- output: structuredClone(response.output),
749
- compaction: structuredClone(compaction),
750
- responseId: typeof response.id === 'string' ? response.id : undefined,
751
- }
752
- }