dsh-lcx-codex 0.4.1 → 0.4.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/ARCHITECTURE.md +24 -0
- package/CHANGELOG.md +15 -0
- package/README.md +177 -125
- package/README_EN.md +180 -116
- package/cordis.patch.yml +1 -0
- package/lib/client.js +15 -18
- package/lib/compact-v2.js +21 -37
- package/lib/dsh-responses.js +13 -6
- package/lib/index.js +69 -43
- package/lib/native-checkpoint.js +18 -7
- package/lib/responses-replay.js +63 -276
- package/lib/responses-request.js +145 -0
- package/lib/responses-stream.js +539 -0
- package/lib/route.js +58 -43
- package/lib/transport.js +10 -4
- package/lib/web-search-alpha.js +5 -2
- package/package.json +4 -4
package/lib/responses-replay.js
CHANGED
|
@@ -1,281 +1,68 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
1
|
+
import { mergeFeatureHeader } from './compact-v2.js'
|
|
2
|
+
import { resolvePiResponsesModel } from './dsh-responses.js'
|
|
3
|
+
import { buildResponsesBody } from './responses-request.js'
|
|
4
|
+
import { streamResponsesRequest } from './responses-stream.js'
|
|
4
5
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
const
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
const
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
let dataLines = []
|
|
32
|
-
let bytes = 0
|
|
33
|
-
const maxBytes = options.maxResponseBytes ?? 8 * 1024 * 1024
|
|
34
|
-
const decodeEvent = () => {
|
|
35
|
-
if (dataLines.length === 0) return undefined
|
|
36
|
-
const data = dataLines.join('\n')
|
|
37
|
-
dataLines = []
|
|
38
|
-
if (data === '[DONE]') return undefined
|
|
39
|
-
try { return JSON.parse(data) } catch (cause) { throw fail(`malformed Responses SSE JSON: ${cause}`, 'LCX_INVALID_SSE') }
|
|
40
|
-
}
|
|
41
|
-
try {
|
|
42
|
-
while (true) {
|
|
43
|
-
if (options.signal?.aborted) throw options.signal.reason ?? fail('request aborted', 'LCX_ABORTED')
|
|
44
|
-
const { done, value } = await reader.read()
|
|
45
|
-
if (done) break
|
|
46
|
-
bytes += value.byteLength
|
|
47
|
-
if (bytes > maxBytes) throw fail(`Responses SSE exceeds ${maxBytes} bytes`, 'LCX_RESPONSE_TOO_LARGE')
|
|
48
|
-
pending += decoder.decode(value, { stream: true })
|
|
49
|
-
let newline
|
|
50
|
-
while ((newline = pending.indexOf('\n')) >= 0) {
|
|
51
|
-
const line = pending.slice(0, newline).replace(/\r$/u, '')
|
|
52
|
-
pending = pending.slice(newline + 1)
|
|
53
|
-
if (line === '') {
|
|
54
|
-
const event = decodeEvent()
|
|
55
|
-
if (event) yield event
|
|
56
|
-
} else if (!line.startsWith(':') && line.startsWith('data:')) dataLines.push(line.slice(5).replace(/^ /u, ''))
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
pending += decoder.decode()
|
|
60
|
-
if (pending.startsWith('data:')) dataLines.push(pending.slice(5).replace(/^ /u, ''))
|
|
61
|
-
const event = decodeEvent()
|
|
62
|
-
if (event) yield event
|
|
63
|
-
} finally {
|
|
64
|
-
await reader.cancel().catch(() => undefined)
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
function streamError(event) {
|
|
69
|
-
const e = fail(`Responses stream ended with ${String(event?.type)}`)
|
|
70
|
-
if (Number.isInteger(event?.error?.status)) e.status = event.error.status
|
|
71
|
-
return e
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
function textSignature(itemId, phase) {
|
|
75
|
-
if (typeof itemId !== 'string' || itemId.length === 0) return undefined
|
|
76
|
-
const value = { v: 1, id: itemId }
|
|
77
|
-
if (phase === 'commentary' || phase === 'final_answer') value.phase = phase
|
|
78
|
-
return JSON.stringify(value)
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
function replayBlockFor(item, fallbackKind) {
|
|
82
|
-
const kind = item?.kind ?? fallbackKind
|
|
83
|
-
if (kind === 'text') {
|
|
84
|
-
const signature = textSignature(item?.itemId, item?.phase)
|
|
85
|
-
return { type: 'text', ...(signature ? { textSignature: signature } : {}) }
|
|
86
|
-
}
|
|
87
|
-
if (kind === 'reasoning') {
|
|
88
|
-
const signature = isObject(item?.rawItem) ? JSON.stringify(item.rawItem) : undefined
|
|
89
|
-
return { type: 'reasoning', ...(signature ? { thinkingSignature: signature } : {}) }
|
|
90
|
-
}
|
|
91
|
-
return { type: 'tool-call' }
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
function replayStopReason(reason) {
|
|
95
|
-
if (reason?.kind === 'tool-calls') return 'toolUse'
|
|
96
|
-
if (reason?.kind === 'max-tokens') return 'length'
|
|
97
|
-
if (reason?.kind === 'error') return 'error'
|
|
98
|
-
if (reason?.kind === 'aborted') return 'aborted'
|
|
99
|
-
return 'stop'
|
|
100
|
-
}
|
|
101
|
-
function dshToolCallId(callId, itemId) {
|
|
102
|
-
const call = typeof callId === 'string' && callId.length > 0 ? callId : undefined
|
|
103
|
-
const item = typeof itemId === 'string' && itemId.length > 0 ? itemId : undefined
|
|
104
|
-
if (call && item) return `${call}|${item}`
|
|
105
|
-
return call ?? item ?? ''
|
|
6
|
+
/**
|
|
7
|
+
* Backward-compatible body helper retained for protocol tests and downstream imports.
|
|
8
|
+
* Production managed replay already carries the DSH system prompt in canonical input;
|
|
9
|
+
* `system` remains only as a low-level compatibility field.
|
|
10
|
+
*/
|
|
11
|
+
export function replayBody({ model, modelDescriptor, input, system, tools, promptCacheKey, promptCacheRetention, cacheRetention, reasoningEffort, temperature, maxTokens }) {
|
|
12
|
+
const descriptor = modelDescriptor ?? resolvePiResponsesModel({
|
|
13
|
+
route: { provider: 'lcx', model, baseURL: '' },
|
|
14
|
+
model: { id: model, provider: 'lcx', baseUrl: '', api: 'openai-responses', reasoning: true, input: ['text'] },
|
|
15
|
+
})
|
|
16
|
+
const body = buildResponsesBody({
|
|
17
|
+
model: descriptor,
|
|
18
|
+
input,
|
|
19
|
+
instructions: system,
|
|
20
|
+
tools,
|
|
21
|
+
promptCacheKey,
|
|
22
|
+
promptCacheRetention,
|
|
23
|
+
cacheRetention: cacheRetention ?? (promptCacheKey ? (promptCacheRetention ? 'long' : 'short') : 'none'),
|
|
24
|
+
reasoningEffort,
|
|
25
|
+
temperature,
|
|
26
|
+
maxTokens,
|
|
27
|
+
})
|
|
28
|
+
// Opaque Native replay stays on the Remote V2 wire contract while sharing the standard builder.
|
|
29
|
+
body.tool_choice = 'auto'
|
|
30
|
+
body.parallel_tool_calls = true
|
|
31
|
+
return body
|
|
106
32
|
}
|
|
107
33
|
|
|
108
|
-
|
|
109
|
-
|
|
34
|
+
/**
|
|
35
|
+
* Compatibility wrapper: Native replay now uses the same builder/transport/parser as ordinary turns.
|
|
36
|
+
*/
|
|
37
|
+
export async function* requestNativeReplay({ baseURL, provider, model, modelDescriptor, input, system, tools, promptCacheKey, promptCacheRetention, cacheRetention, reasoningEffort, temperature, maxTokens, grammarToolInputProperties, headers, signal, timeoutMs, maxAttempts = 1, maxResponseBytes }) {
|
|
38
|
+
const descriptor = modelDescriptor ?? resolvePiResponsesModel({
|
|
39
|
+
route: { provider, model, baseURL },
|
|
40
|
+
model: { id: model, provider, baseUrl: baseURL, api: 'openai-responses', reasoning: true, input: ['text'] },
|
|
41
|
+
})
|
|
42
|
+
const body = replayBody({
|
|
110
43
|
model,
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
records.push(record)
|
|
136
|
-
if (streamIdentity !== undefined) byStreamIdentity.set(streamIdentity, record)
|
|
137
|
-
return { record, created: true }
|
|
138
|
-
}
|
|
139
|
-
for await (const event of sseEvents(response, { signal, maxResponseBytes })) {
|
|
140
|
-
if (!isObject(event)) continue
|
|
141
|
-
if (event.type === 'error' || event.type === 'response.failed' || event.type === 'response.incomplete') throw streamError(event)
|
|
142
|
-
if (event.type === 'response.output_text.delta') {
|
|
143
|
-
const itemId = typeof event.item_id === 'string' ? event.item_id : undefined
|
|
144
|
-
const key = identity('text', itemId, event.content_index)
|
|
145
|
-
const allocated = createRecord('text', key ?? `text:index:${event.output_index ?? ''}:${event.content_index ?? ''}`, event.output_index, event.content_index)
|
|
146
|
-
const record = allocated.record
|
|
147
|
-
if (allocated.created) yield { type: 'block-start', index: record.index, blockType: 'text' }
|
|
148
|
-
if (typeof event.delta === 'string' && event.delta) { record.text += event.delta; yield { type: 'text-delta', index: record.index, text: event.delta } }
|
|
149
|
-
continue
|
|
150
|
-
}
|
|
151
|
-
if (event.type === 'response.reasoning_summary_text.delta' || event.type === 'response.reasoning_text.delta') {
|
|
152
|
-
const itemId = typeof event.item_id === 'string' ? event.item_id : undefined
|
|
153
|
-
const key = identity('reasoning', itemId, event.summary_index ?? event.content_index)
|
|
154
|
-
const allocated = createRecord('reasoning', key ?? `reasoning:index:${event.output_index ?? ''}:${event.summary_index ?? event.content_index ?? ''}`, event.output_index, event.summary_index ?? event.content_index)
|
|
155
|
-
const record = allocated.record
|
|
156
|
-
if (allocated.created) yield { type: 'block-start', index: record.index, blockType: 'reasoning' }
|
|
157
|
-
if (typeof event.delta === 'string' && event.delta) { record.text += event.delta; yield { type: 'reasoning-delta', index: record.index, text: event.delta } }
|
|
158
|
-
continue
|
|
159
|
-
}
|
|
160
|
-
if (event.type === 'response.function_call_arguments.delta') {
|
|
161
|
-
const itemId = typeof event.item_id === 'string' ? event.item_id : undefined
|
|
162
|
-
const callId = typeof event.call_id === 'string' ? event.call_id : itemId ?? `call-${event.output_index ?? nextIndex}`
|
|
163
|
-
const key = identity('tool-call', typeof event.item_id === 'string' ? event.item_id : callId, 0) ?? `tool-call:${callId}`
|
|
164
|
-
const allocated = createRecord('tool-call', key, event.output_index, 0)
|
|
165
|
-
const record = allocated.record
|
|
166
|
-
record.callId = callId
|
|
167
|
-
record.itemId = itemId
|
|
168
|
-
if (typeof event.name === 'string') record.name = event.name
|
|
169
|
-
if (allocated.created) yield { type: 'block-start', index: record.index, blockType: 'tool-call' }
|
|
170
|
-
const delta = typeof event.delta === 'string' ? event.delta : ''
|
|
171
|
-
record.arguments += delta
|
|
172
|
-
yield { type: 'tool-call-delta', index: record.index, id: dshToolCallId(callId, itemId), ...(record.name ? { name: record.name } : {}), argumentsDelta: delta }
|
|
173
|
-
continue
|
|
174
|
-
}
|
|
175
|
-
if (event.type === 'response.output_item.added' && event.item?.type === 'function_call') {
|
|
176
|
-
const itemId = typeof event.item.id === 'string' ? event.item.id : undefined
|
|
177
|
-
const callId = typeof event.item.call_id === 'string' ? event.item.call_id : itemId ?? `call-${event.output_index ?? nextIndex}`
|
|
178
|
-
const key = identity('tool-call', itemId ?? callId, 0) ?? `tool-call:${callId}`
|
|
179
|
-
const allocated = createRecord('tool-call', key, event.output_index, 0)
|
|
180
|
-
const record = allocated.record
|
|
181
|
-
record.callId = callId
|
|
182
|
-
record.itemId = itemId
|
|
183
|
-
record.name = typeof event.item.name === 'string' ? event.item.name : record.name
|
|
184
|
-
if (allocated.created) {
|
|
185
|
-
yield { type: 'block-start', index: record.index, blockType: 'tool-call' }
|
|
186
|
-
if (typeof event.item.arguments === 'string' && event.item.arguments) { record.arguments = event.item.arguments; yield { type: 'tool-call-delta', index: record.index, id: dshToolCallId(callId, itemId), ...(record.name ? { name: record.name } : {}), argumentsDelta: event.item.arguments } }
|
|
187
|
-
}
|
|
188
|
-
continue
|
|
189
|
-
}
|
|
190
|
-
if (event.type === 'response.output_item.done' && event.item?.type === 'function_call') {
|
|
191
|
-
const itemId = typeof event.item.id === 'string' ? event.item.id : undefined
|
|
192
|
-
const callId = typeof event.item.call_id === 'string' ? event.item.call_id : itemId ?? `call-${event.output_index ?? nextIndex}`
|
|
193
|
-
const key = identity('tool-call', itemId ?? callId, 0) ?? `tool-call:${callId}`
|
|
194
|
-
const allocated = createRecord('tool-call', key, event.output_index, 0)
|
|
195
|
-
const record = allocated.record
|
|
196
|
-
record.callId = callId
|
|
197
|
-
record.itemId = itemId
|
|
198
|
-
record.name = typeof event.item.name === 'string' ? event.item.name : record.name
|
|
199
|
-
const completedArguments = typeof event.item.arguments === 'string' ? event.item.arguments : record.arguments
|
|
200
|
-
if (allocated.created) {
|
|
201
|
-
yield { type: 'block-start', index: record.index, blockType: 'tool-call' }
|
|
202
|
-
if (completedArguments) yield { type: 'tool-call-delta', index: record.index, id: dshToolCallId(callId, itemId), ...(record.name ? { name: record.name } : {}), argumentsDelta: completedArguments }
|
|
203
|
-
}
|
|
204
|
-
record.arguments = completedArguments
|
|
205
|
-
continue
|
|
206
|
-
}
|
|
207
|
-
if (event.type === 'response.completed') terminal = event.response
|
|
208
|
-
}
|
|
209
|
-
if (!isObject(terminal)) throw fail('Responses replay ended without response.completed', 'LCX_RESPONSES_INCOMPLETE')
|
|
210
|
-
if (terminal.status !== 'completed') throw fail('Responses replay response.completed did not carry completed status', 'LCX_RESPONSES_INCOMPLETE')
|
|
211
|
-
|
|
212
|
-
const terminalItems = []
|
|
213
|
-
for (const [outputIndex, item] of (terminal.output ?? []).entries()) {
|
|
214
|
-
if (item?.type === 'message') {
|
|
215
|
-
for (const [contentIndex, part] of (item.content ?? []).entries()) if (part?.type === 'output_text') terminalItems.push({ kind: 'text', outputIndex, partIndex: contentIndex, itemId: item.id, phase: item.phase, text: String(part.text ?? ''), rawItem: item })
|
|
216
|
-
} else if (item?.type === 'reasoning') {
|
|
217
|
-
const parts = Array.isArray(item.summary) && item.summary.length ? item.summary : item.content ?? []
|
|
218
|
-
parts.forEach((part, index) => { if (typeof part?.text === 'string') terminalItems.push({ kind: 'reasoning', outputIndex, partIndex: index, itemId: item.id, text: part.text, rawItem: item }) })
|
|
219
|
-
} else if (item?.type === 'function_call') {
|
|
220
|
-
terminalItems.push({ kind: 'tool-call', outputIndex, partIndex: 0, itemId: item.id, callId: item.call_id, name: String(item.name ?? ''), arguments: String(item.arguments ?? ''), rawItem: item })
|
|
221
|
-
}
|
|
222
|
-
}
|
|
223
|
-
const matched = new Set()
|
|
224
|
-
const matchesRecord = (item) => {
|
|
225
|
-
const stable = identity(item.kind, item.itemId ?? item.callId, item.partIndex)
|
|
226
|
-
if (stable) {
|
|
227
|
-
const exact = records.find((record) => !matched.has(record) && record.kind === item.kind && (record.streamIdentity === stable || (item.kind === 'tool-call' && record.callId === item.callId)))
|
|
228
|
-
if (exact) return exact
|
|
229
|
-
}
|
|
230
|
-
const candidates = records.filter((record) => !matched.has(record) && record.kind === item.kind)
|
|
231
|
-
if (candidates.length === 1) return candidates[0]
|
|
232
|
-
const contentMatches = candidates.filter((record) => item.kind === 'tool-call'
|
|
233
|
-
? record.callId === item.callId || (item.arguments && record.arguments && (item.arguments.startsWith(record.arguments) || record.arguments.startsWith(item.arguments)))
|
|
234
|
-
: item.text !== undefined && record.text && (item.text === record.text || item.text.startsWith(record.text) || record.text.startsWith(item.text)))
|
|
235
|
-
return contentMatches.length === 1 ? contentMatches[0] : undefined
|
|
236
|
-
}
|
|
237
|
-
for (const item of terminalItems) {
|
|
238
|
-
const record = matchesRecord(item)
|
|
239
|
-
if (!record) continue
|
|
240
|
-
matched.add(record)
|
|
241
|
-
record.terminal = item
|
|
242
|
-
}
|
|
243
|
-
const replayBlocks = []
|
|
244
|
-
for (const record of records) {
|
|
245
|
-
const item = record.terminal
|
|
246
|
-
replayBlocks[record.index] = replayBlockFor(item, record.kind)
|
|
247
|
-
if (record.kind === 'text') yield { type: 'block-end', index: record.index, block: { type: 'text', text: item?.text ?? record.text } }
|
|
248
|
-
else if (record.kind === 'reasoning') yield { type: 'block-end', index: record.index, block: { type: 'reasoning', text: item?.text ?? record.text } }
|
|
249
|
-
else yield { type: 'block-end', index: record.index, block: { type: 'tool-call', id: dshToolCallId(item?.callId ?? record.callId, item?.itemId ?? record.itemId), name: item?.name ?? record.name, arguments: item?.arguments ?? record.arguments } }
|
|
250
|
-
}
|
|
251
|
-
for (const item of terminalItems) {
|
|
252
|
-
const record = records.find((candidate) => candidate.terminal === item)
|
|
253
|
-
if (record) continue
|
|
254
|
-
if (item.kind === 'text' && !item.text) continue
|
|
255
|
-
const index = nextIndex++
|
|
256
|
-
replayBlocks[index] = replayBlockFor(item, item.kind)
|
|
257
|
-
yield { type: 'block-start', index, blockType: item.kind === 'tool-call' ? 'tool-call' : item.kind }
|
|
258
|
-
if (item.kind === 'text' && item.text) yield { type: 'text-delta', index, text: item.text }
|
|
259
|
-
if (item.kind === 'reasoning' && item.text) yield { type: 'reasoning-delta', index, text: item.text }
|
|
260
|
-
if (item.kind === 'tool-call') yield { type: 'tool-call-delta', index, id: dshToolCallId(item.callId, item.itemId), name: item.name, argumentsDelta: item.arguments }
|
|
261
|
-
yield { type: 'block-end', index, block: item.kind === 'text' ? { type: 'text', text: item.text } : item.kind === 'reasoning' ? { type: 'reasoning', text: item.text } : { type: 'tool-call', id: dshToolCallId(item.callId, item.itemId), name: item.name, arguments: item.arguments } }
|
|
262
|
-
}
|
|
263
|
-
const usage = usageFrom(terminal.usage)
|
|
264
|
-
if (usage) yield { type: 'usage', usage }
|
|
265
|
-
const reason = terminal.status === 'incomplete'
|
|
266
|
-
? { kind: 'max-tokens' }
|
|
267
|
-
: terminal.error ? { kind: 'error', failure: { message: terminal.error.message ?? 'Responses error', code: 'LCX_RESPONSES_UPSTREAM_ERROR' } }
|
|
268
|
-
: terminalItems.some((item) => item.kind === 'tool-call') ? { kind: 'tool-calls' } : { kind: 'stop' }
|
|
269
|
-
const replayState = typeof provider === 'string' && provider.length > 0 && typeof model === 'string' && model.length > 0
|
|
270
|
-
? {
|
|
271
|
-
response: {
|
|
272
|
-
kind: 'pi-ai', version: 2, api: 'openai-responses', provider, model,
|
|
273
|
-
...(typeof terminal.model === 'string' && terminal.model.length > 0 ? { responseModel: terminal.model } : {}),
|
|
274
|
-
...(typeof terminal.id === 'string' && terminal.id.length > 0 ? { responseId: terminal.id } : {}),
|
|
275
|
-
stopReason: replayStopReason(reason),
|
|
276
|
-
},
|
|
277
|
-
blocks: Array.from({ length: nextIndex }, (_, index) => replayBlocks[index] ?? { type: records.find((record) => record.index === index)?.kind ?? 'text' }),
|
|
278
|
-
}
|
|
279
|
-
: undefined
|
|
280
|
-
yield { type: 'finish', reason, ...(replayState ? { replayState } : {}) }
|
|
44
|
+
modelDescriptor: descriptor,
|
|
45
|
+
input,
|
|
46
|
+
system,
|
|
47
|
+
tools,
|
|
48
|
+
promptCacheKey,
|
|
49
|
+
promptCacheRetention,
|
|
50
|
+
cacheRetention,
|
|
51
|
+
reasoningEffort,
|
|
52
|
+
temperature,
|
|
53
|
+
maxTokens,
|
|
54
|
+
})
|
|
55
|
+
yield* streamResponsesRequest({
|
|
56
|
+
baseURL,
|
|
57
|
+
provider,
|
|
58
|
+
model,
|
|
59
|
+
piModel: descriptor,
|
|
60
|
+
body,
|
|
61
|
+
grammarToolInputProperties,
|
|
62
|
+
headers: mergeFeatureHeader(headers),
|
|
63
|
+
signal,
|
|
64
|
+
timeoutMs,
|
|
65
|
+
maxAttempts,
|
|
66
|
+
maxResponseBytes,
|
|
67
|
+
})
|
|
281
68
|
}
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
|
|
3
|
+
import { clampOpenAIPromptCacheKey } from '@earendil-works/pi-ai/api/openai-prompt-cache'
|
|
4
|
+
import { responsesTools } from './dsh-responses.js'
|
|
5
|
+
|
|
6
|
+
const OPENAI_RESPONSES_MIN_OUTPUT_TOKENS = 16
|
|
7
|
+
|
|
8
|
+
/** @typedef {'none' | 'short' | 'long'} CacheRetention */
|
|
9
|
+
/** @typedef {Record<string, unknown>} UnknownRecord */
|
|
10
|
+
/**
|
|
11
|
+
* @typedef {object} ResponsesCompat
|
|
12
|
+
* @property {boolean} [supportsLongCacheRetention]
|
|
13
|
+
* @property {boolean} [supportsExplicitPromptCacheMode]
|
|
14
|
+
*/
|
|
15
|
+
/**
|
|
16
|
+
* @typedef {object} PiResponsesModel
|
|
17
|
+
* @property {string} id
|
|
18
|
+
* @property {string} provider
|
|
19
|
+
* @property {boolean} [reasoning]
|
|
20
|
+
* @property {Record<string, string | null>} [thinkingLevelMap]
|
|
21
|
+
* @property {ResponsesCompat} [compat]
|
|
22
|
+
*/
|
|
23
|
+
/**
|
|
24
|
+
* @typedef {object} GenerationControls
|
|
25
|
+
* @property {unknown} [reasoningEffort]
|
|
26
|
+
* @property {unknown} [temperature]
|
|
27
|
+
* @property {unknown} [maxTokens]
|
|
28
|
+
*/
|
|
29
|
+
/**
|
|
30
|
+
* @typedef {object} BuildResponsesBodyOptions
|
|
31
|
+
* @property {PiResponsesModel | string} model
|
|
32
|
+
* @property {unknown[]} input
|
|
33
|
+
* @property {string} [instructions]
|
|
34
|
+
* @property {unknown} [tools]
|
|
35
|
+
* @property {string} [sessionId]
|
|
36
|
+
* @property {string} [promptCacheKey]
|
|
37
|
+
* @property {string} [promptCacheRetention]
|
|
38
|
+
* @property {CacheRetention} [cacheRetention]
|
|
39
|
+
* @property {unknown} [reasoningEffort]
|
|
40
|
+
* @property {unknown} [temperature]
|
|
41
|
+
* @property {unknown} [maxTokens]
|
|
42
|
+
* @property {UnknownRecord} [samplingParams]
|
|
43
|
+
*/
|
|
44
|
+
|
|
45
|
+
/** @param {unknown} value */
|
|
46
|
+
function isObject(value) { return value !== null && typeof value === 'object' && !Array.isArray(value) }
|
|
47
|
+
|
|
48
|
+
/** @param {PiResponsesModel | string} model */
|
|
49
|
+
function modelId(model) { return typeof model === 'string' ? model : model.id }
|
|
50
|
+
|
|
51
|
+
/** @param {PiResponsesModel | string} model */
|
|
52
|
+
function modelRecord(model) {
|
|
53
|
+
return typeof model === 'string'
|
|
54
|
+
? /** @type {PiResponsesModel} */ ({ id: model, provider: 'lcx', reasoning: true })
|
|
55
|
+
: model
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** @param {unknown} value */
|
|
59
|
+
function normalizeCacheRetention(value) {
|
|
60
|
+
return /** @type {CacheRetention} */ (['none', 'short', 'long'].includes(String(value)) ? value : 'short')
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Pi-parity generation controls for OpenAI Responses.
|
|
65
|
+
* @param {GenerationControls & { model?: PiResponsesModel | string, includeDefaultReasoning?: boolean }} [controls]
|
|
66
|
+
*/
|
|
67
|
+
export function responsesGenerationEnvelope({ model = 'unknown', reasoningEffort, temperature, maxTokens, includeDefaultReasoning = false } = {}) {
|
|
68
|
+
const descriptor = modelRecord(model)
|
|
69
|
+
/** @type {UnknownRecord} */
|
|
70
|
+
const result = {}
|
|
71
|
+
if (descriptor.reasoning !== false) {
|
|
72
|
+
if (reasoningEffort !== undefined && reasoningEffort !== 'off') {
|
|
73
|
+
const requested = String(reasoningEffort)
|
|
74
|
+
const wire = descriptor.thinkingLevelMap?.[requested] ?? requested
|
|
75
|
+
if (wire !== null) {
|
|
76
|
+
result.reasoning = { effort: wire, summary: 'auto' }
|
|
77
|
+
result.include = ['reasoning.encrypted_content']
|
|
78
|
+
}
|
|
79
|
+
} else if (includeDefaultReasoning && descriptor.provider !== 'github-copilot' && descriptor.thinkingLevelMap?.off !== null) {
|
|
80
|
+
result.reasoning = { effort: descriptor.thinkingLevelMap?.off ?? 'none' }
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
if (temperature !== undefined) {
|
|
84
|
+
if (!Number.isFinite(temperature)) throw Object.assign(new Error('Responses temperature must be finite'), { code: 'LCX_RESPONSES_INVALID_INPUT' })
|
|
85
|
+
result.temperature = Number(temperature)
|
|
86
|
+
}
|
|
87
|
+
if (maxTokens !== undefined) {
|
|
88
|
+
if (!Number.isSafeInteger(maxTokens) || Number(maxTokens) <= 0) throw Object.assign(new Error('Responses maxTokens must be a positive safe integer'), { code: 'LCX_RESPONSES_INVALID_INPUT' })
|
|
89
|
+
result.max_output_tokens = Math.max(OPENAI_RESPONSES_MIN_OUTPUT_TOKENS, Number(maxTokens))
|
|
90
|
+
}
|
|
91
|
+
return result
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Build the shared LCX-owned request envelope while retaining Pi 0.84 Responses semantics.
|
|
96
|
+
* Production LCX ordinary/compact/replay construction places the DSH system prompt in canonical input.
|
|
97
|
+
* `instructions` remains accepted only for the exported low-level helper's backward-compatible callers.
|
|
98
|
+
* @param {BuildResponsesBodyOptions} options
|
|
99
|
+
*/
|
|
100
|
+
export function buildResponsesBody({ model, input, instructions, tools, sessionId, promptCacheKey, promptCacheRetention, cacheRetention, reasoningEffort, temperature, maxTokens, samplingParams }) {
|
|
101
|
+
if (!Array.isArray(input)) throw Object.assign(new Error('Responses input must be an array'), { code: 'LCX_RESPONSES_INVALID_INPUT' })
|
|
102
|
+
const descriptor = modelRecord(model)
|
|
103
|
+
const retention = normalizeCacheRetention(cacheRetention)
|
|
104
|
+
const compat = descriptor.compat ?? {}
|
|
105
|
+
const cacheKey = retention === 'none'
|
|
106
|
+
? undefined
|
|
107
|
+
: promptCacheKey ?? clampOpenAIPromptCacheKey(sessionId)
|
|
108
|
+
const longRetention = promptCacheRetention ?? (retention === 'long' && compat.supportsLongCacheRetention !== false ? '24h' : undefined)
|
|
109
|
+
// Pi's flag is route/model proof that prompt_cache_options is accepted; no mode keeps implicit caching.
|
|
110
|
+
const currentCache = retention === 'short' && compat.supportsExplicitPromptCacheMode === true
|
|
111
|
+
const explicitCache = retention === 'none' && compat.supportsExplicitPromptCacheMode === true
|
|
112
|
+
const promptCacheOptions = currentCache ? { ttl: '30m' } : explicitCache ? { mode: 'explicit' } : undefined
|
|
113
|
+
const nativeTools = responsesTools(tools)
|
|
114
|
+
/** @type {UnknownRecord} */
|
|
115
|
+
const body = {
|
|
116
|
+
model: modelId(model),
|
|
117
|
+
input: structuredClone(input),
|
|
118
|
+
stream: true,
|
|
119
|
+
store: false,
|
|
120
|
+
...(instructions === undefined ? {} : { instructions }),
|
|
121
|
+
...(nativeTools !== undefined && nativeTools.length > 0 ? { tools: nativeTools } : {}),
|
|
122
|
+
...(cacheKey ? { prompt_cache_key: cacheKey } : {}),
|
|
123
|
+
...(longRetention ? { prompt_cache_retention: longRetention } : {}),
|
|
124
|
+
...(promptCacheOptions ? { prompt_cache_options: promptCacheOptions } : {}),
|
|
125
|
+
...responsesGenerationEnvelope({ model: descriptor, reasoningEffort, temperature, maxTokens, includeDefaultReasoning: true }),
|
|
126
|
+
}
|
|
127
|
+
if (isObject(samplingParams)) Object.assign(body, structuredClone(samplingParams))
|
|
128
|
+
return body
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Compact is the standard request plus the one opaque-history transition patch.
|
|
133
|
+
* @param {BuildResponsesBodyOptions} options
|
|
134
|
+
*/
|
|
135
|
+
export function buildCompactionResponsesBody(options) {
|
|
136
|
+
const body = /** @type {UnknownRecord & { input: unknown[] }} */ (buildResponsesBody(options))
|
|
137
|
+
if (body.input.some((item) => /** @type {UnknownRecord | undefined} */ (item)?.type === 'compaction_trigger')) {
|
|
138
|
+
throw Object.assign(new Error('native compaction input already contains compaction_trigger'), { code: 'LCX_COMPACT_DUPLICATE_TRIGGER' })
|
|
139
|
+
}
|
|
140
|
+
body.input = [...body.input, { type: 'compaction_trigger' }]
|
|
141
|
+
// Remote Compaction V2 has historically required these explicit controls.
|
|
142
|
+
body.tool_choice = 'auto'
|
|
143
|
+
body.parallel_tool_calls = true
|
|
144
|
+
return body
|
|
145
|
+
}
|