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
package/lib/route.js
ADDED
|
@@ -0,0 +1,222 @@
|
|
|
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
|
+
function clampPromptCacheKey(value) {
|
|
24
|
+
if (value === undefined) return undefined
|
|
25
|
+
const chars = Array.from(String(value))
|
|
26
|
+
return chars.length <= 64 ? String(value) : chars.slice(0, 64).join('')
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function promptCacheSessionId(route, config = {}) {
|
|
30
|
+
if (config?.cacheRetention === 'none') return undefined
|
|
31
|
+
return route?.sessionId ? String(route.sessionId) : undefined
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function promptCacheKey(route, config = {}) {
|
|
35
|
+
return clampPromptCacheKey(promptCacheSessionId(route, config))
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function promptCacheRetention(config = {}) {
|
|
39
|
+
return config?.cacheRetention === 'long' && config?.supportsLongCacheRetention !== false ? '24h' : undefined
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function retryAttempts(policy, fallback = 3) {
|
|
43
|
+
if (!policy || typeof policy !== 'object') return fallback
|
|
44
|
+
try {
|
|
45
|
+
const resolved = resolveRetryPolicy(policy, 'llm-pi-ai provider retryPolicy')
|
|
46
|
+
if (resolved.mode === 'normal' && Number.isSafeInteger(resolved.maxRetries)) return Math.min(resolved.maxRetries + 1, 6)
|
|
47
|
+
} catch { /* keep fallback */ }
|
|
48
|
+
return fallback
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function settingsValue(ctx, namespace) {
|
|
52
|
+
const settings = ctx?.get?.('settings') ?? ctx?.settings
|
|
53
|
+
return settings?.get?.(settingsNamespace(namespace))
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const RESPONSES_COMPAT_FIELDS = new Set(['supportsDeveloperRole', 'supportsStrictMode', 'supportsLongCacheRetention'])
|
|
57
|
+
|
|
58
|
+
function copyResponsesCompat(target, source) {
|
|
59
|
+
if (!source || typeof source !== 'object') return
|
|
60
|
+
for (const field of RESPONSES_COMPAT_FIELDS) if (typeof source[field] === 'boolean') target[field] = source[field]
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function configuredResponsesCompat(profile, modelId) {
|
|
64
|
+
const compat = {}
|
|
65
|
+
copyResponsesCompat(compat, profile?.compat)
|
|
66
|
+
const configuredModels = Array.isArray(profile?.models) ? profile.models : []
|
|
67
|
+
const modelEntry = configuredModels.length > 0
|
|
68
|
+
? configuredModels.find((entry) => String(entry?.id ?? '') === String(modelId))
|
|
69
|
+
: profile?.modelOverrides?.[String(modelId)]
|
|
70
|
+
copyResponsesCompat(compat, modelEntry?.compat)
|
|
71
|
+
return Object.keys(compat).length > 0 ? compat : undefined
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function resolveResponsesRouteConfig(ctx, options, fallbackConfig) {
|
|
75
|
+
if (!isGptModel(options?.model)) return undefined
|
|
76
|
+
const provider = String(options?.provider ?? '')
|
|
77
|
+
const section = settingsValue(ctx, 'llm-pi-ai')
|
|
78
|
+
const profile = section?.providers?.[provider]
|
|
79
|
+
if (profile === undefined) {
|
|
80
|
+
if (provider !== fallbackConfig.provider) return undefined
|
|
81
|
+
return {
|
|
82
|
+
...fallbackConfig,
|
|
83
|
+
provider,
|
|
84
|
+
model: String(options.model),
|
|
85
|
+
api: 'openai-responses',
|
|
86
|
+
baseURL: normalizeBaseURL(fallbackConfig.baseURL),
|
|
87
|
+
cacheRetention: ['none', 'short', 'long'].includes(fallbackConfig.cacheRetention) ? fallbackConfig.cacheRetention : 'short',
|
|
88
|
+
supportsLongCacheRetention: fallbackConfig.supportsLongCacheRetention !== false,
|
|
89
|
+
responsesCompat: fallbackConfig.responsesCompat && typeof fallbackConfig.responsesCompat === 'object' ? { ...fallbackConfig.responsesCompat } : undefined,
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
if (profile.api !== 'openai-responses') return undefined
|
|
93
|
+
const baseURL = profile.baseURL ?? (provider === fallbackConfig.provider ? fallbackConfig.baseURL : undefined)
|
|
94
|
+
const apiKeyEnv = profile.apiKeyEnv ?? (provider === fallbackConfig.provider ? fallbackConfig.apiKeyEnv : undefined)
|
|
95
|
+
if (!baseURL || !apiKeyEnv) return undefined
|
|
96
|
+
const responsesCompat = configuredResponsesCompat(profile, options.model)
|
|
97
|
+
return {
|
|
98
|
+
...fallbackConfig,
|
|
99
|
+
provider,
|
|
100
|
+
model: String(options.model),
|
|
101
|
+
api: 'openai-responses',
|
|
102
|
+
baseURL: normalizeBaseURL(baseURL),
|
|
103
|
+
apiKeyEnv,
|
|
104
|
+
headers: profile.headers && typeof profile.headers === 'object' ? { ...profile.headers } : { ...(fallbackConfig.headers ?? {}) },
|
|
105
|
+
cacheRetention: ['none', 'short', 'long'].includes(profile.cacheRetention) ? profile.cacheRetention : (['none', 'short', 'long'].includes(fallbackConfig.cacheRetention) ? fallbackConfig.cacheRetention : 'short'),
|
|
106
|
+
supportsLongCacheRetention: responsesCompat?.supportsLongCacheRetention ?? fallbackConfig.supportsLongCacheRetention ?? true,
|
|
107
|
+
responsesCompat,
|
|
108
|
+
timeoutMs: Number.isInteger(profile.timeoutMs) && profile.timeoutMs > 0 ? profile.timeoutMs : fallbackConfig.timeoutMs,
|
|
109
|
+
maxAttempts: retryAttempts(profile.retryPolicy, fallbackConfig.maxAttempts),
|
|
110
|
+
maxRequestImageBytes: Number.isSafeInteger(profile.maxRequestImageBytes) && profile.maxRequestImageBytes > 0
|
|
111
|
+
? profile.maxRequestImageBytes
|
|
112
|
+
: fallbackConfig.maxRequestImageBytes,
|
|
113
|
+
requestImagePixelBudget: Number.isSafeInteger(profile.requestImagePixelBudget) && profile.requestImagePixelBudget > 0
|
|
114
|
+
? profile.requestImagePixelBudget
|
|
115
|
+
: fallbackConfig.requestImagePixelBudget,
|
|
116
|
+
requestImageMaxBytes: Number.isSafeInteger(profile.requestImageMaxBytes) && profile.requestImageMaxBytes > 0
|
|
117
|
+
? profile.requestImageMaxBytes
|
|
118
|
+
: fallbackConfig.requestImageMaxBytes,
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export async function resolveApiKey(ctx, config) {
|
|
123
|
+
const credentials = ctx?.get?.('credentials') ?? ctx?.credentials
|
|
124
|
+
if (credentials?.resolve) {
|
|
125
|
+
const resolved = await credentials.resolve(config.apiKeyEnv)
|
|
126
|
+
if (typeof resolved?.value === 'string' && resolved.value.trim()) return resolved.value.trim()
|
|
127
|
+
}
|
|
128
|
+
const ambient = String(process.env[config.apiKeyEnv] ?? '').trim()
|
|
129
|
+
if (ambient) return ambient
|
|
130
|
+
const error = new Error(`DSH provider credential is unavailable: ${config.apiKeyEnv}`)
|
|
131
|
+
error.code = 'LCX_CREDENTIAL_UNAVAILABLE'
|
|
132
|
+
throw error
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function hasHeader(headers, name) { return Object.keys(headers ?? {}).some((key) => key.toLowerCase() === name.toLowerCase()) }
|
|
136
|
+
function hasExplicitSessionAffinity(headers) { return ['session-id', 'session_id', 'x-session-id'].some((name) => hasHeader(headers, name)) }
|
|
137
|
+
function sessionAffinityFormat(config) {
|
|
138
|
+
if (config?.api && config.api !== 'openai-responses') return 'none'
|
|
139
|
+
const provider = String(config?.provider ?? '').toLowerCase()
|
|
140
|
+
const baseURL = String(config?.baseURL ?? '').toLowerCase()
|
|
141
|
+
return provider === 'openrouter' || baseURL.includes('openrouter.ai') ? 'openrouter' : 'openai'
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export async function authenticatedHeaders(ctx, config, sessionId, requestId) {
|
|
145
|
+
const explicit = { ...(config.headers ?? {}) }
|
|
146
|
+
const headers = {
|
|
147
|
+
...attributionHeaders(),
|
|
148
|
+
authorization: `Bearer ${await resolveApiKey(ctx, config)}`,
|
|
149
|
+
}
|
|
150
|
+
const sid = sessionId ? String(sessionId) : undefined
|
|
151
|
+
const format = sessionAffinityFormat(config)
|
|
152
|
+
if (sid && !hasExplicitSessionAffinity(explicit)) {
|
|
153
|
+
if (format === 'openai') headers.session_id = sid
|
|
154
|
+
else if (format === 'openrouter') headers['x-session-id'] = sid
|
|
155
|
+
}
|
|
156
|
+
if (requestId !== null && !hasHeader(explicit, 'x-client-request-id')) {
|
|
157
|
+
const correlation = requestId ?? (sid && format === 'openai' ? sid : (!sid ? randomUUID() : undefined))
|
|
158
|
+
if (correlation) headers['x-client-request-id'] = correlation
|
|
159
|
+
}
|
|
160
|
+
return { ...headers, ...explicit }
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export function currentRoute(options, config) {
|
|
164
|
+
return {
|
|
165
|
+
provider: String(options?.provider ?? config.provider ?? ''),
|
|
166
|
+
model: String(options?.model ?? config.model ?? ''),
|
|
167
|
+
baseURL: normalizeBaseURL(config.baseURL),
|
|
168
|
+
sessionId: String(options?.sessionId ?? ''),
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
export function generationControlsFromHeader(header, route) {
|
|
173
|
+
const config = header?.config
|
|
174
|
+
if (!config || String(config.provider ?? '') !== String(route?.provider ?? '') || String(config.model ?? '') !== String(route?.model ?? '')) return {}
|
|
175
|
+
const controls = {}
|
|
176
|
+
if (config.reasoningEffort !== undefined) controls.reasoningEffort = config.reasoningEffort
|
|
177
|
+
if (config.temperature !== undefined) controls.temperature = config.temperature
|
|
178
|
+
if (config.maxTokens !== undefined) controls.maxTokens = config.maxTokens
|
|
179
|
+
return controls
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
export function generationControlsFromSession(session, route) {
|
|
183
|
+
try { return generationControlsFromHeader(session?.requestHeader?.(), route) }
|
|
184
|
+
catch { return {} }
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
export function updateRequestHeaderCache(cache, session, event) {
|
|
188
|
+
if (!cache?.set || !session?.id) return false
|
|
189
|
+
let header
|
|
190
|
+
if (event?.type === 'request/header') header = event?.data?.header
|
|
191
|
+
else if (event?.type === 'compaction/start') {
|
|
192
|
+
try { header = session.requestHeader?.() } catch { return false }
|
|
193
|
+
}
|
|
194
|
+
if (!header) return false
|
|
195
|
+
cache.set(String(session.id), header)
|
|
196
|
+
return true
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
export function sessionAncestry(ctx, sessionId) {
|
|
200
|
+
if (!sessionId) return []
|
|
201
|
+
const sessions = ctx?.get?.('sessions') ?? ctx?.sessions
|
|
202
|
+
const result = []
|
|
203
|
+
const seen = new Set()
|
|
204
|
+
let current = sessions?.get?.(sessionId)
|
|
205
|
+
while (current && !seen.has(current.id)) {
|
|
206
|
+
seen.add(current.id)
|
|
207
|
+
result.push(String(current.id))
|
|
208
|
+
const parent = current.header?.parentSession
|
|
209
|
+
if (!parent) break
|
|
210
|
+
current = sessions?.get?.(parent)
|
|
211
|
+
}
|
|
212
|
+
return result
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
export function routeCompatible(record, route, ctx) {
|
|
216
|
+
if (!record || ![4, 5].includes(record.version)) return false
|
|
217
|
+
if (record.provider !== route.provider || record.model !== route.model) return false
|
|
218
|
+
if (record.baseURLFingerprint !== baseURLFingerprint(route.baseURL)) return false
|
|
219
|
+
// Ancestry authorizes portable migration only. Opaque native output is
|
|
220
|
+
// replayable exclusively by the session that created the checkpoint.
|
|
221
|
+
return record.sourceSessionId === route.sessionId
|
|
222
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
function abortReason(signal) {
|
|
2
|
+
if (signal?.reason instanceof Error) return signal.reason
|
|
3
|
+
return new DOMException('The operation was aborted', 'AbortError')
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
export class ServiceMutex {
|
|
7
|
+
constructor() {
|
|
8
|
+
this.locked = false
|
|
9
|
+
this.closed = false
|
|
10
|
+
this.closeReason = undefined
|
|
11
|
+
this.queue = []
|
|
12
|
+
this.idleWaiters = []
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
acquire(signal) {
|
|
16
|
+
if (this.closed) return Promise.reject(this.closeReason ?? new Error('service mutex closed'))
|
|
17
|
+
if (signal?.aborted) return Promise.reject(abortReason(signal))
|
|
18
|
+
if (!this.locked) {
|
|
19
|
+
this.locked = true
|
|
20
|
+
return Promise.resolve(() => this.release())
|
|
21
|
+
}
|
|
22
|
+
return new Promise((resolve, reject) => {
|
|
23
|
+
const waiter = { resolve, reject, signal, onAbort: undefined }
|
|
24
|
+
if (signal) {
|
|
25
|
+
waiter.onAbort = () => {
|
|
26
|
+
const index = this.queue.indexOf(waiter)
|
|
27
|
+
if (index >= 0) this.queue.splice(index, 1)
|
|
28
|
+
signal.removeEventListener('abort', waiter.onAbort)
|
|
29
|
+
reject(abortReason(signal))
|
|
30
|
+
}
|
|
31
|
+
signal.addEventListener('abort', waiter.onAbort, { once: true })
|
|
32
|
+
}
|
|
33
|
+
this.queue.push(waiter)
|
|
34
|
+
})
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
release() {
|
|
38
|
+
while (this.queue.length > 0) {
|
|
39
|
+
const waiter = this.queue.shift()
|
|
40
|
+
if (waiter.signal && waiter.onAbort) waiter.signal.removeEventListener('abort', waiter.onAbort)
|
|
41
|
+
if (waiter.signal?.aborted) {
|
|
42
|
+
waiter.reject(abortReason(waiter.signal))
|
|
43
|
+
continue
|
|
44
|
+
}
|
|
45
|
+
waiter.resolve(() => this.release())
|
|
46
|
+
return
|
|
47
|
+
}
|
|
48
|
+
this.locked = false
|
|
49
|
+
const idle = this.idleWaiters.splice(0)
|
|
50
|
+
for (const resolve of idle) resolve()
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async run(signal, task) {
|
|
54
|
+
const release = await this.acquire(signal)
|
|
55
|
+
try { return await task() }
|
|
56
|
+
finally { release() }
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
close(reason = new Error('service mutex closing')) {
|
|
60
|
+
if (!this.closed) {
|
|
61
|
+
this.closed = true
|
|
62
|
+
this.closeReason = reason
|
|
63
|
+
const queued = this.queue.splice(0)
|
|
64
|
+
for (const waiter of queued) {
|
|
65
|
+
if (waiter.signal && waiter.onAbort) waiter.signal.removeEventListener('abort', waiter.onAbort)
|
|
66
|
+
waiter.reject(reason)
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
if (!this.locked) return Promise.resolve()
|
|
70
|
+
return new Promise((resolve) => this.idleWaiters.push(resolve))
|
|
71
|
+
}
|
|
72
|
+
}
|