dsh-lcx-codex 0.3.3 → 0.4.0-rc.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/ARCHITECTURE.md +93 -0
- package/CHANGELOG.md +143 -41
- package/README.md +208 -100
- package/README_EN.md +133 -101
- package/assets/dsh-lcx-codex-banner.jpg +0 -0
- package/cordis.patch.yml +10 -4
- package/lib/client.js +117 -194
- package/lib/compact-v2.js +124 -273
- package/lib/dsh-responses.js +299 -0
- package/lib/index.js +548 -1593
- package/lib/json-store.js +34 -0
- package/lib/legacy-v3.js +20 -0
- package/lib/native-checkpoint.js +93 -0
- package/lib/responses-replay.js +281 -0
- package/lib/route.js +222 -0
- package/lib/service-mutex.js +72 -0
- package/lib/transport.js +59 -380
- package/lib/web-run-output.js +13 -152
- package/lib/web-search-alpha.js +26 -448
- package/lib/web-search-capability.js +8 -10
- package/lib/web-search-hosted.js +22 -372
- package/lib/web-search-ref-store.js +7 -7
- package/package.json +20 -14
- package/scripts/probe-alpha.mjs +28 -76
- package/scripts/validate-dsh-schema.mjs +31 -0
- package/lib/checkpoint-store-v3.js +0 -459
- package/lib/compact.js +0 -752
- package/lib/dsh-pi-responses.js +0 -339
- package/lib/private-file.js +0 -18
- package/lib/session-lease.js +0 -70
- package/lib/web-search-store.js +0 -181
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { mkdirSync, readFileSync, renameSync, writeFileSync, chmodSync } from 'node:fs'
|
|
2
|
+
import { dirname } from 'node:path'
|
|
3
|
+
|
|
4
|
+
export class JsonStore {
|
|
5
|
+
constructor(file, empty, validate, corruptCode = 'LCX_STORE_CORRUPT') {
|
|
6
|
+
this.file = file
|
|
7
|
+
this.empty = empty
|
|
8
|
+
this.validate = validate
|
|
9
|
+
this.corruptCode = corruptCode
|
|
10
|
+
this.data = empty()
|
|
11
|
+
this.refresh()
|
|
12
|
+
}
|
|
13
|
+
refresh() {
|
|
14
|
+
try {
|
|
15
|
+
const parsed = JSON.parse(readFileSync(this.file, 'utf8'))
|
|
16
|
+
if (!this.validate(parsed)) throw new Error('invalid store')
|
|
17
|
+
this.data = parsed
|
|
18
|
+
} catch (error) {
|
|
19
|
+
if (error?.code === 'ENOENT') { this.data = this.empty(); return }
|
|
20
|
+
const wrapped = new Error(`Invalid LCX store ${this.file}`, { cause: error }); wrapped.code = this.corruptCode; throw wrapped
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
update(mutator) {
|
|
24
|
+
this.refresh()
|
|
25
|
+
const next = mutator(structuredClone(this.data))
|
|
26
|
+
if (!this.validate(next)) throw new Error(`Refusing to write invalid LCX store ${this.file}`)
|
|
27
|
+
mkdirSync(dirname(this.file), { recursive: true })
|
|
28
|
+
const tmp = `${this.file}.${process.pid}.tmp`
|
|
29
|
+
writeFileSync(tmp, `${JSON.stringify(next, null, 2)}\n`, { mode: 0o600 })
|
|
30
|
+
try { chmodSync(tmp, 0o600) } catch {}
|
|
31
|
+
renameSync(tmp, this.file)
|
|
32
|
+
this.data = next
|
|
33
|
+
}
|
|
34
|
+
}
|
package/lib/legacy-v3.js
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs'
|
|
2
|
+
import { baseURLFingerprint, sessionAncestry } from './route.js'
|
|
3
|
+
|
|
4
|
+
const PATTERN = /\[dsh-lcx-codex-v3-checkpoint:([0-9a-f-]{36})\]/iu
|
|
5
|
+
function textOf(message) { return (message?.content ?? []).filter((b) => b?.type === 'text').map((b) => b.text).join('') }
|
|
6
|
+
export function legacyV3Id(message) { return textOf(message).match(PATTERN)?.[1]?.toLowerCase() }
|
|
7
|
+
export function loadLegacyRecord(file, id) {
|
|
8
|
+
if (!file || !id) return undefined
|
|
9
|
+
try {
|
|
10
|
+
const parsed = JSON.parse(readFileSync(file, 'utf8'))
|
|
11
|
+
const record = parsed?.version === 3 ? parsed.checkpoints?.[id] : undefined
|
|
12
|
+
if (!record || record.version !== 3 || !Array.isArray(record.nativeOutput)) return undefined
|
|
13
|
+
return structuredClone(record)
|
|
14
|
+
} catch { return undefined }
|
|
15
|
+
}
|
|
16
|
+
export function legacyRouteCompatible(record, route, ctx) {
|
|
17
|
+
if (!record || record.provider !== route.provider || record.model !== route.model) return false
|
|
18
|
+
if (record.baseURLFingerprint !== baseURLFingerprint(route.baseURL)) return false
|
|
19
|
+
return sessionAncestry(ctx, route.sessionId).includes(record.sourceSessionId ?? record.lineageId)
|
|
20
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { baseURLFingerprint, routeCompatible } from './route.js'
|
|
2
|
+
import { persistNativeImageReferences } from './dsh-responses.js'
|
|
3
|
+
|
|
4
|
+
export const NATIVE_BLOCK_TYPE = 'lcx-native-compaction-v5'
|
|
5
|
+
export const NATIVE_BLOCK_VERSION = 5
|
|
6
|
+
export const LEGACY_V4_BLOCK_TYPE = 'lcx-native-compaction-v4'
|
|
7
|
+
export const LEGACY_V4_BLOCK_VERSION = 4
|
|
8
|
+
const LEGACY_V3_PATTERN = /\[dsh-lcx-codex-v3-checkpoint:([0-9a-f-]{36})\]/iu
|
|
9
|
+
|
|
10
|
+
export const RETAINED_MESSAGE_TOKEN_BUDGET = 64_000
|
|
11
|
+
export const ASSISTANT_RETENTION_TOKEN_RESERVE = 24_000
|
|
12
|
+
export const ASSISTANT_RETENTION_PER_MESSAGE_TOKEN_CAP = 3_000
|
|
13
|
+
|
|
14
|
+
function isObject(value) { return value !== null && typeof value === 'object' && !Array.isArray(value) }
|
|
15
|
+
function textOf(message) { return (message?.content ?? []).filter((b) => b?.type === 'text').map((b) => b.text).join('') }
|
|
16
|
+
function estimatedItemTokens(item) { try { return Math.max(1, Math.ceil(JSON.stringify(item).length / 4)) } catch { return RETAINED_MESSAGE_TOKEN_BUDGET + 1 } }
|
|
17
|
+
function isRetainedClientItem(item) { return isObject(item) && (item.type === undefined || item.type === 'message') && ['user', 'developer', 'system'].includes(String(item.role ?? '')) }
|
|
18
|
+
function assistantTextParts(item) { if (!isObject(item) || item.type !== 'message' || item.role !== 'assistant' || !Array.isArray(item.content)) return []; return item.content.filter((part) => part?.type === 'output_text' && typeof part.text === 'string' && part.text.length > 0) }
|
|
19
|
+
function isRetainedAssistantItem(item) { return assistantTextParts(item).length > 0 }
|
|
20
|
+
function assistantText(item) { return assistantTextParts(item).map((part) => part.text).join('') }
|
|
21
|
+
|
|
22
|
+
function truncateVisibleAssistantItem(item, maxTokens = ASSISTANT_RETENTION_PER_MESSAGE_TOKEN_CAP) {
|
|
23
|
+
const text = assistantText(item); if (!text) return undefined
|
|
24
|
+
const maxChars = Math.max(256, maxTokens * 4); let retained = text
|
|
25
|
+
if (text.length > maxChars) { const marker = '\n…[LCX retained answer truncated]…\n'; const available = Math.max(0, maxChars - marker.length); const head = Math.floor(available * 0.72); const tail = available - head; retained = `${text.slice(0, head)}${marker}${text.slice(Math.max(head, text.length - tail))}` }
|
|
26
|
+
return { type: 'message', role: 'assistant', content: [{ type: 'output_text', text: retained }] }
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function selectNewest(candidates, budget, transform = (value) => structuredClone(value.item)) {
|
|
30
|
+
const selected = []; let used = 0
|
|
31
|
+
for (let index = candidates.length - 1; index >= 0; index -= 1) { const candidate = candidates[index]; const item = transform(candidate); if (!item) continue; const tokens = estimatedItemTokens(item); if (tokens > budget || used + tokens > budget) continue; selected.push({ index: candidate.index, item, tokens }); used += tokens }
|
|
32
|
+
return { selected, used }
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function retainedCompactionInput(input, options = {}) {
|
|
36
|
+
const budget = Number.isSafeInteger(options.tokenBudget) && options.tokenBudget > 0 ? options.tokenBudget : RETAINED_MESSAGE_TOKEN_BUDGET
|
|
37
|
+
const candidates = (input ?? []).map((item, index) => ({ item, index })).filter(({ item }) => isRetainedClientItem(item))
|
|
38
|
+
return selectNewest(candidates, budget).selected.sort((a, b) => a.index - b.index).map(({ item }) => item)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function retainedConversationPlan(input, options = {}) {
|
|
42
|
+
const totalBudget = Number.isSafeInteger(options.tokenBudget) && options.tokenBudget > 0 ? options.tokenBudget : RETAINED_MESSAGE_TOKEN_BUDGET
|
|
43
|
+
const assistantReserve = Math.min(totalBudget, Number.isSafeInteger(options.assistantTokenReserve) && options.assistantTokenReserve >= 0 ? options.assistantTokenReserve : ASSISTANT_RETENTION_TOKEN_RESERVE)
|
|
44
|
+
const perMessageCap = Number.isSafeInteger(options.assistantPerMessageTokenCap) && options.assistantPerMessageTokenCap > 0 ? options.assistantPerMessageTokenCap : ASSISTANT_RETENTION_PER_MESSAGE_TOKEN_CAP
|
|
45
|
+
const indexed = (input ?? []).map((item, index) => ({ item, index }))
|
|
46
|
+
const assistant = selectNewest(indexed.filter(({ item }) => isRetainedAssistantItem(item)), assistantReserve, ({ item }) => truncateVisibleAssistantItem(item, perMessageCap))
|
|
47
|
+
const remaining = Math.max(0, totalBudget - assistant.used)
|
|
48
|
+
const client = selectNewest(indexed.filter(({ item }) => isRetainedClientItem(item)), remaining)
|
|
49
|
+
const selected = [...client.selected, ...assistant.selected].sort((a, b) => a.index - b.index)
|
|
50
|
+
return { items: selected.map(({ item }) => item), clientCount: client.selected.length, assistantCount: assistant.selected.length, estimatedTokens: client.used + assistant.used, clientEstimatedTokens: client.used, assistantEstimatedTokens: assistant.used }
|
|
51
|
+
}
|
|
52
|
+
export function retainedConversationInput(input, options = {}) { return retainedConversationPlan(input, options).items }
|
|
53
|
+
export function hasRetainedCompactionInput(items) { return (items ?? []).some((item) => isRetainedClientItem(item) || isRetainedAssistantItem(item)) }
|
|
54
|
+
export function compactCheckpointId(message) { const source = message?.source; return source?.kind === 'plugin' && source?.plugin === 'compact' && typeof source.compactionId === 'string' && source.compactionId ? source.compactionId : undefined }
|
|
55
|
+
export function legacyCheckpointId(message) { return textOf(message).match(LEGACY_V3_PATTERN)?.[1]?.toLowerCase() }
|
|
56
|
+
export function activeCompactionId(session) { if (!session?.events) return undefined; const ended = new Set(); for (let index = session.events.length - 1; index >= 0; index -= 1) { const event = session.events[index]; if (event.type === 'compaction/end' && event.data?.compactionId) ended.add(event.data.compactionId); if (event.type === 'compaction/start' && event.data?.compactionId && !ended.has(event.data.compactionId)) return event.data.compactionId } return undefined }
|
|
57
|
+
|
|
58
|
+
export function createNativeCheckpointBlock({ session, route, result, input = [], imageMap, retentionOptions = {} }) {
|
|
59
|
+
const compactionId = activeCompactionId(session)
|
|
60
|
+
if (!compactionId) { const error = new Error('Native compaction could not correlate the active DSH compaction transaction'); error.code = 'LCX_COMPACTION_ID_UNAVAILABLE'; throw error }
|
|
61
|
+
const retention = retainedConversationPlan(input, retentionOptions)
|
|
62
|
+
const nativeOutput = persistNativeImageReferences([...retention.items, structuredClone(result.compaction)], imageMap)
|
|
63
|
+
return { type: NATIVE_BLOCK_TYPE, version: NATIVE_BLOCK_VERSION, retentionPolicy: 'conversation-fidelity-v1', compactionId, provider: route.provider, model: route.model, baseURLFingerprint: baseURLFingerprint(route.baseURL), sourceSessionId: route.sessionId, nativeOutput, nativeCompaction: structuredClone(result.compaction), retainedInputCount: retention.items.length, retainedClientCount: retention.clientCount, retainedAssistantCount: retention.assistantCount, retainedEstimatedTokens: retention.estimatedTokens, createdAt: Date.now() }
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function nativeCheckpointChunks(block, usage) {
|
|
67
|
+
const text = 'LCX Native V2 checkpoint saved in the DSH session log.'
|
|
68
|
+
return [{ type: 'block-start', index: 0, blockType: 'text' }, { type: 'text-delta', index: 0, text }, { type: 'block-end', index: 0, block: { type: 'text', text } }, { type: 'block-start', index: 1, blockType: NATIVE_BLOCK_TYPE }, { type: 'block-end', index: 1, block: structuredClone(block) }, ...(usage ? [{ type: 'usage', usage: structuredClone(usage) }] : []), { type: 'finish', reason: { kind: 'stop' } }]
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function compactionSummaryEvent(session, compactionId) { if (!session?.events || !compactionId) return undefined; for (let index = session.events.length - 1; index >= 0; index -= 1) { const event = session.events[index]; if (event.type === 'compaction/summary' && event.data?.compactionId === compactionId) return event } return undefined }
|
|
72
|
+
function nativeBlockFromSummary(event) { const raw = event?.data?.rawOutput ?? []; return raw.find((value) => value?.type === NATIVE_BLOCK_TYPE && value?.version === NATIVE_BLOCK_VERSION) ?? raw.find((value) => value?.type === LEGACY_V4_BLOCK_TYPE && value?.version === LEGACY_V4_BLOCK_VERSION) }
|
|
73
|
+
function validateCount(value) { return value === undefined || (Number.isSafeInteger(value) && value >= 0) }
|
|
74
|
+
export function stateFromSummaryEvent(event) {
|
|
75
|
+
if (event?.type !== 'compaction/summary') return undefined
|
|
76
|
+
const block = nativeBlockFromSummary(event); if (!isObject(block)) return undefined
|
|
77
|
+
if (block.compactionId !== event.data.compactionId || !Array.isArray(block.nativeOutput) || block.nativeOutput.length === 0) return undefined
|
|
78
|
+
if (typeof block.provider !== 'string' || typeof block.model !== 'string' || typeof block.baseURLFingerprint !== 'string' || typeof block.sourceSessionId !== 'string') return undefined
|
|
79
|
+
if (!validateCount(block.retainedInputCount) || !validateCount(block.retainedClientCount) || !validateCount(block.retainedAssistantCount)) return undefined
|
|
80
|
+
const compactions = block.nativeOutput.filter((item) => item?.type === 'compaction'); if (compactions.length !== 1 || typeof compactions[0]?.encrypted_content !== 'string') return undefined
|
|
81
|
+
const compactionIndex = block.nativeOutput.findIndex((item) => item?.type === 'compaction'); const prefix = compactionIndex >= 0 ? block.nativeOutput.slice(0, compactionIndex) : []
|
|
82
|
+
if (block.version === NATIVE_BLOCK_VERSION) { const clients = prefix.filter(isRetainedClientItem).length; const assistants = prefix.filter(isRetainedAssistantItem).length; const total = clients + assistants; if (block.retainedInputCount !== undefined && block.retainedInputCount !== total) return undefined; if (block.retainedClientCount !== undefined && block.retainedClientCount !== clients) return undefined; if (block.retainedAssistantCount !== undefined && block.retainedAssistantCount !== assistants) return undefined } else { const clients = prefix.filter(isRetainedClientItem).length; if (block.retainedInputCount !== undefined && block.retainedInputCount !== clients) return undefined }
|
|
83
|
+
return structuredClone(block)
|
|
84
|
+
}
|
|
85
|
+
export function checkpointStateForMessage(session, message) { const id = compactCheckpointId(message); return id ? stateFromSummaryEvent(compactionSummaryEvent(session, id)) : undefined }
|
|
86
|
+
export function stateRouteCompatible(state, route, ctx) { return routeCompatible(state, route, ctx) }
|
|
87
|
+
function eventMessage(session, seq) { const events = session?.events ?? []; const event = events[seq]?.seq === seq ? events[seq] : events.find((candidate) => candidate?.seq === seq); if (!event) return undefined; if (typeof session.deriveEventMessage === 'function') return session.deriveEventMessage(event) ?? undefined; if (event.type === 'user/message') return event.data; if (event.type === 'assistant/message') return event.data?.message; if (event.type === 'tool/result') return event.data?.message; return undefined }
|
|
88
|
+
function estimateChars(message) { try { return JSON.stringify(message).length } catch { return 0 } }
|
|
89
|
+
function expandedCheckpointMessages(session, compactionId, visited, depth) { if (depth > 16 || visited.has(compactionId)) return []; visited.add(compactionId); const summary = compactionSummaryEvent(session, compactionId); if (!summary) return []; const result = []; for (const seq of summary.data?.shadowedSeqs ?? []) { const message = eventMessage(session, seq); if (!message) continue; const nested = compactCheckpointId(message); if (nested) result.push(...expandedCheckpointMessages(session, nested, visited, depth + 1)); else result.push(structuredClone(message)) } return result }
|
|
90
|
+
export function shadowedMessagesForCheckpoint(session, compactionId) { return expandedCheckpointMessages(session, compactionId, new Set(), 0) }
|
|
91
|
+
function groupMessages(messages) { const groups = []; for (let index = 0; index < messages.length; index += 1) { const message = messages[index]; const toolCalls = message?.role === 'assistant' ? (message.content ?? []).filter((b) => b?.type === 'tool-call').map((b) => String(b.id)) : []; if (toolCalls.length === 0) { groups.push([message]); continue } const group = [message]; const pending = new Set(toolCalls); let cursor = index + 1; while (cursor < messages.length && pending.size > 0) { const next = messages[cursor]; const results = next?.role === 'user' ? (next.content ?? []).filter((b) => b?.type === 'tool-result').map((b) => String(b.toolCallId)) : []; if (results.length === 0) break; group.push(next); for (const id of results) pending.delete(id); cursor += 1 } if (pending.size === 0) index = cursor - 1; groups.push(group) } return groups }
|
|
92
|
+
export function portableMessagesForCheckpoint(session, compactionId, options = {}) { const maxChars = Number.isSafeInteger(options.maxChars) && options.maxChars > 0 ? options.maxChars : 80_000; const expanded = shadowedMessagesForCheckpoint(session, compactionId); if (expanded.length === 0) return []; const groups = groupMessages(expanded); const kept = []; let chars = 0; for (let index = groups.length - 1; index >= 0; index -= 1) { const group = groups[index]; const size = group.reduce((sum, message) => sum + estimateChars(message), 0); if (kept.length > 0 && chars + size > maxChars) break; kept.unshift(...group); chars += size } return kept }
|
|
93
|
+
export function rewriteCheckpointsPortable(messages, session, options = {}) { const rewritten = []; let changed = false; for (const message of messages ?? []) { const id = compactCheckpointId(message); if (!id || !checkpointStateForMessage(session, message)) { rewritten.push(message); continue } const portable = portableMessagesForCheckpoint(session, id, options); if (portable.length === 0) { rewritten.push(message); continue } rewritten.push(...portable); changed = true } return changed ? rewritten : messages }
|
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
import { fetchSseWithRetry } from './transport.js'
|
|
2
|
+
import { mergeFeatureHeader, responsesGenerationEnvelope } from './compact-v2.js'
|
|
3
|
+
import { responsesTools } from './dsh-responses.js'
|
|
4
|
+
|
|
5
|
+
function isObject(value) { return value !== null && typeof value === 'object' && !Array.isArray(value) }
|
|
6
|
+
function fail(message, code = 'LCX_RESPONSES_UPSTREAM_ERROR') { const e = new Error(message); e.code = code; return e }
|
|
7
|
+
|
|
8
|
+
function usageFrom(raw) {
|
|
9
|
+
if (!isObject(raw)) return undefined
|
|
10
|
+
const inputTotal = Number(raw.input_tokens ?? 0)
|
|
11
|
+
const output = Number(raw.output_tokens ?? 0)
|
|
12
|
+
const details = raw.input_tokens_details ?? raw.input_token_details ?? raw.prompt_tokens_details ?? {}
|
|
13
|
+
const cacheRead = Number(details.cached_tokens ?? 0)
|
|
14
|
+
const cacheWrite = Number(details.cache_write_tokens ?? 0)
|
|
15
|
+
const usage = {
|
|
16
|
+
inputTokens: Math.max(0, Number.isFinite(inputTotal) ? inputTotal - (Number.isFinite(cacheRead) ? cacheRead : 0) - (Number.isFinite(cacheWrite) ? cacheWrite : 0) : 0),
|
|
17
|
+
outputTokens: Number.isFinite(output) && output > 0 ? output : 0,
|
|
18
|
+
}
|
|
19
|
+
if (Number.isFinite(cacheRead) && cacheRead > 0) usage.cacheReadTokens = cacheRead
|
|
20
|
+
if (Number.isFinite(cacheWrite) && cacheWrite > 0) usage.cacheWriteTokens = cacheWrite
|
|
21
|
+
const reasoning = Number(raw.output_tokens_details?.reasoning_tokens ?? raw.output_token_details?.reasoning_tokens ?? 0)
|
|
22
|
+
if (Number.isFinite(reasoning) && reasoning > 0) usage.reasoningTokens = reasoning
|
|
23
|
+
return usage
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
async function* sseEvents(response, options = {}) {
|
|
27
|
+
if (!response?.body) throw fail('Responses replay returned no SSE body', 'LCX_INVALID_SSE')
|
|
28
|
+
const reader = response.body.getReader()
|
|
29
|
+
const decoder = new TextDecoder()
|
|
30
|
+
let pending = ''
|
|
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 ?? ''
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export function replayBody({ model, input, system, tools, promptCacheKey, promptCacheRetention, reasoningEffort, temperature, maxTokens }) {
|
|
109
|
+
return {
|
|
110
|
+
model,
|
|
111
|
+
input: structuredClone(input),
|
|
112
|
+
stream: true,
|
|
113
|
+
store: false,
|
|
114
|
+
tool_choice: 'auto',
|
|
115
|
+
parallel_tool_calls: true,
|
|
116
|
+
...(system !== undefined ? { instructions: system } : {}),
|
|
117
|
+
...(tools !== undefined ? { tools: responsesTools(tools) } : {}),
|
|
118
|
+
...(promptCacheKey ? { prompt_cache_key: String(promptCacheKey) } : {}),
|
|
119
|
+
...(promptCacheRetention ? { prompt_cache_retention: String(promptCacheRetention) } : {}),
|
|
120
|
+
...responsesGenerationEnvelope({ reasoningEffort, temperature, maxTokens }),
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export async function* requestNativeReplay({ baseURL, provider, model, input, system, tools, promptCacheKey, promptCacheRetention, reasoningEffort, temperature, maxTokens, headers, signal, timeoutMs, maxAttempts, maxResponseBytes }) {
|
|
125
|
+
const response = await fetchSseWithRetry(`${String(baseURL).replace(/\/+$/u, '')}/responses`, replayBody({ model, input, system, tools, promptCacheKey, promptCacheRetention, reasoningEffort, temperature, maxTokens }), mergeFeatureHeader(headers), signal, timeoutMs, { maxAttempts, maxResponseBytes })
|
|
126
|
+
const records = []
|
|
127
|
+
const byStreamIdentity = new Map()
|
|
128
|
+
let nextIndex = 0
|
|
129
|
+
let terminal
|
|
130
|
+
const identity = (kind, itemId, partIndex) => itemId === undefined ? undefined : `${kind}:${String(itemId)}:${partIndex ?? 0}`
|
|
131
|
+
const createRecord = (kind, streamIdentity, outputIndex, partIndex) => {
|
|
132
|
+
const existing = streamIdentity === undefined ? undefined : byStreamIdentity.get(streamIdentity)
|
|
133
|
+
if (existing) return { record: existing, created: false }
|
|
134
|
+
const record = { kind, index: nextIndex++, outputIndex, partIndex, streamIdentity, text: '', arguments: '', name: '', callId: undefined, itemId: undefined, terminal: undefined }
|
|
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 } : {}) }
|
|
281
|
+
}
|