dsh-lcx-codex 0.3.3 → 0.4.0-rc.8
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 +89 -0
- package/CHANGELOG.md +84 -43
- 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 +90 -278
- package/lib/dsh-responses.js +220 -0
- package/lib/index.js +491 -1632
- 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 +191 -0
- package/lib/route.js +137 -0
- package/lib/transport.js +58 -380
- package/lib/web-run-output.js +13 -152
- package/lib/web-search-alpha.js +26 -448
- package/lib/web-search-capability.js +12 -76
- package/lib/web-search-hosted.js +22 -372
- package/lib/web-search-ref-store.js +9 -69
- package/package.json +20 -17
- 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
package/lib/compact-v2.js
CHANGED
|
@@ -1,307 +1,119 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { consumeSse, fetchSseWithRetry } from './transport.js'
|
|
2
|
+
import { responsesTools } from './dsh-responses.js'
|
|
3
3
|
|
|
4
|
-
const REMOTE_COMPACTION_V2_FEATURE = 'remote_compaction_v2'
|
|
5
|
-
const DEFAULT_MAX_SSE_BYTES = 8 * 1024 * 1024
|
|
4
|
+
export const REMOTE_COMPACTION_V2_FEATURE = 'remote_compaction_v2'
|
|
6
5
|
|
|
7
|
-
function isObject(value) {
|
|
8
|
-
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
function protocolError(message, code = 'LCX_COMPACT_INVALID_SSE') {
|
|
12
|
-
const error = new Error(message)
|
|
13
|
-
error.code = code
|
|
14
|
-
return error
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
function outputItemIdentity(event) {
|
|
18
|
-
const outputIndex = Number.isInteger(event?.output_index) && event.output_index >= 0
|
|
19
|
-
? `index:${event.output_index}`
|
|
20
|
-
: undefined
|
|
21
|
-
const itemIds = [event?.item?.id, event?.item_id]
|
|
22
|
-
.filter((value) => typeof value === 'string' && value.length > 0)
|
|
23
|
-
.map((value) => `id:${value}`)
|
|
24
|
-
const uniqueItemIds = [...new Set(itemIds)]
|
|
25
|
-
if (uniqueItemIds.length > 1) {
|
|
26
|
-
throw protocolError('Native compaction SSE event contains conflicting item identities')
|
|
27
|
-
}
|
|
28
|
-
return { outputIndex, itemId: uniqueItemIds[0] }
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
function mergeOutputItem(event, items, recordsByIndex, recordsById) {
|
|
32
|
-
if (!isObject(event?.item)) return
|
|
33
|
-
const { outputIndex, itemId } = outputItemIdentity(event)
|
|
34
|
-
const mappedRecords = new Set()
|
|
35
|
-
if (outputIndex && recordsByIndex.has(outputIndex)) mappedRecords.add(recordsByIndex.get(outputIndex))
|
|
36
|
-
if (itemId && recordsById.has(itemId)) mappedRecords.add(recordsById.get(itemId))
|
|
37
|
-
if (mappedRecords.size > 1) {
|
|
38
|
-
throw protocolError('Native compaction SSE event maps output_index and item id to different items')
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
let record = mappedRecords.values().next().value
|
|
42
|
-
if (!record && event.type === 'response.output_item.done') {
|
|
43
|
-
const pending = items.filter((candidate) => !candidate.done &&
|
|
44
|
-
(!candidate.item.type || !event.item.type || candidate.item.type === event.item.type))
|
|
45
|
-
if (pending.length === 1) record = pending[0]
|
|
46
|
-
else if (pending.length > 1) {
|
|
47
|
-
throw protocolError('Native compaction SSE output item has ambiguous aliases')
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
if (!record) {
|
|
51
|
-
record = { item: {}, outputIndex: undefined, itemId: undefined, done: false }
|
|
52
|
-
items.push(record)
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
if (outputIndex && record.outputIndex && outputIndex !== record.outputIndex) {
|
|
56
|
-
throw protocolError('Native compaction SSE output_index changed for one item')
|
|
57
|
-
}
|
|
58
|
-
if (itemId && record.itemId && itemId !== record.itemId) {
|
|
59
|
-
throw protocolError('Native compaction SSE item id changed for one item')
|
|
60
|
-
}
|
|
61
|
-
if (outputIndex && recordsByIndex.has(outputIndex) && recordsByIndex.get(outputIndex) !== record) {
|
|
62
|
-
throw protocolError('Native compaction SSE output_index is reused by different items')
|
|
63
|
-
}
|
|
64
|
-
if (itemId && recordsById.has(itemId) && recordsById.get(itemId) !== record) {
|
|
65
|
-
throw protocolError('Native compaction SSE item id is reused by different items')
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
const previousEncryptedContent = typeof record.item.encrypted_content === 'string' && record.item.encrypted_content.length > 0
|
|
69
|
-
? record.item.encrypted_content
|
|
70
|
-
: undefined
|
|
71
|
-
Object.assign(record.item, structuredClone(event.item))
|
|
72
|
-
if ((!record.item.encrypted_content || typeof record.item.encrypted_content !== 'string') && previousEncryptedContent) {
|
|
73
|
-
record.item.encrypted_content = previousEncryptedContent
|
|
74
|
-
}
|
|
75
|
-
if (outputIndex) {
|
|
76
|
-
record.outputIndex = outputIndex
|
|
77
|
-
recordsByIndex.set(outputIndex, record)
|
|
78
|
-
}
|
|
79
|
-
if (itemId) {
|
|
80
|
-
record.itemId = itemId
|
|
81
|
-
recordsById.set(itemId, record)
|
|
82
|
-
}
|
|
83
|
-
if (event.type === 'response.output_item.done') record.done = true
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
function validCompletedOutput(output) {
|
|
87
|
-
if (!Array.isArray(output)) return false
|
|
88
|
-
try {
|
|
89
|
-
normalizeCompactionResponse({ object: 'response.compaction', output })
|
|
90
|
-
return true
|
|
91
|
-
} catch {
|
|
92
|
-
return false
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
function usageFrom(response) {
|
|
97
|
-
const usage = response?.usage
|
|
98
|
-
if (!isObject(usage)) return undefined
|
|
99
|
-
const totalInputTokens = Number(usage.input_tokens ?? 0)
|
|
100
|
-
const outputTokens = Number(usage.output_tokens ?? 0)
|
|
101
|
-
const cacheReadValue = Number(usage.input_tokens_details?.cached_tokens ?? usage.input_token_details?.cached_tokens ?? usage.prompt_tokens_details?.cached_tokens ?? 0)
|
|
102
|
-
const cacheWriteValue = Number(usage.input_tokens_details?.cache_write_tokens ?? usage.input_token_details?.cache_write_tokens ?? usage.prompt_tokens_details?.cache_write_tokens ?? 0)
|
|
103
|
-
const cacheReadTokens = Number.isFinite(cacheReadValue) && cacheReadValue > 0 ? cacheReadValue : 0
|
|
104
|
-
const cacheWriteTokens = Number.isFinite(cacheWriteValue) && cacheWriteValue > 0 ? cacheWriteValue : 0
|
|
105
|
-
const inputTokens = Number.isFinite(totalInputTokens)
|
|
106
|
-
? Math.max(0, totalInputTokens - cacheReadTokens - cacheWriteTokens)
|
|
107
|
-
: 0
|
|
108
|
-
const normalizedOutputTokens = Number.isFinite(outputTokens) && outputTokens > 0 ? outputTokens : 0
|
|
109
|
-
if (![inputTokens, cacheReadTokens, cacheWriteTokens, normalizedOutputTokens].some((value) => value > 0)) return undefined
|
|
110
|
-
return {
|
|
111
|
-
inputTokens,
|
|
112
|
-
outputTokens: normalizedOutputTokens,
|
|
113
|
-
...(cacheReadTokens > 0 ? { cacheReadTokens } : {}),
|
|
114
|
-
...(cacheWriteTokens > 0 ? { cacheWriteTokens } : {}),
|
|
115
|
-
...(Number.isFinite(usage.output_token_details?.reasoning_tokens) && usage.output_token_details.reasoning_tokens > 0
|
|
116
|
-
? { reasoningTokens: Number(usage.output_token_details.reasoning_tokens) }
|
|
117
|
-
: {}),
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
function validToolDeclaration(tool) {
|
|
122
|
-
if (!isObject(tool)) return false
|
|
123
|
-
if (tool.type === undefined) return typeof tool.name === 'string' && tool.name.length > 0
|
|
124
|
-
if (typeof tool.type !== 'string' || tool.type.length === 0) return false
|
|
125
|
-
if (tool.type === 'function' || tool.type === 'custom') {
|
|
126
|
-
const name = tool.name ?? tool.function?.name
|
|
127
|
-
return typeof name === 'string' && name.length > 0
|
|
128
|
-
}
|
|
129
|
-
return true
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
export function responsesTools(tools) {
|
|
133
|
-
if (tools === undefined) return undefined
|
|
134
|
-
if (!Array.isArray(tools) || !tools.every(validToolDeclaration)) {
|
|
135
|
-
throw protocolError('Native compaction tools contain an invalid declaration', 'LCX_COMPACT_INVALID_TOOLS')
|
|
136
|
-
}
|
|
137
|
-
return tools.map((tool) => {
|
|
138
|
-
if (tool.type !== undefined) return structuredClone(tool)
|
|
139
|
-
return {
|
|
140
|
-
type: 'function',
|
|
141
|
-
...structuredClone(tool),
|
|
142
|
-
description: typeof tool.description === 'string' ? tool.description : '',
|
|
143
|
-
parameters: tool.parameters && typeof tool.parameters === 'object' ? structuredClone(tool.parameters) : { type: 'object', properties: {} },
|
|
144
|
-
strict: false,
|
|
145
|
-
}
|
|
146
|
-
})
|
|
147
|
-
}
|
|
6
|
+
function isObject(value) { return value !== null && typeof value === 'object' && !Array.isArray(value) }
|
|
7
|
+
function fail(message, code = 'LCX_COMPACT_INVALID_RESPONSE') { const e = new Error(message); e.code = code; return e }
|
|
148
8
|
|
|
149
|
-
export function mergeFeatureHeader(headers) {
|
|
150
|
-
const result = { ...
|
|
9
|
+
export function mergeFeatureHeader(headers = {}) {
|
|
10
|
+
const result = { ...headers }
|
|
151
11
|
const key = Object.keys(result).find((name) => name.toLowerCase() === 'x-codex-beta-features')
|
|
152
|
-
const
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
result[key ?? 'x-codex-beta-features'] = features.join(',')
|
|
12
|
+
const values = String(key ? result[key] ?? '' : '').split(',').map((x) => x.trim()).filter(Boolean)
|
|
13
|
+
if (!values.includes(REMOTE_COMPACTION_V2_FEATURE)) values.push(REMOTE_COMPACTION_V2_FEATURE)
|
|
14
|
+
result[key ?? 'x-codex-beta-features'] = values.join(',')
|
|
156
15
|
return result
|
|
157
16
|
}
|
|
158
17
|
|
|
159
|
-
export function buildNativeCompactionBody({ model, input, instructions,
|
|
160
|
-
if (!Array.isArray(input)) throw
|
|
161
|
-
if (input.some((item) =>
|
|
162
|
-
throw protocolError('Native compaction input already contains compaction_trigger', 'LCX_COMPACT_DUPLICATE_TRIGGER')
|
|
163
|
-
}
|
|
18
|
+
export function buildNativeCompactionBody({ model, input, instructions, tools, promptCacheKey }) {
|
|
19
|
+
if (!Array.isArray(input)) throw fail('native compaction input must be an array', 'LCX_COMPACT_INVALID_INPUT')
|
|
20
|
+
if (input.some((item) => item?.type === 'compaction_trigger')) throw fail('native compaction input already contains compaction_trigger', 'LCX_COMPACT_DUPLICATE_TRIGGER')
|
|
164
21
|
const nativeTools = responsesTools(tools)
|
|
165
22
|
return {
|
|
166
23
|
model,
|
|
167
24
|
input: [...structuredClone(input), { type: 'compaction_trigger' }],
|
|
168
25
|
stream: true,
|
|
169
26
|
store: false,
|
|
27
|
+
...(instructions !== undefined ? { instructions } : {}),
|
|
170
28
|
...(nativeTools !== undefined ? { tools: nativeTools } : {}),
|
|
171
|
-
...(
|
|
172
|
-
...(promptCacheKey !== undefined && promptCacheKey !== null ? { prompt_cache_key: String(promptCacheKey) } : {}),
|
|
173
|
-
...(promptCacheRetention !== undefined && promptCacheRetention !== null ? { prompt_cache_retention: String(promptCacheRetention) } : {}),
|
|
29
|
+
...(promptCacheKey ? { prompt_cache_key: String(promptCacheKey) } : {}),
|
|
174
30
|
}
|
|
175
31
|
}
|
|
176
32
|
|
|
177
|
-
function
|
|
178
|
-
if (
|
|
179
|
-
const
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
}
|
|
33
|
+
function usageFrom(raw) {
|
|
34
|
+
if (!isObject(raw)) return undefined
|
|
35
|
+
const inputTotal = Number(raw.input_tokens ?? 0)
|
|
36
|
+
const output = Number(raw.output_tokens ?? 0)
|
|
37
|
+
const details = raw.input_tokens_details ?? raw.input_token_details ?? raw.prompt_tokens_details ?? {}
|
|
38
|
+
const cacheRead = Number(details.cached_tokens ?? 0)
|
|
39
|
+
const cacheWrite = Number(details.cache_write_tokens ?? 0)
|
|
40
|
+
const result = {
|
|
41
|
+
inputTokens: Math.max(0, Number.isFinite(inputTotal) ? inputTotal - (Number.isFinite(cacheRead) ? cacheRead : 0) - (Number.isFinite(cacheWrite) ? cacheWrite : 0) : 0),
|
|
42
|
+
outputTokens: Number.isFinite(output) && output > 0 ? output : 0,
|
|
43
|
+
}
|
|
44
|
+
if (Number.isFinite(cacheRead) && cacheRead > 0) result.cacheReadTokens = cacheRead
|
|
45
|
+
if (Number.isFinite(cacheWrite) && cacheWrite > 0) result.cacheWriteTokens = cacheWrite
|
|
46
|
+
const reasoning = Number(raw.output_tokens_details?.reasoning_tokens ?? raw.output_token_details?.reasoning_tokens ?? 0)
|
|
47
|
+
if (Number.isFinite(reasoning) && reasoning > 0) result.reasoningTokens = reasoning
|
|
48
|
+
return Object.values(result).some((v) => Number(v) > 0) ? result : undefined
|
|
188
49
|
}
|
|
189
50
|
|
|
190
|
-
|
|
191
|
-
if (!
|
|
192
|
-
const
|
|
193
|
-
|
|
194
|
-
const
|
|
195
|
-
|
|
196
|
-
const
|
|
197
|
-
const
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
const event = parseSseEvent(dataLines)
|
|
207
|
-
dataLines = []
|
|
208
|
-
if (!event) return
|
|
209
|
-
if (event.type === 'error' || event.type === 'response.failed' || event.type === 'response.incomplete') {
|
|
210
|
-
const message = event.error?.message ?? event.response?.error?.message ?? `Native compaction ended with ${String(event.type)}`
|
|
211
|
-
throw protocolError(message, 'LCX_COMPACT_UPSTREAM_ERROR')
|
|
212
|
-
}
|
|
213
|
-
if (event.type === 'response.output_item.added' || event.type === 'response.output_item.done') {
|
|
214
|
-
mergeOutputItem(event, eventItems, recordsByIndex, recordsById)
|
|
215
|
-
}
|
|
216
|
-
if (event.type === 'response.completed') {
|
|
217
|
-
sawTerminal = true
|
|
218
|
-
completedResponse = isObject(event.response) ? event.response : {}
|
|
219
|
-
completedOutput = Array.isArray(completedResponse.output) ? structuredClone(completedResponse.output) : undefined
|
|
51
|
+
function validateOutput(output) {
|
|
52
|
+
if (!Array.isArray(output) || output.length === 0) throw fail('native compaction output is empty')
|
|
53
|
+
const compactions = output.filter((item) => isObject(item) && item.type === 'compaction')
|
|
54
|
+
if (compactions.length !== 1) throw fail(`native compaction must return exactly one compaction item, got ${compactions.length}`)
|
|
55
|
+
const compact = compactions[0]
|
|
56
|
+
if (typeof compact.encrypted_content !== 'string' || compact.encrypted_content.length === 0) throw fail('native compaction item has no encrypted_content')
|
|
57
|
+
const calls = new Set()
|
|
58
|
+
const results = new Set()
|
|
59
|
+
for (const item of output) {
|
|
60
|
+
if (!isObject(item) || typeof item.type !== 'string') throw fail('native compaction output contains a malformed item')
|
|
61
|
+
if (item.type === 'function_call') {
|
|
62
|
+
if (typeof item.call_id !== 'string' || !item.call_id || calls.has(item.call_id)) throw fail('native compaction output contains an invalid function_call')
|
|
63
|
+
calls.add(item.call_id)
|
|
64
|
+
} else if (item.type === 'function_call_output') {
|
|
65
|
+
if (typeof item.call_id !== 'string' || !item.call_id || results.has(item.call_id)) throw fail('native compaction output contains an invalid function_call_output')
|
|
66
|
+
results.add(item.call_id)
|
|
220
67
|
}
|
|
221
68
|
}
|
|
69
|
+
for (const id of calls) if (!results.has(id)) throw fail(`native compaction output has orphan function_call ${id}`)
|
|
70
|
+
return { output: structuredClone(output), compaction: structuredClone(compact) }
|
|
71
|
+
}
|
|
222
72
|
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
73
|
+
export async function parseNativeCompactionSse(response, options = {}) {
|
|
74
|
+
const byIndex = new Map()
|
|
75
|
+
const byId = new Map()
|
|
76
|
+
let terminal
|
|
77
|
+
const mergeItem = (event) => {
|
|
78
|
+
if (!isObject(event?.item)) return
|
|
79
|
+
const index = Number.isInteger(event.output_index) ? event.output_index : undefined
|
|
80
|
+
const id = typeof event.item.id === 'string' ? event.item.id : typeof event.item_id === 'string' ? event.item_id : undefined
|
|
81
|
+
let record = (index !== undefined ? byIndex.get(index) : undefined) ?? (id ? byId.get(id) : undefined)
|
|
82
|
+
if (!record) record = {}
|
|
83
|
+
const encrypted = record.encrypted_content
|
|
84
|
+
Object.assign(record, structuredClone(event.item))
|
|
85
|
+
if (typeof record.encrypted_content !== 'string' && typeof encrypted === 'string') record.encrypted_content = encrypted
|
|
86
|
+
if (index !== undefined) byIndex.set(index, record)
|
|
87
|
+
if (id) byId.set(id, record)
|
|
88
|
+
}
|
|
89
|
+
await consumeSse(response, (event) => {
|
|
90
|
+
if (!isObject(event)) return
|
|
91
|
+
if (event.type === 'error' || event.type === 'response.failed' || event.type === 'response.incomplete') {
|
|
92
|
+
const e = fail(event.error?.message ?? event.response?.error?.message ?? `native compaction ended with ${event.type}`, 'LCX_COMPACT_UPSTREAM_ERROR')
|
|
93
|
+
e.status = event.error?.status
|
|
94
|
+
throw e
|
|
245
95
|
}
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
if (completedResponse.object !== undefined && !['response.compaction', 'response'].includes(completedResponse.object)) {
|
|
256
|
-
throw protocolError(`Native compaction returned unexpected response object: ${String(completedResponse.object)}`, 'LCX_COMPACT_INVALID_RESPONSE')
|
|
257
|
-
}
|
|
258
|
-
const mergedEventOutput = eventItems.map((record) => record.item)
|
|
259
|
-
const output = validCompletedOutput(completedOutput) ? completedOutput : mergedEventOutput
|
|
260
|
-
const normalized = normalizeCompactionResponse({
|
|
261
|
-
object: completedResponse.object ?? 'response.compaction',
|
|
262
|
-
id: typeof completedResponse.id === 'string' ? completedResponse.id : undefined,
|
|
263
|
-
output,
|
|
264
|
-
})
|
|
96
|
+
if (event.type === 'response.output_item.added' || event.type === 'response.output_item.done') mergeItem(event)
|
|
97
|
+
if (event.type === 'response.completed' || event.type === 'response.done') terminal = event.response
|
|
98
|
+
}, options)
|
|
99
|
+
if (!isObject(terminal)) throw fail('native compaction stream ended without response.completed', 'LCX_COMPACT_INCOMPLETE_SSE')
|
|
100
|
+
const eventItems = [...new Set([...byIndex.values(), ...byId.values()])]
|
|
101
|
+
const output = Array.isArray(terminal.output) && terminal.output.length > 0 ? terminal.output : eventItems
|
|
102
|
+
const normalized = validateOutput(output)
|
|
265
103
|
return {
|
|
104
|
+
object: terminal.object ?? 'response.compaction',
|
|
105
|
+
...(typeof terminal.id === 'string' ? { id: terminal.id } : {}),
|
|
266
106
|
...normalized,
|
|
267
|
-
usage: usageFrom(
|
|
107
|
+
usage: usageFrom(terminal.usage),
|
|
268
108
|
}
|
|
269
109
|
}
|
|
270
110
|
|
|
271
|
-
export async function requestNativeCompaction({
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
tools,
|
|
280
|
-
headers,
|
|
281
|
-
signal,
|
|
282
|
-
timeoutMs,
|
|
283
|
-
maxAttempts,
|
|
284
|
-
maxResponseBytes,
|
|
285
|
-
}) {
|
|
286
|
-
const body = buildNativeCompactionBody({ model, input, instructions, promptCacheKey, promptCacheRetention, tools })
|
|
287
|
-
const requestHeaders = { ...(headers ?? {}) }
|
|
288
|
-
if (idempotencyKey !== undefined && idempotencyKey !== null && !Object.keys(requestHeaders).some((name) => name.toLowerCase() === 'idempotency-key')) {
|
|
289
|
-
requestHeaders['idempotency-key'] = String(idempotencyKey)
|
|
290
|
-
}
|
|
291
|
-
const featureHeaders = mergeFeatureHeader(requestHeaders)
|
|
292
|
-
return fetchSseWithRetry(
|
|
293
|
-
`${String(baseURL).replace(/\/+$/u, '')}/responses`,
|
|
294
|
-
body,
|
|
295
|
-
featureHeaders,
|
|
296
|
-
signal,
|
|
297
|
-
timeoutMs,
|
|
298
|
-
{
|
|
299
|
-
maxAttempts,
|
|
300
|
-
maxResponseBytes,
|
|
301
|
-
consume: (response, consumeOptions = {}) => parseNativeCompactionSse(response, {
|
|
302
|
-
signal: consumeOptions.requestSignal ?? signal,
|
|
303
|
-
maxResponseBytes,
|
|
304
|
-
}),
|
|
305
|
-
},
|
|
306
|
-
)
|
|
111
|
+
export async function requestNativeCompaction({ baseURL, model, input, instructions, tools, promptCacheKey, idempotencyKey, headers, signal, timeoutMs, maxAttempts, maxResponseBytes }) {
|
|
112
|
+
const body = buildNativeCompactionBody({ model, input, instructions, tools, promptCacheKey })
|
|
113
|
+
const requestHeaders = mergeFeatureHeader({ ...headers, ...(idempotencyKey ? { 'idempotency-key': String(idempotencyKey) } : {}) })
|
|
114
|
+
return fetchSseWithRetry(`${String(baseURL).replace(/\/+$/u, '')}/responses`, body, requestHeaders, signal, timeoutMs, {
|
|
115
|
+
maxAttempts,
|
|
116
|
+
maxResponseBytes,
|
|
117
|
+
consume: (response, consumeOptions) => parseNativeCompactionSse(response, { signal: consumeOptions.requestSignal ?? signal, maxResponseBytes }),
|
|
118
|
+
})
|
|
307
119
|
}
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
import { offloadRequestImagesWithPolicy } from '@deepseek-ai/dsh-llm'
|
|
2
|
+
const DEFAULT_MAX_REQUEST_IMAGE_BYTES = 20 * 1024 * 1024
|
|
3
|
+
const DEFAULT_REQUEST_IMAGE_PIXEL_BUDGET = 2048 * 2048
|
|
4
|
+
const DEFAULT_REQUEST_IMAGE_MAX_BYTES = 1024 * 1024
|
|
5
|
+
export { DEFAULT_MAX_REQUEST_IMAGE_BYTES, DEFAULT_REQUEST_IMAGE_PIXEL_BUDGET, DEFAULT_REQUEST_IMAGE_MAX_BYTES }
|
|
6
|
+
|
|
7
|
+
function isObject(value) { return value !== null && typeof value === 'object' && !Array.isArray(value) }
|
|
8
|
+
|
|
9
|
+
function error(message, code) {
|
|
10
|
+
const value = new Error(message)
|
|
11
|
+
value.code = code
|
|
12
|
+
return value
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function textOf(blocks) {
|
|
16
|
+
return (Array.isArray(blocks) ? blocks : []).filter((b) => b?.type === 'text' && typeof b.text === 'string').map((b) => b.text).join('')
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function hasImage(blocks) {
|
|
20
|
+
return (Array.isArray(blocks) ? blocks : []).some((block) => block?.type === 'image' || (block?.type === 'tool-result' && hasImage(block.content)))
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export async function resolveModelImageSupport(ctx, route, signal) {
|
|
24
|
+
const llm = ctx?.get?.('llm') ?? ctx?.llm
|
|
25
|
+
if (typeof llm?.resolveModelInfo !== 'function') return 'unknown'
|
|
26
|
+
try {
|
|
27
|
+
const info = await llm.resolveModelInfo(route.provider, route.model, signal)
|
|
28
|
+
const modalities = info?.inputModalities
|
|
29
|
+
if (!Array.isArray(modalities)) return 'unknown'
|
|
30
|
+
return modalities.includes('image') ? 'supported' : 'unsupported'
|
|
31
|
+
} catch {
|
|
32
|
+
return 'unknown'
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function attachmentResolver(ctx, options) {
|
|
37
|
+
const attachments = ctx?.get?.('attachments') ?? ctx?.attachments
|
|
38
|
+
return async (block, signal) => {
|
|
39
|
+
if (typeof attachments?.readImageRequest !== 'function') {
|
|
40
|
+
throw error('LCX requires the DSH 0.1.1-rc.2 request-image attachment API', 'LCX_COMPACT_IMAGE_API_UNAVAILABLE')
|
|
41
|
+
}
|
|
42
|
+
const request = await attachments.readImageRequest(block?.attachment, {
|
|
43
|
+
maxPixels: options.requestImagePixelBudget,
|
|
44
|
+
maxBytes: options.requestImageMaxBytes,
|
|
45
|
+
}, signal)
|
|
46
|
+
const data = request?.data
|
|
47
|
+
const mediaType = request?.mediaType ?? block?.attachment?.mediaType
|
|
48
|
+
if (!(data instanceof Uint8Array) && !Buffer.isBuffer(data)) throw error('DSH attachment returned no request-image bytes', 'LCX_COMPACT_IMAGE_UNAVAILABLE')
|
|
49
|
+
if (typeof mediaType !== 'string' || !mediaType.startsWith('image/')) throw error('DSH attachment returned an invalid request-image media type', 'LCX_COMPACT_IMAGE_UNAVAILABLE')
|
|
50
|
+
return { data: Buffer.from(data), mediaType, ref: request?.attachment ?? block.attachment }
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async function imagePart(block, ctx, options, imageMap) {
|
|
55
|
+
if (options.imageSupport === 'unsupported') return { type: 'input_text', text: '[image omitted because the target model does not support image input]' }
|
|
56
|
+
const image = await attachmentResolver(ctx, options)(block, options.signal)
|
|
57
|
+
const encodedBytes = Math.ceil(image.data.byteLength / 3) * 4
|
|
58
|
+
if (encodedBytes > options.maxRequestImageBytes) throw error('one image exceeds the configured LCX request image bound', 'LCX_COMPACT_IMAGE_TOO_LARGE')
|
|
59
|
+
const imageUrl = `data:${image.mediaType};base64,${image.data.toString('base64')}`
|
|
60
|
+
imageMap.set(imageUrl, structuredClone(image.ref))
|
|
61
|
+
return { type: 'input_image', image_url: imageUrl }
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async function toolResultOutput(block, ctx, options, imageMap) {
|
|
65
|
+
if (!hasImage(block?.content)) return textOf(block?.content) || '(no output)'
|
|
66
|
+
const parts = []
|
|
67
|
+
let pending = ''
|
|
68
|
+
const flush = () => { if (pending) parts.push({ type: 'input_text', text: pending }); pending = '' }
|
|
69
|
+
for (const part of block.content ?? []) {
|
|
70
|
+
if (part?.type === 'text') pending += part.text
|
|
71
|
+
else if (part?.type === 'image') { flush(); parts.push(await imagePart(part, ctx, options, imageMap)) }
|
|
72
|
+
else if (part?.type === 'tool-result') {
|
|
73
|
+
const nested = await toolResultOutput(part, ctx, options, imageMap)
|
|
74
|
+
if (typeof nested === 'string') pending += nested === '(no output)' ? '' : nested
|
|
75
|
+
else { flush(); parts.push(...nested) }
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
flush()
|
|
79
|
+
return parts.some((part) => part.type === 'input_image') ? parts : parts.map((part) => part.text ?? '').join('') || '(no output)'
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
async function messageItems(message, ctx, options, imageMap) {
|
|
83
|
+
const blocks = Array.isArray(message?.content) ? message.content : []
|
|
84
|
+
const items = []
|
|
85
|
+
if (message?.role === 'assistant') {
|
|
86
|
+
const text = textOf(blocks)
|
|
87
|
+
if (text) items.push({ type: 'message', role: 'assistant', content: [{ type: 'output_text', text }] })
|
|
88
|
+
for (const call of blocks.filter((block) => block?.type === 'tool-call')) {
|
|
89
|
+
items.push({
|
|
90
|
+
type: 'function_call',
|
|
91
|
+
call_id: String(call.id),
|
|
92
|
+
name: String(call.name),
|
|
93
|
+
arguments: typeof call.arguments === 'string' ? call.arguments : JSON.stringify(call.arguments ?? {}),
|
|
94
|
+
})
|
|
95
|
+
}
|
|
96
|
+
return items
|
|
97
|
+
}
|
|
98
|
+
if (message?.role !== 'user') return items
|
|
99
|
+
|
|
100
|
+
const ordinary = blocks.filter((block) => block?.type === 'text' || block?.type === 'image')
|
|
101
|
+
if (ordinary.length > 0) {
|
|
102
|
+
const content = []
|
|
103
|
+
let pending = ''
|
|
104
|
+
const flush = () => { if (pending) content.push({ type: 'input_text', text: pending }); pending = '' }
|
|
105
|
+
for (const block of ordinary) {
|
|
106
|
+
if (block.type === 'text') pending += block.text
|
|
107
|
+
else { flush(); content.push(await imagePart(block, ctx, options, imageMap)) }
|
|
108
|
+
}
|
|
109
|
+
flush()
|
|
110
|
+
if (content.length > 0) items.push({ type: 'message', role: 'user', content })
|
|
111
|
+
}
|
|
112
|
+
for (const result of blocks.filter((block) => block?.type === 'tool-result')) {
|
|
113
|
+
items.push({
|
|
114
|
+
type: 'function_call_output',
|
|
115
|
+
call_id: String(result.toolCallId),
|
|
116
|
+
output: await toolResultOutput(result, ctx, options, imageMap),
|
|
117
|
+
})
|
|
118
|
+
}
|
|
119
|
+
return items
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export async function serializeDshMessages(messages, ctx, options = {}) {
|
|
123
|
+
const imageMap = new Map()
|
|
124
|
+
const input = []
|
|
125
|
+
const normalized = {
|
|
126
|
+
imageSupport: options.imageSupport ?? 'unknown',
|
|
127
|
+
signal: options.signal,
|
|
128
|
+
maxRequestImageBytes: Number.isSafeInteger(options.maxRequestImageBytes) && options.maxRequestImageBytes > 0
|
|
129
|
+
? options.maxRequestImageBytes
|
|
130
|
+
: DEFAULT_MAX_REQUEST_IMAGE_BYTES,
|
|
131
|
+
requestImagePixelBudget: Number.isSafeInteger(options.requestImagePixelBudget) && options.requestImagePixelBudget > 0
|
|
132
|
+
? options.requestImagePixelBudget
|
|
133
|
+
: DEFAULT_REQUEST_IMAGE_PIXEL_BUDGET,
|
|
134
|
+
requestImageMaxBytes: Number.isSafeInteger(options.requestImageMaxBytes) && options.requestImageMaxBytes > 0
|
|
135
|
+
? options.requestImageMaxBytes
|
|
136
|
+
: DEFAULT_REQUEST_IMAGE_MAX_BYTES,
|
|
137
|
+
}
|
|
138
|
+
const projected = offloadRequestImagesWithPolicy(messages ?? [], {
|
|
139
|
+
representation: 'base64',
|
|
140
|
+
maxBytes: normalized.maxRequestImageBytes,
|
|
141
|
+
byteQuantum: 1,
|
|
142
|
+
byteLength: ref => Math.min(ref.bytes, normalized.requestImageMaxBytes),
|
|
143
|
+
})
|
|
144
|
+
for (const message of projected) {
|
|
145
|
+
if (message?.role === 'system') continue
|
|
146
|
+
input.push(...await messageItems(message, ctx, normalized, imageMap))
|
|
147
|
+
}
|
|
148
|
+
return { input, imageMap }
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export function responsesTools(tools) {
|
|
152
|
+
if (tools === undefined) return undefined
|
|
153
|
+
if (!Array.isArray(tools)) throw error('LCX Responses tools must be an array', 'LCX_COMPACT_INVALID_TOOLS')
|
|
154
|
+
return tools.map((tool) => {
|
|
155
|
+
if (!isObject(tool) || typeof tool.name !== 'string' || !tool.name) throw error('LCX Responses tool has no name', 'LCX_COMPACT_INVALID_TOOLS')
|
|
156
|
+
return {
|
|
157
|
+
type: 'function',
|
|
158
|
+
name: tool.name,
|
|
159
|
+
description: typeof tool.description === 'string' ? tool.description : '',
|
|
160
|
+
parameters: isObject(tool.parameters) ? structuredClone(tool.parameters) : { type: 'object', properties: {} },
|
|
161
|
+
strict: false,
|
|
162
|
+
}
|
|
163
|
+
})
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function mapImageParts(value, mapper) {
|
|
167
|
+
if (!Array.isArray(value)) return value
|
|
168
|
+
return value.map((part) => {
|
|
169
|
+
if (part?.type === 'input_image' || part?.type === 'dsh_image_attachment') return mapper(part)
|
|
170
|
+
if (part?.type === 'function_call_output' && Array.isArray(part.output)) return { ...structuredClone(part), output: mapImageParts(part.output, mapper) }
|
|
171
|
+
return structuredClone(part)
|
|
172
|
+
})
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
export function persistNativeImageReferences(output, imageMap) {
|
|
176
|
+
const map = imageMap instanceof Map ? imageMap : new Map()
|
|
177
|
+
const persist = (part) => {
|
|
178
|
+
if (part.type !== 'input_image' || typeof part.image_url !== 'string') throw error('native compact returned an invalid image item', 'LCX_COMPACT_INVALID_RESPONSE')
|
|
179
|
+
const ref = map.get(part.image_url)
|
|
180
|
+
if (!ref) throw error('native compact returned an untracked image item', 'LCX_COMPACT_INVALID_RESPONSE')
|
|
181
|
+
return { type: 'dsh_image_attachment', attachment: structuredClone(ref) }
|
|
182
|
+
}
|
|
183
|
+
return (output ?? []).map((item) => {
|
|
184
|
+
if (item?.type === 'message' && Array.isArray(item.content)) return { ...structuredClone(item), content: mapImageParts(item.content, persist) }
|
|
185
|
+
if (item?.type === 'function_call_output' && Array.isArray(item.output)) return { ...structuredClone(item), output: mapImageParts(item.output, persist) }
|
|
186
|
+
return structuredClone(item)
|
|
187
|
+
})
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
async function hydratePart(part, ctx, options) {
|
|
191
|
+
if (part?.type !== 'dsh_image_attachment') return structuredClone(part)
|
|
192
|
+
if (options.imageSupport === 'unsupported') return { type: 'input_text', text: '[image omitted because the target model does not support image input]' }
|
|
193
|
+
const block = { type: 'image', attachment: part.attachment }
|
|
194
|
+
const map = options.imageMap instanceof Map ? options.imageMap : new Map()
|
|
195
|
+
return imagePart(block, ctx, options, map)
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
export async function hydrateNativeImageReferences(output, ctx, options = {}) {
|
|
199
|
+
const normalized = {
|
|
200
|
+
imageSupport: options.imageSupport ?? 'unknown',
|
|
201
|
+
signal: options.signal,
|
|
202
|
+
maxRequestImageBytes: options.maxRequestImageBytes ?? DEFAULT_MAX_REQUEST_IMAGE_BYTES,
|
|
203
|
+
requestImagePixelBudget: options.requestImagePixelBudget ?? DEFAULT_REQUEST_IMAGE_PIXEL_BUDGET,
|
|
204
|
+
requestImageMaxBytes: options.requestImageMaxBytes ?? DEFAULT_REQUEST_IMAGE_MAX_BYTES,
|
|
205
|
+
imageMap: options.imageMap,
|
|
206
|
+
}
|
|
207
|
+
const result = []
|
|
208
|
+
for (const item of output ?? []) {
|
|
209
|
+
if (item?.type === 'message' && Array.isArray(item.content)) {
|
|
210
|
+
const content = []
|
|
211
|
+
for (const part of item.content) content.push(await hydratePart(part, ctx, normalized))
|
|
212
|
+
result.push({ ...structuredClone(item), content })
|
|
213
|
+
} else if (item?.type === 'function_call_output' && Array.isArray(item.output)) {
|
|
214
|
+
const value = []
|
|
215
|
+
for (const part of item.output) value.push(await hydratePart(part, ctx, normalized))
|
|
216
|
+
result.push({ ...structuredClone(item), output: value })
|
|
217
|
+
} else result.push(structuredClone(item))
|
|
218
|
+
}
|
|
219
|
+
return result
|
|
220
|
+
}
|