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.
@@ -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
+ }
@@ -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 === '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,191 @@
1
+ import { fetchSseWithRetry } from './transport.js'
2
+ import { mergeFeatureHeader } 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 message = event?.error?.message ?? event?.response?.error?.message ?? `Responses stream ended with ${String(event?.type)}`
70
+ const e = fail(message)
71
+ if (Number.isInteger(event?.error?.status)) e.status = event.error.status
72
+ return e
73
+ }
74
+
75
+ export function replayBody({ model, input, system, tools, promptCacheKey }) {
76
+ return {
77
+ model,
78
+ input: structuredClone(input),
79
+ stream: true,
80
+ store: false,
81
+ ...(system !== undefined ? { instructions: system } : {}),
82
+ ...(tools !== undefined ? { tools: responsesTools(tools) } : {}),
83
+ ...(promptCacheKey ? { prompt_cache_key: String(promptCacheKey) } : {}),
84
+ }
85
+ }
86
+
87
+ export async function* requestNativeReplay({ baseURL, model, input, system, tools, promptCacheKey, headers, signal, timeoutMs, maxAttempts, maxResponseBytes }) {
88
+ const response = await fetchSseWithRetry(`${String(baseURL).replace(/\/+$/u, '')}/responses`, replayBody({ model, input, system, tools, promptCacheKey }), mergeFeatureHeader(headers), signal, timeoutMs, { maxAttempts, maxResponseBytes })
89
+ const textIndexes = new Map()
90
+ const reasoningIndexes = new Map()
91
+ const callIndexes = new Map()
92
+ let nextIndex = 0
93
+ let terminal
94
+ let finished = false
95
+ const allocate = (map, key, blockType) => {
96
+ if (map.has(key)) return { index: map.get(key), created: false }
97
+ const index = nextIndex++
98
+ map.set(key, index)
99
+ return { index, created: true, blockType }
100
+ }
101
+ for await (const event of sseEvents(response, { signal, maxResponseBytes })) {
102
+ if (!isObject(event)) continue
103
+ if (event.type === 'error' || event.type === 'response.failed' || event.type === 'response.incomplete') throw streamError(event)
104
+ if (event.type === 'response.output_text.delta') {
105
+ const key = `${event.output_index ?? ''}:${event.content_index ?? ''}`
106
+ const slot = allocate(textIndexes, key, 'text')
107
+ if (slot.created) yield { type: 'block-start', index: slot.index, blockType: 'text' }
108
+ if (typeof event.delta === 'string' && event.delta) yield { type: 'text-delta', index: slot.index, text: event.delta }
109
+ continue
110
+ }
111
+ if (event.type === 'response.reasoning_summary_text.delta' || event.type === 'response.reasoning_text.delta') {
112
+ const key = `${event.output_index ?? ''}:${event.summary_index ?? event.content_index ?? ''}`
113
+ const slot = allocate(reasoningIndexes, key, 'reasoning')
114
+ if (slot.created) yield { type: 'block-start', index: slot.index, blockType: 'reasoning' }
115
+ if (typeof event.delta === 'string' && event.delta) yield { type: 'reasoning-delta', index: slot.index, text: event.delta }
116
+ continue
117
+ }
118
+ if (event.type === 'response.function_call_arguments.delta') {
119
+ const callId = String(event.call_id ?? event.item_id ?? `call-${event.output_index ?? nextIndex}`)
120
+ const slot = allocate(callIndexes, callId, 'tool-call')
121
+ if (slot.created) yield { type: 'block-start', index: slot.index, blockType: 'tool-call' }
122
+ yield { type: 'tool-call-delta', index: slot.index, id: callId, ...(typeof event.name === 'string' ? { name: event.name } : {}), argumentsDelta: typeof event.delta === 'string' ? event.delta : '' }
123
+ continue
124
+ }
125
+ if (event.type === 'response.output_item.added' && event.item?.type === 'function_call') {
126
+ const callId = String(event.item.call_id ?? event.item.id ?? `call-${event.output_index ?? nextIndex}`)
127
+ const slot = allocate(callIndexes, callId, 'tool-call')
128
+ if (slot.created) {
129
+ yield { type: 'block-start', index: slot.index, blockType: 'tool-call' }
130
+ if (typeof event.item.name === 'string' || typeof event.item.arguments === 'string') {
131
+ yield { type: 'tool-call-delta', index: slot.index, id: callId, ...(typeof event.item.name === 'string' ? { name: event.item.name } : {}), argumentsDelta: typeof event.item.arguments === 'string' ? event.item.arguments : '' }
132
+ }
133
+ }
134
+ continue
135
+ }
136
+ if (event.type === 'response.completed' || event.type === 'response.done') {
137
+ terminal = event.response
138
+ continue
139
+ }
140
+ }
141
+ if (!isObject(terminal)) throw fail('Responses replay ended without response.completed', 'LCX_RESPONSES_INCOMPLETE')
142
+
143
+ // Close all blocks with a best-effort projection from the terminal response.
144
+ const terminalText = new Map()
145
+ const terminalReasoning = new Map()
146
+ const terminalCalls = new Map()
147
+ for (const [outputIndex, item] of (terminal.output ?? []).entries()) {
148
+ if (item?.type === 'message') {
149
+ for (const [contentIndex, part] of (item.content ?? []).entries()) {
150
+ if (part?.type === 'output_text') terminalText.set(`${outputIndex}:${contentIndex}`, String(part.text ?? ''))
151
+ }
152
+ } else if (item?.type === 'reasoning') {
153
+ const parts = Array.isArray(item.summary) && item.summary.length ? item.summary : item.content ?? []
154
+ parts.forEach((part, idx) => { if (typeof part?.text === 'string') terminalReasoning.set(`${outputIndex}:${idx}`, part.text) })
155
+ } else if (item?.type === 'function_call') {
156
+ const callId = String(item.call_id ?? item.id ?? `call-${outputIndex}`)
157
+ terminalCalls.set(callId, item)
158
+ }
159
+ }
160
+ for (const [key, index] of textIndexes) yield { type: 'block-end', index, block: { type: 'text', text: terminalText.get(key) ?? '' } }
161
+ for (const [key, index] of reasoningIndexes) yield { type: 'block-end', index, block: { type: 'reasoning', text: terminalReasoning.get(key) ?? '' } }
162
+ for (const [callId, index] of callIndexes) {
163
+ const item = terminalCalls.get(callId)
164
+ yield { type: 'block-end', index, block: { type: 'tool-call', id: callId, name: String(item?.name ?? ''), arguments: String(item?.arguments ?? '') } }
165
+ }
166
+
167
+ // Providers sometimes send no deltas for tiny answers; materialize unseen terminal blocks.
168
+ for (const [key, text] of terminalText) {
169
+ if (textIndexes.has(key)) continue
170
+ const index = nextIndex++
171
+ yield { type: 'block-start', index, blockType: 'text' }
172
+ if (text) yield { type: 'text-delta', index, text }
173
+ yield { type: 'block-end', index, block: { type: 'text', text } }
174
+ }
175
+ for (const [callId, item] of terminalCalls) {
176
+ if (callIndexes.has(callId)) continue
177
+ const index = nextIndex++
178
+ yield { type: 'block-start', index, blockType: 'tool-call' }
179
+ yield { type: 'tool-call-delta', index, id: callId, name: String(item.name ?? ''), argumentsDelta: String(item.arguments ?? '') }
180
+ yield { type: 'block-end', index, block: { type: 'tool-call', id: callId, name: String(item.name ?? ''), arguments: String(item.arguments ?? '') } }
181
+ }
182
+ const usage = usageFrom(terminal.usage)
183
+ if (usage) yield { type: 'usage', usage }
184
+ const reason = terminal.status === 'incomplete'
185
+ ? { kind: 'max-tokens' }
186
+ : terminal.error ? { kind: 'error', failure: { message: terminal.error.message ?? 'Responses error', code: 'LCX_RESPONSES_UPSTREAM_ERROR' } }
187
+ : terminalCalls.size > 0 ? { kind: 'tool-calls' } : { kind: 'stop' }
188
+ yield { type: 'finish', reason }
189
+ finished = true
190
+ if (!finished) throw fail('Responses replay did not finish')
191
+ }
package/lib/route.js ADDED
@@ -0,0 +1,137 @@
1
+ import { createHash, randomUUID } from 'node:crypto'
2
+ import { attributionHeaders, resolveRetryPolicy } from '@deepseek-ai/dsh-llm'
3
+ import { settingsNamespace } from '@deepseek-ai/dsh-settings'
4
+
5
+ export function normalizeBaseURL(value) {
6
+ return String(value ?? '').trim().replace(/\/+$/u, '')
7
+ }
8
+
9
+ export function isGptModel(model) {
10
+ return /(^|[^a-z])gpt(?:[^a-z]|$)/iu.test(String(model ?? ''))
11
+ }
12
+
13
+ export function baseURLFingerprint(baseURL) {
14
+ return createHash('sha256').update(normalizeBaseURL(baseURL), 'utf8').digest('hex')
15
+ }
16
+
17
+ export function routeFingerprint(route, options = {}) {
18
+ const includeSession = options.includeSession !== false
19
+ const fields = [route?.provider ?? '', route?.model ?? '', normalizeBaseURL(route?.baseURL), includeSession ? route?.sessionId ?? '' : '']
20
+ return createHash('sha256').update(fields.join('\u001f'), 'utf8').digest('hex')
21
+ }
22
+
23
+ export function promptCacheKey(route) {
24
+ if (!route?.sessionId) return undefined
25
+ // Stable per conversation + exact upstream route. It deliberately does not
26
+ // include the current checkpoint, so native compaction/replay keep one cache key.
27
+ return `dsh-lcx:${routeFingerprint(route)}`
28
+ }
29
+
30
+ function retryAttempts(policy, fallback = 3) {
31
+ if (!policy || typeof policy !== 'object') return fallback
32
+ try {
33
+ const resolved = resolveRetryPolicy(policy, 'llm-pi-ai provider retryPolicy')
34
+ if (resolved.mode === 'normal' && Number.isSafeInteger(resolved.maxRetries)) return Math.min(resolved.maxRetries + 1, 6)
35
+ } catch { /* keep fallback */ }
36
+ return fallback
37
+ }
38
+
39
+ export function settingsValue(ctx, namespace) {
40
+ const settings = ctx?.get?.('settings') ?? ctx?.settings
41
+ return settings?.get?.(settingsNamespace(namespace))
42
+ }
43
+
44
+ export function resolveResponsesRouteConfig(ctx, options, fallbackConfig) {
45
+ if (!isGptModel(options?.model)) return undefined
46
+ const provider = String(options?.provider ?? '')
47
+ const section = settingsValue(ctx, 'llm-pi-ai')
48
+ const profile = section?.providers?.[provider]
49
+ if (profile === undefined) {
50
+ if (provider !== fallbackConfig.provider) return undefined
51
+ return {
52
+ ...fallbackConfig,
53
+ provider,
54
+ model: String(options.model),
55
+ baseURL: normalizeBaseURL(fallbackConfig.baseURL),
56
+ }
57
+ }
58
+ if (profile.api !== 'openai-responses') return undefined
59
+ const baseURL = profile.baseURL ?? (provider === fallbackConfig.provider ? fallbackConfig.baseURL : undefined)
60
+ const apiKeyEnv = profile.apiKeyEnv ?? (provider === fallbackConfig.provider ? fallbackConfig.apiKeyEnv : undefined)
61
+ if (!baseURL || !apiKeyEnv) return undefined
62
+ return {
63
+ ...fallbackConfig,
64
+ provider,
65
+ model: String(options.model),
66
+ baseURL: normalizeBaseURL(baseURL),
67
+ apiKeyEnv,
68
+ headers: profile.headers && typeof profile.headers === 'object' ? { ...profile.headers } : { ...(fallbackConfig.headers ?? {}) },
69
+ timeoutMs: Number.isInteger(profile.timeoutMs) && profile.timeoutMs > 0 ? profile.timeoutMs : fallbackConfig.timeoutMs,
70
+ maxAttempts: retryAttempts(profile.retryPolicy, fallbackConfig.maxAttempts),
71
+ maxRequestImageBytes: Number.isSafeInteger(profile.maxRequestImageBytes) && profile.maxRequestImageBytes > 0
72
+ ? profile.maxRequestImageBytes
73
+ : fallbackConfig.maxRequestImageBytes,
74
+ requestImagePixelBudget: Number.isSafeInteger(profile.requestImagePixelBudget) && profile.requestImagePixelBudget > 0
75
+ ? profile.requestImagePixelBudget
76
+ : fallbackConfig.requestImagePixelBudget,
77
+ requestImageMaxBytes: Number.isSafeInteger(profile.requestImageMaxBytes) && profile.requestImageMaxBytes > 0
78
+ ? profile.requestImageMaxBytes
79
+ : fallbackConfig.requestImageMaxBytes,
80
+ }
81
+ }
82
+
83
+ export async function resolveApiKey(ctx, config) {
84
+ const credentials = ctx?.get?.('credentials') ?? ctx?.credentials
85
+ if (credentials?.resolve) {
86
+ const resolved = await credentials.resolve(config.apiKeyEnv)
87
+ if (typeof resolved?.value === 'string' && resolved.value.trim()) return resolved.value.trim()
88
+ }
89
+ const ambient = String(process.env[config.apiKeyEnv] ?? '').trim()
90
+ if (ambient) return ambient
91
+ const error = new Error(`DSH provider credential is unavailable: ${config.apiKeyEnv}`)
92
+ error.code = 'LCX_CREDENTIAL_UNAVAILABLE'
93
+ throw error
94
+ }
95
+
96
+ export async function authenticatedHeaders(ctx, config, sessionId, requestId) {
97
+ return {
98
+ ...(config.headers ?? {}),
99
+ ...attributionHeaders(),
100
+ authorization: `Bearer ${await resolveApiKey(ctx, config)}`,
101
+ 'x-client-request-id': requestId ?? randomUUID(),
102
+ ...(sessionId ? { 'session-id': String(sessionId) } : {}),
103
+ }
104
+ }
105
+
106
+ export function currentRoute(options, config) {
107
+ return {
108
+ provider: String(options?.provider ?? config.provider ?? ''),
109
+ model: String(options?.model ?? config.model ?? ''),
110
+ baseURL: normalizeBaseURL(config.baseURL),
111
+ sessionId: String(options?.sessionId ?? ''),
112
+ }
113
+ }
114
+
115
+ export function sessionAncestry(ctx, sessionId) {
116
+ if (!sessionId) return []
117
+ const sessions = ctx?.get?.('sessions') ?? ctx?.sessions
118
+ const result = []
119
+ const seen = new Set()
120
+ let current = sessions?.get?.(sessionId)
121
+ while (current && !seen.has(current.id)) {
122
+ seen.add(current.id)
123
+ result.push(String(current.id))
124
+ const parent = current.header?.parentSession
125
+ if (!parent) break
126
+ current = sessions?.get?.(parent)
127
+ }
128
+ return result
129
+ }
130
+
131
+ export function routeCompatible(record, route, ctx) {
132
+ if (!record || ![4, 5].includes(record.version)) return false
133
+ if (record.provider !== route.provider || record.model !== route.model) return false
134
+ if (record.baseURLFingerprint !== baseURLFingerprint(route.baseURL)) return false
135
+ const ancestry = sessionAncestry(ctx, route.sessionId)
136
+ return ancestry.includes(record.sourceSessionId)
137
+ }