dsh-lcx-codex 0.3.3 → 0.4.0-rc.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/ARCHITECTURE.md +89 -0
- package/CHANGELOG.md +84 -43
- package/README.md +208 -100
- package/README_EN.md +133 -101
- package/assets/dsh-lcx-codex-banner.jpg +0 -0
- package/cordis.patch.yml +10 -4
- package/lib/client.js +117 -194
- package/lib/compact-v2.js +90 -278
- package/lib/dsh-responses.js +220 -0
- package/lib/index.js +491 -1632
- package/lib/json-store.js +34 -0
- package/lib/legacy-v3.js +20 -0
- package/lib/native-checkpoint.js +93 -0
- package/lib/responses-replay.js +191 -0
- package/lib/route.js +137 -0
- package/lib/transport.js +58 -380
- package/lib/web-run-output.js +13 -152
- package/lib/web-search-alpha.js +26 -448
- package/lib/web-search-capability.js +12 -76
- package/lib/web-search-hosted.js +22 -372
- package/lib/web-search-ref-store.js +9 -69
- package/package.json +20 -17
- package/scripts/probe-alpha.mjs +28 -76
- package/scripts/validate-dsh-schema.mjs +31 -0
- package/lib/checkpoint-store-v3.js +0 -459
- package/lib/compact.js +0 -752
- package/lib/dsh-pi-responses.js +0 -339
- package/lib/private-file.js +0 -18
- package/lib/session-lease.js +0 -70
- package/lib/web-search-store.js +0 -181
|
@@ -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 === '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
|
+
}
|