dsh-lcx-codex 0.4.1 → 0.4.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/route.js CHANGED
@@ -3,11 +3,12 @@
3
3
  import { createHash, randomUUID } from 'node:crypto'
4
4
  import { attributionHeaders, resolveRetryPolicy } from '@deepseek-ai/dsh-llm'
5
5
  import { settingsNamespace } from '@deepseek-ai/dsh-settings'
6
+ import { getBuiltinModels } from '@earendil-works/pi-ai/providers/all'
6
7
 
7
8
  /** @typedef {Record<string, string>} HeaderMap */
8
9
  /** @typedef {Parameters<typeof resolveRetryPolicy>[0]} RetryPolicyConfig */
9
10
  /** @typedef {'none' | 'short' | 'long'} CacheRetention */
10
- /** @typedef {{ supportsDeveloperRole?: boolean, supportsStrictMode?: boolean, supportsLongCacheRetention?: boolean }} ResponsesCompat */
11
+ /** @typedef {{ supportsDeveloperRole?: boolean, sessionAffinityFormat?: 'openai' | 'openai-nosession' | 'openrouter', supportsStrictMode?: boolean, supportsLongCacheRetention?: boolean, supportsOpenAIGrammarTools?: boolean, supportsAdditionalTools?: boolean, supportsToolSearch?: boolean, supportsExplicitPromptCacheMode?: boolean }} ResponsesCompat */
11
12
  /** @typedef {{ provider: string, model: string, baseURL: string, sessionId: string }} RouteIdentity */
12
13
  /** @typedef {{ provider?: unknown, model?: unknown, sessionId?: unknown }} RouteOptions */
13
14
  /** @typedef {{ id?: unknown, compat?: unknown }} ProviderModelProfile */
@@ -50,6 +51,7 @@ import { settingsNamespace } from '@deepseek-ai/dsh-settings'
50
51
  * @property {HeaderMap} [headers]
51
52
  * @property {unknown} [cacheRetention]
52
53
  * @property {unknown} [supportsLongCacheRetention]
54
+ * @property {unknown} [supportsExplicitPromptCacheMode]
53
55
  * @property {unknown} [responsesCompat]
54
56
  * @property {number} [timeoutMs]
55
57
  * @property {number} [maxAttempts]
@@ -76,11 +78,6 @@ export function normalizeBaseURL(value) {
76
78
  return String(value ?? '').trim().replace(/\/+$/u, '')
77
79
  }
78
80
 
79
- /** @param {unknown} model */
80
- export function isGptModel(model) {
81
- return /(^|[^a-z])gpt(?:[^a-z]|$)/iu.test(String(model ?? ''))
82
- }
83
-
84
81
  /** @param {unknown} baseURL */
85
82
  export function baseURLFingerprint(baseURL) {
86
83
  return createHash('sha256').update(normalizeBaseURL(baseURL), 'utf8').digest('hex')
@@ -148,7 +145,7 @@ export function settingsValue(ctx, namespace) {
148
145
  }
149
146
 
150
147
  /** @type {Set<keyof ResponsesCompat>} */
151
- const RESPONSES_COMPAT_FIELDS = new Set(['supportsDeveloperRole', 'supportsStrictMode', 'supportsLongCacheRetention'])
148
+ const RESPONSES_COMPAT_FIELDS = new Set(['supportsDeveloperRole', 'sessionAffinityFormat', 'supportsStrictMode', 'supportsLongCacheRetention', 'supportsOpenAIGrammarTools', 'supportsAdditionalTools', 'supportsToolSearch', 'supportsExplicitPromptCacheMode'])
152
149
 
153
150
  /**
154
151
  * @param {ResponsesCompat} target
@@ -156,7 +153,22 @@ const RESPONSES_COMPAT_FIELDS = new Set(['supportsDeveloperRole', 'supportsStric
156
153
  */
157
154
  function copyResponsesCompat(target, source) {
158
155
  if (!source || typeof source !== 'object') return
159
- for (const field of RESPONSES_COMPAT_FIELDS) if (typeof /** @type {Record<keyof ResponsesCompat, unknown>} */ (source)[field] === 'boolean') target[field] = /** @type {boolean} */ (/** @type {Record<keyof ResponsesCompat, unknown>} */ (source)[field])
156
+ const values = /** @type {Record<string, unknown>} */ (source)
157
+ const output = /** @type {Record<string, unknown>} */ (/** @type {unknown} */ (target))
158
+ for (const field of RESPONSES_COMPAT_FIELDS) {
159
+ const value = values[field]
160
+ if (field === 'sessionAffinityFormat') {
161
+ if (['openai', 'openai-nosession', 'openrouter'].includes(String(value))) output[field] = value
162
+ } else if (typeof value === 'boolean') output[field] = value
163
+ }
164
+ }
165
+
166
+ /** @param {unknown} provider @param {unknown} modelId */
167
+ function builtinResponsesModel(provider, modelId) {
168
+ try {
169
+ return getBuiltinModels(/** @type {any} */ (String(provider ?? '')))
170
+ .find((model) => model?.id === String(modelId ?? '') && model?.api === 'openai-responses')
171
+ } catch { return undefined }
160
172
  }
161
173
 
162
174
  /**
@@ -183,50 +195,51 @@ function configuredResponsesCompat(profile, modelId) {
183
195
  * @returns {ResolvedResponsesRoute | undefined}
184
196
  */
185
197
  export function resolveResponsesRouteConfig(ctx, options, fallbackConfig) {
186
- if (!isGptModel(options?.model)) return undefined
187
198
  const provider = String(options?.provider ?? '')
199
+ const model = String(options?.model ?? '')
188
200
  const section = settingsValue(ctx, 'llm-pi-ai')
189
201
  const profile = section?.providers?.[provider]
190
- if (profile === undefined) {
191
- if (provider !== fallbackConfig.provider) return undefined
192
- return {
193
- ...fallbackConfig,
194
- provider,
195
- model: String(options.model),
196
- api: 'openai-responses',
197
- baseURL: normalizeBaseURL(fallbackConfig.baseURL),
198
- cacheRetention: /** @type {CacheRetention} */ (['none', 'short', 'long'].includes(/** @type {string} */ (fallbackConfig.cacheRetention)) ? fallbackConfig.cacheRetention : 'short'),
199
- supportsLongCacheRetention: fallbackConfig.supportsLongCacheRetention !== false,
200
- responsesCompat: fallbackConfig.responsesCompat && typeof fallbackConfig.responsesCompat === 'object' ? /** @type {ResponsesCompat} */ ({ ...fallbackConfig.responsesCompat }) : undefined,
201
- }
202
- }
203
- if (profile.api !== 'openai-responses') return undefined
204
- const baseURL = profile.baseURL ?? (provider === fallbackConfig.provider ? fallbackConfig.baseURL : undefined)
205
- const apiKeyEnv = profile.apiKeyEnv ?? (provider === fallbackConfig.provider ? fallbackConfig.apiKeyEnv : undefined)
202
+ const fallbackOwned = provider === fallbackConfig.provider
203
+ if (profile === undefined && !fallbackOwned) return undefined
204
+ const configured = /** @type {ProviderProfile} */ (profile ?? {})
205
+
206
+ const builtin = builtinResponsesModel(provider, model)
207
+ const api = configured.api ?? builtin?.api ?? (fallbackOwned ? 'openai-responses' : undefined)
208
+ if (api !== 'openai-responses') return undefined
209
+
210
+ const baseURL = configured.baseURL ?? builtin?.baseUrl ?? (fallbackOwned ? fallbackConfig.baseURL : undefined)
211
+ const apiKeyEnv = configured.apiKeyEnv ?? (fallbackOwned ? fallbackConfig.apiKeyEnv : undefined)
206
212
  if (!baseURL || !apiKeyEnv) return undefined
207
- const responsesCompat = configuredResponsesCompat(profile, options.model)
213
+
214
+ /** @type {ResponsesCompat} */
215
+ const responsesCompat = {}
216
+ copyResponsesCompat(responsesCompat, builtin?.compat)
217
+ copyResponsesCompat(responsesCompat, fallbackOwned ? fallbackConfig.responsesCompat : undefined)
218
+ copyResponsesCompat(responsesCompat, configuredResponsesCompat(configured, model))
219
+ // Host Pi rc.2 withholds this field; the plugin opt-in still requires Pi's exact model capability.
220
+ const promptCacheModel = fallbackOwned && fallbackConfig.supportsExplicitPromptCacheMode === true
221
+ ? builtinResponsesModel('openai', model)
222
+ : undefined
223
+ const promptCacheCompat = /** @type {ResponsesCompat | undefined} */ (promptCacheModel?.compat)
224
+ if (promptCacheCompat?.supportsExplicitPromptCacheMode === true) responsesCompat.supportsExplicitPromptCacheMode = true
225
+ const resolvedCompat = Object.keys(responsesCompat).length > 0 ? responsesCompat : undefined
226
+
208
227
  return {
209
228
  ...fallbackConfig,
210
229
  provider,
211
- model: String(options.model),
230
+ model,
212
231
  api: 'openai-responses',
213
232
  baseURL: normalizeBaseURL(baseURL),
214
233
  apiKeyEnv,
215
- headers: profile.headers && typeof profile.headers === 'object' ? { ...profile.headers } : { ...(fallbackConfig.headers ?? {}) },
216
- cacheRetention: /** @type {CacheRetention} */ (['none', 'short', 'long'].includes(/** @type {string} */ (profile.cacheRetention)) ? profile.cacheRetention : (['none', 'short', 'long'].includes(/** @type {string} */ (fallbackConfig.cacheRetention)) ? fallbackConfig.cacheRetention : 'short')),
217
- supportsLongCacheRetention: responsesCompat?.supportsLongCacheRetention ?? /** @type {boolean | undefined} */ (fallbackConfig.supportsLongCacheRetention) ?? true,
218
- responsesCompat,
219
- timeoutMs: Number.isInteger(profile.timeoutMs) && /** @type {number} */ (profile.timeoutMs) > 0 ? profile.timeoutMs : fallbackConfig.timeoutMs,
220
- maxAttempts: retryAttempts(profile.retryPolicy, fallbackConfig.maxAttempts),
221
- maxRequestImageBytes: Number.isSafeInteger(profile.maxRequestImageBytes) && /** @type {number} */ (profile.maxRequestImageBytes) > 0
222
- ? profile.maxRequestImageBytes
223
- : fallbackConfig.maxRequestImageBytes,
224
- requestImagePixelBudget: Number.isSafeInteger(profile.requestImagePixelBudget) && /** @type {number} */ (profile.requestImagePixelBudget) > 0
225
- ? profile.requestImagePixelBudget
226
- : fallbackConfig.requestImagePixelBudget,
227
- requestImageMaxBytes: Number.isSafeInteger(profile.requestImageMaxBytes) && /** @type {number} */ (profile.requestImageMaxBytes) > 0
228
- ? profile.requestImageMaxBytes
229
- : fallbackConfig.requestImageMaxBytes,
234
+ headers: configured.headers && typeof configured.headers === 'object' ? { ...configured.headers } : { ...(fallbackConfig.headers ?? {}) },
235
+ cacheRetention: /** @type {CacheRetention} */ (['none', 'short', 'long'].includes(/** @type {string} */ (configured.cacheRetention)) ? configured.cacheRetention : (['none', 'short', 'long'].includes(/** @type {string} */ (fallbackConfig.cacheRetention)) ? fallbackConfig.cacheRetention : 'short')),
236
+ supportsLongCacheRetention: resolvedCompat?.supportsLongCacheRetention ?? /** @type {boolean | undefined} */ (fallbackConfig.supportsLongCacheRetention) ?? true,
237
+ responsesCompat: resolvedCompat,
238
+ timeoutMs: Number.isInteger(configured.timeoutMs) && /** @type {number} */ (configured.timeoutMs) > 0 ? /** @type {number} */ (configured.timeoutMs) : fallbackConfig.timeoutMs,
239
+ maxAttempts: retryAttempts(configured.retryPolicy, fallbackConfig.maxAttempts),
240
+ maxRequestImageBytes: Number.isSafeInteger(configured.maxRequestImageBytes) && /** @type {number} */ (configured.maxRequestImageBytes) > 0 ? /** @type {number} */ (configured.maxRequestImageBytes) : fallbackConfig.maxRequestImageBytes,
241
+ requestImagePixelBudget: Number.isSafeInteger(configured.requestImagePixelBudget) && /** @type {number} */ (configured.requestImagePixelBudget) > 0 ? /** @type {number} */ (configured.requestImagePixelBudget) : fallbackConfig.requestImagePixelBudget,
242
+ requestImageMaxBytes: Number.isSafeInteger(configured.requestImageMaxBytes) && /** @type {number} */ (configured.requestImageMaxBytes) > 0 ? /** @type {number} */ (configured.requestImageMaxBytes) : fallbackConfig.requestImageMaxBytes,
230
243
  }
231
244
  }
232
245
 
@@ -258,6 +271,8 @@ function hasExplicitSessionAffinity(headers) { return ['session-id', 'session_id
258
271
  /** @param {Partial<ResolvedResponsesRoute> | null | undefined} config */
259
272
  function sessionAffinityFormat(config) {
260
273
  if (config?.api && config.api !== 'openai-responses') return 'none'
274
+ const explicit = config?.responsesCompat?.sessionAffinityFormat
275
+ if (['openai', 'openai-nosession', 'openrouter'].includes(String(explicit))) return explicit
261
276
  const provider = String(config?.provider ?? '').toLowerCase()
262
277
  const baseURL = String(config?.baseURL ?? '').toLowerCase()
263
278
  return provider === 'openrouter' || baseURL.includes('openrouter.ai') ? 'openrouter' : 'openai'
@@ -284,7 +299,7 @@ export async function authenticatedHeaders(ctx, config, sessionId, requestId) {
284
299
  else if (format === 'openrouter') headers['x-session-id'] = sid
285
300
  }
286
301
  if (requestId !== null && !hasHeader(explicit, 'x-client-request-id')) {
287
- const correlation = requestId ?? (sid && format === 'openai' ? sid : (!sid ? randomUUID() : undefined))
302
+ const correlation = requestId ?? (sid && (format === 'openai' || format === 'openai-nosession') ? sid : (!sid ? randomUUID() : undefined))
288
303
  if (correlation) headers['x-client-request-id'] = correlation
289
304
  }
290
305
  return { ...headers, ...explicit }
package/lib/transport.js CHANGED
@@ -6,9 +6,14 @@ export function abortIfNeeded(signal) {
6
6
  }
7
7
  function makeError(message, code, extra = {}) { const error = new Error(message); error.code = code; Object.assign(error, extra); return error }
8
8
  function retryableStatus(status) { return status === 408 || status === 409 || status === 425 || status === 429 || (status >= 500 && status <= 599) }
9
- function delayFromHeaders(headers, attempt) {
10
- const millis = Number(headers?.get?.('retry-after-ms')); if (Number.isFinite(millis) && millis >= 0) return Math.min(millis, 30_000)
9
+ function retryAfterFromHeaders(headers) {
10
+ const rawMillis = headers?.get?.('retry-after-ms'); const millis = Number(rawMillis); if (rawMillis !== null && rawMillis !== undefined && rawMillis !== '' && Number.isFinite(millis) && millis >= 0) return Math.min(millis, 30_000)
11
11
  const retryAfter = headers?.get?.('retry-after'); if (retryAfter) { const seconds = Number(retryAfter); if (Number.isFinite(seconds) && seconds >= 0) return Math.min(seconds * 1000, 30_000); const date = Date.parse(retryAfter); if (!Number.isNaN(date)) return Math.min(Math.max(0, date - Date.now()), 30_000) }
12
+ return undefined
13
+ }
14
+ function delayFromHeaders(headers, attempt) {
15
+ const retryAfter = retryAfterFromHeaders(headers)
16
+ if (retryAfter !== undefined) return retryAfter
12
17
  return Math.min(10_000, 500 * 2 ** Math.max(0, attempt - 1))
13
18
  }
14
19
  async function sleep(ms, signal) { if (ms <= 0) return; await new Promise((resolve, reject) => { if (signal?.aborted) return reject(signal.reason); const timer = setTimeout(resolve, ms); signal?.addEventListener('abort', () => { clearTimeout(timer); reject(signal.reason) }, { once: true }) }) }
@@ -25,7 +30,8 @@ export async function fetchJsonWithRetry(url, body, headers = {}, signal, timeou
25
30
  const text = await readLimited(response, maxResponseBytes)
26
31
  if (!response.ok) {
27
32
  const retryable = retryableStatus(response.status)
28
- const error = makeError(`HTTP ${response.status}`, retryable ? 'LCX_HTTP_RETRYABLE' : 'LCX_HTTP_ERROR', { status: response.status, retryable, requestId: response.headers.get('x-request-id') ?? response.headers.get('request-id') ?? undefined })
33
+ const providerRetryAfterMs = retryAfterFromHeaders(response.headers)
34
+ const error = makeError(`HTTP ${response.status}`, retryable ? 'LCX_HTTP_RETRYABLE' : 'LCX_HTTP_ERROR', { status: response.status, retryable, requestId: response.headers.get('x-request-id') ?? response.headers.get('request-id') ?? undefined, ...(providerRetryAfterMs === undefined ? {} : { providerRetryAfterMs }) })
29
35
  if (!error.retryable || attempt >= maxAttempts) throw error; last = error; await sleep(delayFromHeaders(response.headers, attempt), signal); continue
30
36
  }
31
37
  let parsed
@@ -50,7 +56,7 @@ export async function fetchSseWithRetry(url, body, headers = {}, signal, timeout
50
56
  const response = await fetch(url, { method: 'POST', headers: { 'content-type': 'application/json', accept: 'text/event-stream', ...headers }, body: JSON.stringify(body), signal: requestSignal, redirect: 'error' })
51
57
  if (!response.ok) {
52
58
  await readLimited(response, Math.min(options.maxResponseBytes ?? DEFAULT_MAX_BYTES, 512 * 1024))
53
- const retryable = retryableStatus(response.status); const error = makeError(`HTTP ${response.status}`, retryable ? 'LCX_HTTP_RETRYABLE' : 'LCX_HTTP_ERROR', { status: response.status, retryable, requestId: response.headers.get('x-request-id') ?? response.headers.get('request-id') ?? undefined })
59
+ const retryable = retryableStatus(response.status); const providerRetryAfterMs = retryAfterFromHeaders(response.headers); const error = makeError(`HTTP ${response.status}`, retryable ? 'LCX_HTTP_RETRYABLE' : 'LCX_HTTP_ERROR', { status: response.status, retryable, requestId: response.headers.get('x-request-id') ?? response.headers.get('request-id') ?? undefined, ...(providerRetryAfterMs === undefined ? {} : { providerRetryAfterMs }) })
54
60
  if (!retryable || attempt >= maxAttempts) throw error; last = error; await sleep(delayFromHeaders(response.headers, attempt), signal); continue
55
61
  }
56
62
  if (typeof options.consume === 'function') return await options.consume(response, { requestSignal }); return response
@@ -9,7 +9,7 @@ const RESPONSE_LENGTHS = ['short','medium','long']
9
9
  export const ALPHA_SEARCH_PARAMETERS = { type: 'object', properties: { action: { type: 'string', enum: ALPHA_ACTIONS }, query: { type: 'string' }, domains: { type: 'array', items: { type: 'string' } }, recency: { type: 'integer' }, refId: { type: 'string' }, lineNumber: { type: 'integer' }, linkId: { type: 'integer' }, pattern: { type: 'string' }, pageNumber: { type: 'integer' }, ticker: { type: 'string' }, assetType: { type: 'string', enum: ASSET_TYPES }, market: { type: 'string' }, location: { type: 'string' }, start: { type: 'string' }, duration: { type: 'integer' }, fn: { type: 'string', enum: ['schedule','standings'] }, league: { type: 'string', enum: LEAGUES }, team: { type: 'string' }, opponent: { type: 'string' }, dateFrom: { type: 'string' }, dateTo: { type: 'string' }, numberOfGames: { type: 'integer' }, locale: { type: 'string' }, utcOffset: { type: 'string' }, responseLength: { type: 'string', enum: RESPONSE_LENGTHS } }, required: ['action'], additionalProperties: false }
10
10
  export const ALPHA_SEARCH_OUTPUT = { type: 'object', properties: { mode: { type:'string' }, action: { type:'string' }, capability:{type:'string'}, emulation:{type:'string'}, content:{type:'string'}, results:{type:'array',items:{type:'object'}}, refs:{type:'array',items:{type:'string'}}, sources:{type:'array',items:{type:'object'}}, citations:{type:'array',items:{type:'object'}}, outputBlocks:{type:'array',items:{type:'object'}}, links:{type:'array',items:{type:'object'}}, pdfRefs:{type:'array',items:{type:'string'}}, domains:{type:'array',items:{type:'string'}}, lineRange:{type:'object'}, requestId:{type:'string'}, responseId:{type:'string'}, retrievedAt:{type:'string'}, warnings:{type:'array',items:{type:'string'}} }, required:['mode','action','capability','emulation','content','results','refs','sources','citations','outputBlocks','links','pdfRefs','domains','requestId','retrievedAt','warnings'], additionalProperties:false }
11
11
  export const ALPHA_SCHEMA_FINGERPRINT = createHash('sha256').update(JSON.stringify(ALPHA_SEARCH_PARAMETERS)).digest('hex')
12
- export const ALPHA_PROBE_VERSION = 10
12
+ export const ALPHA_PROBE_VERSION = 11
13
13
  function failure(message, code='WEB_INVALID_REQUEST') { const e=new Error(message); e.code=code; return e }
14
14
  function isObject(v){ return v!==null && typeof v==='object' && !Array.isArray(v) }
15
15
  function text(value,field,max=1000){ if(typeof value!=='string'||!value.trim()||value.trim().length>max) throw failure(`websearch_alpha.${field} is invalid`); return value.trim() }
@@ -101,7 +101,9 @@ function safeResult(value,seen=new Set()){ if(value===null||typeof value!=='obje
101
101
  function artifacts(results){ const refs=[],refRecords=[],sources=[],seenRefs=new Set(),seenSources=new Set(); const visit=(value)=>{ if(!value||typeof value!=='object') return; if(Array.isArray(value)){ value.forEach(visit); return } const candidate=typeof value.ref_id==='string'&&value.ref_id?value.ref_id:typeof value.id==='string'&&/^turn[\w-]+$/u.test(value.id)?value.id:undefined; const refId=isAlphaHttpUrl(candidate)?undefined:candidate; const url=httpUrl(value.url??value.source_url??value.source_website_url??candidate); if(refId&&!seenRefs.has(refId)){seenRefs.add(refId);refs.push(refId);refRecords.push({refId,...(url?{url}:{})})} if(url&&!seenSources.has(url)){seenSources.add(url);sources.push({url,...(typeof value.title==='string'?{title:value.title}:{}),...(typeof value.snippet==='string'?{snippet:value.snippet}:{}),...(refId?{refId}:{})})} Object.values(value).forEach(visit) }; visit(results); return {refs,refRecords,sources} }
102
102
  const ACTION_ERROR=/^\s*(?:Error parsing function call\b|Invalid function_name=|Invalid function call\b)/iu
103
103
  const ALPHA_SEMANTIC_FAILURE=/^\s*(?:reference(?: id)?\s+(?:is\s+)?(?:invalid|unavailable)\b|unable to access requested content\b|service unavailable\b)/iu
104
- export function parseAlphaSearchResponse(response,options){ if(!isObject(response)||typeof response.output!=='string'||(response.results!==undefined&&!Array.isArray(response.results))) throw failure('LCX Alpha Web Search returned an invalid response','LCX_ALPHA_INVALID_RESPONSE'); if(ACTION_ERROR.test(response.output)||(['open','find','click','screenshot'].includes(options.action)&&ALPHA_SEMANTIC_FAILURE.test(response.output))) throw failure('LCX Alpha Web Search could not execute the requested action','LCX_ALPHA_ACTION_FAILED'); const results=safeResult(response.results??[]); const a=artifacts(results); const outputBlocks=parseWebRunOutput(response.output); for(const block of outputBlocks) if(block.url){ const url=httpUrl(block.url); if(url) block.url=url; else delete block.url } for(const block of outputBlocks) for(const ref of block.references??[]){ if(isAlphaHttpUrl(ref)) continue; if(!a.refs.includes(ref)) a.refs.push(ref); if(!a.refRecords.some((item)=>item.refId===ref)) a.refRecords.push({refId:ref,...(block.url?{url:block.url}:{})}) } for(const source of outputBlocks.flatMap((b)=>b.url?[{url:b.url,...(b.title?{title:b.title}:{})}]:[])) if(!a.sources.some((x)=>x.url===source.url)) a.sources.push(source); return {mode:'alpha',action:options.action,capability:options.capability,emulation:options.capability==='native'?'native':'unknown',content:response.output,results,refs:a.refs,sources:a.sources,citations:a.sources.map((s)=>({...s})),outputBlocks,links:outputLinks(outputBlocks),pdfRefs:outputPdfRefs(outputBlocks),domains:outputDomains(outputBlocks),...(outputLineRange(outputBlocks)?{lineRange:outputLineRange(outputBlocks)}:{}),requestId:options.requestId,...(typeof response.id==='string'&&response.id?{responseId:response.id}:{}),retrievedAt:options.retrievedAt??new Date().toISOString(),warnings:options.capability==='command-capable'?['Alpha command behavior is verified, but trusted native backend provenance is unavailable.']:[],refRecords:a.refRecords} }
104
+ const ALPHA_COMMAND_ERROR_ENVELOPE=/^\s*Internal Error\s*\([^\r\n)]*\)\s*(?:\r?\n[ \t]*)+(?:\uE200cite\uE202[^\uE201\r\n]+\uE201[ \t]*(?:\[wordlim:\s*\d+\][ \t]*)?)?Unable to resolve (open|find|click|screenshot) call\s*:/iu
105
+ function alphaCommandErrorEnvelope(output,action){ const match=String(output??'').match(ALPHA_COMMAND_ERROR_ENVELOPE); return match?.[1]?.toLowerCase()===action }
106
+ export function parseAlphaSearchResponse(response,options){ if(!isObject(response)||typeof response.output!=='string'||(response.results!==undefined&&!Array.isArray(response.results))) throw failure('LCX Alpha Web Search returned an invalid response','LCX_ALPHA_INVALID_RESPONSE'); if(ACTION_ERROR.test(response.output)||(['open','find','click','screenshot'].includes(options.action)&&(ALPHA_SEMANTIC_FAILURE.test(response.output)||alphaCommandErrorEnvelope(response.output,options.action)))) throw failure('LCX Alpha Web Search could not execute the requested action','LCX_ALPHA_ACTION_FAILED'); const results=safeResult(response.results??[]); const a=artifacts(results); const outputBlocks=parseWebRunOutput(response.output); for(const block of outputBlocks) if(block.url){ const url=httpUrl(block.url); if(url) block.url=url; else delete block.url } for(const block of outputBlocks) for(const ref of block.references??[]){ if(isAlphaHttpUrl(ref)) continue; if(!a.refs.includes(ref)) a.refs.push(ref); if(!a.refRecords.some((item)=>item.refId===ref)) a.refRecords.push({refId:ref,...(block.url?{url:block.url}:{})}) } for(const source of outputBlocks.flatMap((b)=>b.url?[{url:b.url,...(b.title?{title:b.title}:{})}]:[])) if(!a.sources.some((x)=>x.url===source.url)) a.sources.push(source); return {mode:'alpha',action:options.action,capability:options.capability,emulation:options.capability==='native'?'native':'unknown',content:response.output,results,refs:a.refs,sources:a.sources,citations:a.sources.map((s)=>({...s})),outputBlocks,links:outputLinks(outputBlocks),pdfRefs:outputPdfRefs(outputBlocks),domains:outputDomains(outputBlocks),...(outputLineRange(outputBlocks)?{lineRange:outputLineRange(outputBlocks)}:{}),requestId:options.requestId,...(typeof response.id==='string'&&response.id?{responseId:response.id}:{}),retrievedAt:options.retrievedAt??new Date().toISOString(),warnings:options.capability==='command-capable'?['Alpha command behavior is verified, but trusted native backend provenance is unavailable.']:[],refRecords:a.refRecords} }
105
107
  export function renderAlphaSearchResult(value){ const parts=[value.content]; if(value.sources?.length) parts.push(`来源:\n${value.sources.map((s)=>`- [${s.title??s.url}](${s.url})`).join('\n')}`); if(value.warnings?.length) parts.push(value.warnings.map((w)=>`警告:${w}`).join('\n')); parts.push(`检索时间:${value.retrievedAt}`); return [{type:'text',text:parts.filter(Boolean).join('\n\n')}] }
106
108
  function probeFailureState(error){ if([404,405].includes(error?.status)||['LCX_ALPHA_ACTION_UNAVAILABLE','LCX_ALPHA_ACTION_FAILED'].includes(error?.code)||/channel does not support|unsupported action|not implemented/iu.test(String(error?.message??''))) return 'unsupported'; return 'unknown' }
107
109
  function alphaProbeStructuredResult(value,seen=new Set()){
@@ -122,6 +124,7 @@ function alphaProbeStructuredBlock(block){
122
124
  }
123
125
  const ALPHA_PROBE_SEMANTIC_FAILURE=/\breference(?: id)?\s+(?:is\s+)?(?:invalid|unavailable)\b/iu
124
126
  function alphaProbeResultHasEvidence(action,value){
127
+ if(alphaCommandErrorEnvelope(value?.content,action)) return false
125
128
  if(ALPHA_PROBE_SEMANTIC_FAILURE.test(String(value?.content??''))&&!(Array.isArray(value?.results)&&value.results.length)) return false
126
129
  const refs=Array.isArray(value?.refs)&&value.refs.some((ref)=>typeof ref==='string'&&ref&&!isAlphaHttpUrl(ref))
127
130
  const links=Array.isArray(value?.links)&&value.links.some((link)=>Number.isSafeInteger(link?.id))
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "dsh-lcx-codex",
3
- "version": "0.4.1",
4
- "description": "DSH Codex/Responses capabilities for existing openai-responses routes: hosted/alpha search and session-native V2 compaction",
3
+ "version": "0.4.2",
4
+ "description": "DSH GPT Responses lifecycle owner with Native V2 compaction/replay and Hosted/Alpha Search capabilities",
5
5
  "keywords": [
6
6
  "deepseek-harness",
7
7
  "dsh",
@@ -53,7 +53,7 @@
53
53
  }
54
54
  },
55
55
  "dependencies": {
56
- "@earendil-works/pi-ai": "0.82.1"
56
+ "@earendil-works/pi-ai": "0.84.3"
57
57
  },
58
58
  "devDependencies": {
59
59
  "@deepseek-ai/dsh-llm": "0.1.1-rc.2",
@@ -84,7 +84,7 @@
84
84
  "test:schema": "node scripts/validate-dsh-schema.mjs"
85
85
  },
86
86
  "engines": {
87
- "node": ">=20"
87
+ "node": "^22.19.0 || >=24.0.0"
88
88
  },
89
89
  "publishConfig": {
90
90
  "access": "public"