dsh-lcx-codex 0.3.4 → 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.
@@ -3,381 +3,31 @@ import { outputDomains, outputLineRange, parseWebRunOutput } from './web-run-out
3
3
  export const HOSTED_SEARCH_PARAMETERS = {
4
4
  type: 'object',
5
5
  properties: {
6
- query: {
7
- type: 'string',
8
- description: 'Search the web for the requested topic. Include date or freshness requirements in the query when needed.',
9
- },
6
+ query: { type: 'string', description: 'Advanced Responses Hosted Web Search query. Use DSH web_search for ordinary searches.' },
10
7
  searchContextSize: { type: 'string', enum: ['low', 'medium', 'high'] },
11
8
  allowedDomains: { type: 'array', items: { type: 'string' } },
12
9
  blockedDomains: { type: 'array', items: { type: 'string' } },
13
- userLocation: {
14
- type: 'object',
15
- properties: {
16
- country: { type: 'string' },
17
- city: { type: 'string' },
18
- region: { type: 'string' },
19
- timezone: { type: 'string' },
20
- },
21
- additionalProperties: false,
22
- },
10
+ userLocation: { type: 'object', properties: { country: { type: 'string' }, city: { type: 'string' }, region: { type: 'string' }, timezone: { type: 'string' } }, additionalProperties: false },
23
11
  externalWebAccess: { type: 'boolean' },
24
12
  returnTokenBudget: { type: 'string', enum: ['default', 'unlimited'] },
25
- searchContentTypes: {
26
- type: 'array',
27
- items: { type: 'string', enum: ['text', 'image'] },
28
- },
29
- imageSettings: {
30
- type: 'object',
31
- properties: {
32
- maxResults: { type: 'integer' },
33
- caption: { type: 'boolean' },
34
- },
35
- additionalProperties: false,
36
- },
13
+ searchContentTypes: { type: 'array', items: { type: 'string', enum: ['text', 'image'] } },
14
+ imageSettings: { type: 'object', properties: { maxResults: { type: 'integer' }, caption: { type: 'boolean' } }, additionalProperties: false },
37
15
  },
38
- required: ['query'],
39
- additionalProperties: false,
40
- }
41
-
42
- export const HOSTED_SEARCH_OUTPUT = {
43
- type: 'object',
44
- properties: {
45
- mode: { type: 'string', enum: ['hosted'] },
46
- action: { type: 'string' },
47
- emulation: { type: 'string', enum: ['native'] },
48
- content: { type: 'string' },
49
- sources: { type: 'array', items: { type: 'object' } },
50
- citations: { type: 'array', items: { type: 'object' } },
51
- images: { type: 'array', items: { type: 'object' } },
52
- warnings: { type: 'array', items: { type: 'string' } },
53
- outputBlocks: { type: 'array', items: { type: 'object' } },
54
- domains: { type: 'array', items: { type: 'string' } },
55
- lineRange: { type: 'object' },
56
- requestId: { type: 'string' },
57
- responseId: { type: 'string' },
58
- retrievedAt: { type: 'string' },
59
- truncated: { type: 'boolean' },
60
- },
61
- required: ['mode', 'action', 'emulation', 'content', 'sources', 'citations', 'images', 'warnings', 'requestId', 'retrievedAt', 'truncated'],
62
- additionalProperties: false,
63
- }
64
-
65
- function failure(message, code) {
66
- const error = new Error(message)
67
- error.code = code
68
- return error
69
- }
70
-
71
- export function normalizeHostedSearchArgs(args) {
72
- if (args === null || typeof args !== 'object' || Array.isArray(args)) throw failure('websearch_gpt arguments must be an object', 'WEB_INVALID_REQUEST')
73
- if (typeof args.query !== 'string' || args.query.trim().length === 0) throw failure('websearch_gpt.query must be a non-empty string', 'WEB_INVALID_REQUEST')
74
- const known = new Set([
75
- 'query',
76
- 'searchContextSize',
77
- 'allowedDomains',
78
- 'blockedDomains',
79
- 'userLocation',
80
- 'externalWebAccess',
81
- 'returnTokenBudget',
82
- 'searchContentTypes',
83
- 'imageSettings',
84
- ])
85
- if (Object.keys(args).some((key) => !known.has(key))) throw failure('websearch_gpt received an unknown field', 'WEB_INVALID_REQUEST')
86
- const normalized = { query: args.query.trim() }
87
- if (normalized.query.length > 16_000) throw failure('websearch_gpt.query is too long', 'WEB_INVALID_REQUEST')
88
- if (args.searchContextSize !== undefined) {
89
- if (!['low', 'medium', 'high'].includes(args.searchContextSize)) throw failure('websearch_gpt.searchContextSize is invalid', 'WEB_INVALID_REQUEST')
90
- normalized.searchContextSize = args.searchContextSize
91
- }
92
- const allowedDomains = normalizeDomains(args.allowedDomains, 'allowedDomains')
93
- const blockedDomains = normalizeDomains(args.blockedDomains, 'blockedDomains')
94
- if (allowedDomains) normalized.allowedDomains = allowedDomains
95
- if (blockedDomains) normalized.blockedDomains = blockedDomains
96
- if (allowedDomains && blockedDomains) {
97
- const blocked = new Set(blockedDomains)
98
- if (allowedDomains.some((domain) => blocked.has(domain))) throw failure('websearch_gpt domain filters conflict', 'WEB_INVALID_REQUEST')
99
- }
100
- if (args.userLocation !== undefined) normalized.userLocation = normalizeUserLocation(args.userLocation)
101
- if (args.externalWebAccess !== undefined) {
102
- if (typeof args.externalWebAccess !== 'boolean') throw failure('websearch_gpt.externalWebAccess must be boolean', 'WEB_INVALID_REQUEST')
103
- normalized.externalWebAccess = args.externalWebAccess
104
- }
105
- if (args.returnTokenBudget !== undefined) {
106
- if (!['default', 'unlimited'].includes(args.returnTokenBudget)) throw failure('websearch_gpt.returnTokenBudget is invalid', 'WEB_INVALID_REQUEST')
107
- normalized.returnTokenBudget = args.returnTokenBudget
108
- }
109
- if (args.searchContentTypes !== undefined) normalized.searchContentTypes = normalizeContentTypes(args.searchContentTypes)
110
- if (args.imageSettings !== undefined) {
111
- if (!normalized.searchContentTypes?.includes('image')) throw failure('websearch_gpt.imageSettings requires image search content', 'WEB_INVALID_REQUEST')
112
- normalized.imageSettings = normalizeImageSettings(args.imageSettings)
113
- }
114
- return normalized
115
- }
116
-
117
- function normalizeDomains(value, field) {
118
- if (value === undefined) return undefined
119
- if (!Array.isArray(value) || value.length === 0 || value.length > 100) throw failure(`websearch_gpt.${field} must contain 1 to 100 domains`, 'WEB_INVALID_REQUEST')
120
- const domains = value.map((item) => {
121
- if (typeof item !== 'string') throw failure(`websearch_gpt.${field} contains an invalid domain`, 'WEB_INVALID_REQUEST')
122
- const domain = item.trim().toLowerCase()
123
- if (domain.length === 0 || domain.length > 253 || domain.includes('/') || domain.includes(':') || domain.endsWith('.')) {
124
- throw failure(`websearch_gpt.${field} contains an invalid domain`, 'WEB_INVALID_REQUEST')
125
- }
126
- const labels = domain.split('.')
127
- if (labels.length < 2 || labels.some((label) => !/^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/u.test(label))) {
128
- throw failure(`websearch_gpt.${field} contains an invalid domain`, 'WEB_INVALID_REQUEST')
129
- }
130
- return domain
131
- })
132
- if (new Set(domains).size !== domains.length) throw failure(`websearch_gpt.${field} contains duplicate domains`, 'WEB_INVALID_REQUEST')
133
- return domains
134
- }
135
-
136
- function normalizeUserLocation(value) {
137
- if (!isRecord(value)) throw failure('websearch_gpt.userLocation must be an object', 'WEB_INVALID_REQUEST')
138
- const known = new Set(['country', 'city', 'region', 'timezone'])
139
- if (Object.keys(value).some((key) => !known.has(key))) throw failure('websearch_gpt.userLocation contains an unknown field', 'WEB_INVALID_REQUEST')
140
- const result = {}
141
- if (value.country !== undefined) {
142
- if (typeof value.country !== 'string' || !/^[a-z]{2}$/iu.test(value.country.trim())) throw failure('websearch_gpt.userLocation.country must be an ISO alpha-2 code', 'WEB_INVALID_REQUEST')
143
- result.country = value.country.trim().toUpperCase()
144
- }
145
- for (const field of ['city', 'region']) {
146
- if (value[field] === undefined) continue
147
- if (typeof value[field] !== 'string' || value[field].trim().length === 0 || value[field].trim().length > 200) throw failure(`websearch_gpt.userLocation.${field} is invalid`, 'WEB_INVALID_REQUEST')
148
- result[field] = value[field].trim()
149
- }
150
- if (value.timezone !== undefined) {
151
- if (typeof value.timezone !== 'string' || value.timezone.trim().length === 0) throw failure('websearch_gpt.userLocation.timezone is invalid', 'WEB_INVALID_REQUEST')
152
- const timezone = value.timezone.trim()
153
- try {
154
- new Intl.DateTimeFormat('en-US', { timeZone: timezone }).format()
155
- } catch {
156
- throw failure('websearch_gpt.userLocation.timezone must be an IANA timezone', 'WEB_INVALID_REQUEST')
157
- }
158
- result.timezone = timezone
159
- }
160
- if (Object.keys(result).length === 0) throw failure('websearch_gpt.userLocation must contain a location field', 'WEB_INVALID_REQUEST')
161
- return result
162
- }
163
-
164
- function normalizeContentTypes(value) {
165
- if (!Array.isArray(value) || value.length === 0 || value.length > 2 || value.some((item) => !['text', 'image'].includes(item))) {
166
- throw failure('websearch_gpt.searchContentTypes is invalid', 'WEB_INVALID_REQUEST')
167
- }
168
- if (new Set(value).size !== value.length) throw failure('websearch_gpt.searchContentTypes contains duplicates', 'WEB_INVALID_REQUEST')
169
- return [...value]
170
- }
171
-
172
- function normalizeImageSettings(value) {
173
- if (!isRecord(value) || Object.keys(value).some((key) => !['maxResults', 'caption'].includes(key))) throw failure('websearch_gpt.imageSettings is invalid', 'WEB_INVALID_REQUEST')
174
- const result = {}
175
- if (value.maxResults !== undefined) {
176
- if (!Number.isInteger(value.maxResults) || value.maxResults < 1 || value.maxResults > 100) throw failure('websearch_gpt.imageSettings.maxResults is invalid', 'WEB_INVALID_REQUEST')
177
- result.maxResults = value.maxResults
178
- }
179
- if (value.caption !== undefined) {
180
- if (typeof value.caption !== 'boolean') throw failure('websearch_gpt.imageSettings.caption must be boolean', 'WEB_INVALID_REQUEST')
181
- result.caption = value.caption
182
- }
183
- if (Object.keys(result).length === 0) throw failure('websearch_gpt.imageSettings must contain a setting', 'WEB_INVALID_REQUEST')
184
- return result
185
- }
186
-
187
- export function buildHostedSearchBody(args, model) {
188
- const tool = {
189
- type: 'web_search',
190
- ...(args.searchContextSize ? { search_context_size: args.searchContextSize } : {}),
191
- ...(args.allowedDomains || args.blockedDomains ? {
192
- filters: {
193
- ...(args.allowedDomains ? { allowed_domains: args.allowedDomains } : {}),
194
- ...(args.blockedDomains ? { blocked_domains: args.blockedDomains } : {}),
195
- },
196
- } : {}),
197
- ...(args.userLocation ? { user_location: { type: 'approximate', ...args.userLocation } } : {}),
198
- ...(args.externalWebAccess !== undefined ? { external_web_access: args.externalWebAccess } : {}),
199
- ...(args.returnTokenBudget ? { return_token_budget: args.returnTokenBudget } : {}),
200
- ...(args.searchContentTypes ? { search_content_types: args.searchContentTypes } : {}),
201
- ...(args.imageSettings ? {
202
- image_settings: {
203
- ...(args.imageSettings.maxResults !== undefined ? { max_results: args.imageSettings.maxResults } : {}),
204
- ...(args.imageSettings.caption !== undefined ? { caption: args.imageSettings.caption } : {}),
205
- },
206
- } : {}),
207
- }
208
- return {
209
- model,
210
- input: [{ role: 'user', content: [{ type: 'input_text', text: args.query }] }],
211
- tools: [tool],
212
- tool_choice: 'required',
213
- include: [
214
- 'web_search_call.action.sources',
215
- ...(args.searchContentTypes?.includes('image') ? ['web_search_call.results'] : []),
216
- ],
217
- stream: false,
218
- store: false,
219
- }
220
- }
221
-
222
- function isRecord(value) {
223
- return value !== null && typeof value === 'object' && !Array.isArray(value)
224
- }
225
-
226
- function textFrom(value, seen = new Set()) {
227
- if (typeof value === 'string') return value
228
- if (!value || typeof value !== 'object' || seen.has(value)) return ''
229
- seen.add(value)
230
- if (Array.isArray(value)) return value.map((item) => textFrom(item, seen)).filter(Boolean).join('\n')
231
- if (typeof value.text === 'string' && ['output_text', 'text', 'input_text'].includes(value.type)) return value.text
232
- if (Array.isArray(value.content)) return textFrom(value.content, seen)
233
- return ''
234
- }
235
-
236
- function sourceFrom(value) {
237
- if (!isRecord(value) || typeof value.url !== 'string' || value.url.trim().length === 0) return undefined
238
- return {
239
- url: value.url,
240
- ...(typeof value.title === 'string' && value.title ? { title: value.title } : {}),
241
- ...(typeof value.snippet === 'string' && value.snippet ? { snippet: value.snippet } : {}),
242
- ...(typeof value.publishedAt === 'string' && value.publishedAt ? { publishedAt: value.publishedAt } : {}),
243
- ...(typeof value.published_at === 'string' && value.published_at ? { publishedAt: value.published_at } : {}),
244
- ...(typeof value.ref_id === 'string' && value.ref_id ? { refId: value.ref_id } : {}),
245
- ...(typeof value.domain === 'string' && value.domain ? { domain: value.domain } : {}),
246
- }
247
- }
248
-
249
- function responseArtifacts(response) {
250
- const sources = []
251
- const citations = []
252
- const images = []
253
- const actions = []
254
- for (const item of Array.isArray(response?.output) ? response.output : []) {
255
- if (item?.type === 'web_search_call') {
256
- actions.push(typeof item.action?.type === 'string' ? item.action.type : 'search')
257
- if (Array.isArray(item.action?.sources)) {
258
- for (const value of item.action.sources) {
259
- const source = sourceFrom(value)
260
- if (source) sources.push(source)
261
- }
262
- }
263
- for (const value of Array.isArray(item.results) ? item.results : []) {
264
- const image = imageFrom(value)
265
- if (image) images.push(image)
266
- }
267
- }
268
- if (item?.type !== 'message') continue
269
- for (const part of Array.isArray(item.content) ? item.content : []) {
270
- if (part?.type !== 'output_text') continue
271
- for (const annotation of Array.isArray(part.annotations) ? part.annotations : []) {
272
- if (annotation?.type !== 'url_citation') continue
273
- const source = sourceFrom(annotation)
274
- if (source) {
275
- citations.push(source)
276
- sources.push(source)
277
- }
278
- }
279
- }
280
- }
281
- return { sources, citations, images, actions }
282
- }
283
-
284
- function httpUrl(value) {
285
- try {
286
- const url = new URL(value)
287
- if (!['http:', 'https:'].includes(url.protocol)) return undefined
288
- return url
289
- } catch {
290
- return undefined
291
- }
292
- }
293
-
294
- function imageFrom(value) {
295
- if (!isRecord(value) || value.type !== 'image_result') return undefined
296
- const imageUrl = httpUrl(value.image_url)?.toString()
297
- if (!imageUrl) return undefined
298
- const thumbnailUrl = httpUrl(value.thumbnail_url)?.toString()
299
- const sourceWebsiteUrl = httpUrl(value.source_website_url)?.toString()
300
- return {
301
- imageUrl,
302
- ...(thumbnailUrl ? { thumbnailUrl } : {}),
303
- ...(sourceWebsiteUrl ? { sourceWebsiteUrl } : {}),
304
- ...(typeof value.caption === 'string' && value.caption ? { caption: value.caption } : {}),
305
- }
306
- }
307
-
308
- function canonicalUrl(value) {
309
- const url = httpUrl(value)
310
- if (url) {
311
- url.hash = ''
312
- for (const key of [...url.searchParams.keys()]) if (/^(utm_|gclid$|fbclid$)/iu.test(key)) url.searchParams.delete(key)
313
- return url.toString()
314
- }
315
- return undefined
316
- }
317
-
318
- function uniqueByUrl(values) {
319
- const result = []
320
- const seen = new Set()
321
- for (const value of values) {
322
- const key = canonicalUrl(value.url)
323
- if (!key || seen.has(key)) continue
324
- seen.add(key)
325
- result.push(value)
326
- }
327
- return result
328
- }
329
-
330
- export function parseHostedSearchResponse(response, requestId, maxResults = 8, retrievedAt = new Date().toISOString()) {
331
- if (response?.error) {
332
- const error = failure(response.error.message ?? 'LCX hosted Web Search failed', 'LCX_WEB_PROVIDER_ERROR')
333
- throw error
334
- }
335
- if (response?.status !== 'completed') {
336
- throw failure(`LCX hosted Web Search returned response status: ${String(response?.status ?? 'missing')}`, 'WEB_RESPONSE_INCOMPLETE')
337
- }
338
- const output = typeof response?.output_text === 'string' ? response.output_text : textFrom(response?.output ?? response?.content)
339
- const outputBlocks = parseWebRunOutput(output)
340
- const artifacts = responseArtifacts(response)
341
- if (artifacts.actions.length === 0) throw failure('LCX hosted Web Search completed without executing web_search', 'WEB_SEARCH_NOT_EXECUTED')
342
- const allSources = [...artifacts.sources]
343
- for (const block of outputBlocks) {
344
- if (block.url) allSources.push({ url: block.url, ...(block.title ? { title: block.title } : {}) })
345
- }
346
- const sources = uniqueByUrl(allSources)
347
- const citations = uniqueByUrl(artifacts.citations)
348
- const limited = sources.slice(0, Math.max(1, maxResults))
349
- const images = artifacts.images.slice(0, Math.max(1, maxResults))
350
- if (!output && limited.length === 0 && images.length === 0) throw failure('LCX hosted Web Search returned no text, sources, or images', 'WEB_NO_SOURCES')
351
- return {
352
- mode: 'hosted',
353
- action: artifacts.actions[0],
354
- emulation: 'native',
355
- content: output,
356
- sources: limited,
357
- citations,
358
- images,
359
- warnings: artifacts.actions.length > 1 ? [`Multiple hosted search actions were returned: ${artifacts.actions.join(', ')}`] : [],
360
- outputBlocks,
361
- domains: outputDomains(outputBlocks),
362
- ...(outputLineRange(outputBlocks) ? { lineRange: outputLineRange(outputBlocks) } : {}),
363
- requestId,
364
- ...(typeof response?.id === 'string' && response.id ? { responseId: response.id } : {}),
365
- retrievedAt,
366
- truncated: sources.length > limited.length || artifacts.images.length > images.length,
367
- }
368
- }
369
-
370
- export function renderHostedSearchResult(value) {
371
- const parts = []
372
- if (value.content) parts.push(value.content)
373
- if (value.sources?.length) {
374
- parts.push(`来源:\n${value.sources.map((source) => `- [${source.title ?? source.url}](${source.url})${source.publishedAt ? `(${source.publishedAt})` : ''}${source.snippet ? ` — ${source.snippet}` : ''}`).join('\n')}`)
375
- }
376
- if (value.images?.length) {
377
- parts.push(`图片:\n${value.images.map((image) => `- [${image.caption ?? image.imageUrl}](${image.imageUrl})${image.sourceWebsiteUrl ? `([来源页面](${image.sourceWebsiteUrl}))` : ''}`).join('\n')}`)
378
- }
379
- if (value.warnings?.length) parts.push(value.warnings.map((warning) => `警告:${warning}`).join('\n'))
380
- if (value.retrievedAt) parts.push(`检索时间:${value.retrievedAt}`)
381
- if (value.truncated) parts.push('来源已截断;如需更多结果,请缩小查询范围。')
382
- return [{ type: 'text', text: parts.join('\n\n') || '没有找到结果。' }]
383
- }
16
+ required: ['query'], additionalProperties: false,
17
+ }
18
+ export const HOSTED_SEARCH_OUTPUT = { type: 'object', properties: { mode: { type: 'string', enum: ['hosted'] }, action: { type: 'string' }, emulation: { type: 'string', enum: ['native'] }, content: { type: 'string' }, sources: { type: 'array', items: { type: 'object' } }, citations: { type: 'array', items: { type: 'object' } }, images: { type: 'array', items: { type: 'object' } }, warnings: { type: 'array', items: { type: 'string' } }, outputBlocks: { type: 'array', items: { type: 'object' } }, domains: { type: 'array', items: { type: 'string' } }, lineRange: { type: 'object' }, requestId: { type: 'string' }, responseId: { type: 'string' }, retrievedAt: { type: 'string' }, truncated: { type: 'boolean' } }, required: ['mode','action','emulation','content','sources','citations','images','warnings','requestId','retrievedAt','truncated'], additionalProperties: false }
19
+ function failure(message, code = 'WEB_INVALID_REQUEST') { const e = new Error(message); e.code = code; return e }
20
+ function isRecord(value) { return value !== null && typeof value === 'object' && !Array.isArray(value) }
21
+ function normalizeDomains(value, field) { if (value === undefined) return undefined; if (!Array.isArray(value) || value.length < 1 || value.length > 100) throw failure(`websearch_gpt_advanced.${field} must contain 1 to 100 domains`); const result = value.map((item) => { if (typeof item !== 'string') throw failure(`websearch_gpt_advanced.${field} contains an invalid domain`); const domain = item.trim().toLowerCase(); if (!domain || domain.length > 253 || domain.includes('/') || domain.includes(':') || domain.endsWith('.') || domain.split('.').length < 2) throw failure(`websearch_gpt_advanced.${field} contains an invalid domain`); return domain }); if (new Set(result).size !== result.length) throw failure(`websearch_gpt_advanced.${field} contains duplicates`); return result }
22
+ function normalizeLocation(value) { if (!isRecord(value)) throw failure('websearch_gpt_advanced.userLocation must be an object'); const result = {}; if (value.country !== undefined) { if (typeof value.country !== 'string' || !/^[a-z]{2}$/iu.test(value.country.trim())) throw failure('userLocation.country must be ISO alpha-2'); result.country = value.country.trim().toUpperCase() } for (const field of ['city','region']) if (value[field] !== undefined) { if (typeof value[field] !== 'string' || !value[field].trim() || value[field].length > 200) throw failure(`userLocation.${field} is invalid`); result[field] = value[field].trim() } if (value.timezone !== undefined) { try { new Intl.DateTimeFormat('en-US', { timeZone: value.timezone }).format() } catch { throw failure('userLocation.timezone must be an IANA timezone') }; result.timezone = value.timezone } if (!Object.keys(result).length) throw failure('userLocation is empty'); return result }
23
+ export function normalizeHostedSearchArgs(args) { if (!isRecord(args) || typeof args.query !== 'string' || !args.query.trim()) throw failure('websearch_gpt_advanced.query must be non-empty'); const query = args.query.trim(); if (query.length > 16_000) throw failure('query is too long'); const result = { query }; if (args.searchContextSize !== undefined) { if (!['low','medium','high'].includes(args.searchContextSize)) throw failure('searchContextSize is invalid'); result.searchContextSize = args.searchContextSize } const allowedDomains = normalizeDomains(args.allowedDomains, 'allowedDomains'); const blockedDomains = normalizeDomains(args.blockedDomains, 'blockedDomains'); if (allowedDomains) result.allowedDomains = allowedDomains; if (blockedDomains) result.blockedDomains = blockedDomains; if (allowedDomains && blockedDomains && allowedDomains.some((d) => new Set(blockedDomains).has(d))) throw failure('domain filters conflict'); if (args.userLocation !== undefined) result.userLocation = normalizeLocation(args.userLocation); if (args.externalWebAccess !== undefined) { if (typeof args.externalWebAccess !== 'boolean') throw failure('externalWebAccess must be boolean'); result.externalWebAccess = args.externalWebAccess } if (args.returnTokenBudget !== undefined) { if (!['default','unlimited'].includes(args.returnTokenBudget)) throw failure('returnTokenBudget is invalid'); result.returnTokenBudget = args.returnTokenBudget } if (args.searchContentTypes !== undefined) { if (!Array.isArray(args.searchContentTypes) || !args.searchContentTypes.length || args.searchContentTypes.length > 2 || args.searchContentTypes.some((v) => !['text','image'].includes(v))) throw failure('searchContentTypes is invalid'); result.searchContentTypes = [...new Set(args.searchContentTypes)] } if (args.imageSettings !== undefined) { if (!result.searchContentTypes?.includes('image') || !isRecord(args.imageSettings)) throw failure('imageSettings requires image search'); result.imageSettings = {}; if (args.imageSettings.maxResults !== undefined) { if (!Number.isInteger(args.imageSettings.maxResults) || args.imageSettings.maxResults < 1 || args.imageSettings.maxResults > 100) throw failure('imageSettings.maxResults is invalid'); result.imageSettings.maxResults = args.imageSettings.maxResults } if (args.imageSettings.caption !== undefined) { if (typeof args.imageSettings.caption !== 'boolean') throw failure('imageSettings.caption must be boolean'); result.imageSettings.caption = args.imageSettings.caption } } return result }
24
+ export function buildHostedSearchBody(args, model, options = {}) { const tool = { type: 'web_search', ...(args.searchContextSize ? { search_context_size: args.searchContextSize } : {}), ...(args.allowedDomains || args.blockedDomains ? { filters: { ...(args.allowedDomains ? { allowed_domains: args.allowedDomains } : {}), ...(args.blockedDomains ? { blocked_domains: args.blockedDomains } : {}) } } : {}), ...(args.userLocation ? { user_location: { type: 'approximate', ...args.userLocation } } : {}), ...(args.externalWebAccess !== undefined ? { external_web_access: args.externalWebAccess } : {}), ...(args.returnTokenBudget ? { return_token_budget: args.returnTokenBudget } : {}), ...(args.searchContentTypes ? { search_content_types: args.searchContentTypes } : {}), ...(args.imageSettings ? { image_settings: { ...(args.imageSettings.maxResults !== undefined ? { max_results: args.imageSettings.maxResults } : {}), ...(args.imageSettings.caption !== undefined ? { caption: args.imageSettings.caption } : {}) } } : {}) }; return { model, input: [{ role: 'user', content: [{ type: 'input_text', text: args.query }] }], tools: [tool], tool_choice: 'required', include: ['web_search_call.action.sources', ...(args.searchContentTypes?.includes('image') ? ['web_search_call.results'] : [])], stream: false, store: false, ...(options.promptCacheKey ? { prompt_cache_key: options.promptCacheKey } : {}) } }
25
+ function textFrom(value, seen = new Set()) { if (typeof value === 'string') return value; if (!value || typeof value !== 'object' || seen.has(value)) return ''; seen.add(value); if (Array.isArray(value)) return value.map((item) => textFrom(item, seen)).filter(Boolean).join('\n'); if (typeof value.text === 'string' && ['output_text','text','input_text'].includes(value.type)) return value.text; if (Array.isArray(value.content)) return textFrom(value.content, seen); return '' }
26
+ function httpUrl(value) { try { const url = new URL(value); return ['http:','https:'].includes(url.protocol) ? url : undefined } catch { return undefined } }
27
+ function sourceFrom(value) { if (!isRecord(value) || !httpUrl(value.url)) return undefined; return { url: httpUrl(value.url).toString(), ...(typeof value.title === 'string' && value.title ? { title: value.title } : {}), ...(typeof value.snippet === 'string' && value.snippet ? { snippet: value.snippet } : {}), ...(typeof value.publishedAt === 'string' ? { publishedAt: value.publishedAt } : typeof value.published_at === 'string' ? { publishedAt: value.published_at } : {}), ...(typeof value.ref_id === 'string' ? { refId: value.ref_id } : {}) } }
28
+ function imageFrom(value) { if (!isRecord(value) || value.type !== 'image_result' || !httpUrl(value.image_url)) return undefined; return { imageUrl: httpUrl(value.image_url).toString(), ...(httpUrl(value.thumbnail_url) ? { thumbnailUrl: httpUrl(value.thumbnail_url).toString() } : {}), ...(httpUrl(value.source_website_url) ? { sourceWebsiteUrl: httpUrl(value.source_website_url).toString() } : {}), ...(typeof value.caption === 'string' ? { caption: value.caption } : {}) } }
29
+ function responseArtifacts(response) { const sources = [], citations = [], images = [], actions = []; for (const item of Array.isArray(response?.output) ? response.output : []) { if (item?.type === 'web_search_call') { actions.push(typeof item.action?.type === 'string' ? item.action.type : 'search'); for (const value of item.action?.sources ?? []) { const source = sourceFrom(value); if (source) sources.push(source) } for (const value of item.results ?? []) { const image = imageFrom(value); if (image) images.push(image) } } if (item?.type === 'message') for (const part of item.content ?? []) if (part?.type === 'output_text') for (const annotation of part.annotations ?? []) if (annotation?.type === 'url_citation') { const source = sourceFrom(annotation); if (source) { citations.push(source); sources.push(source) } } } return { sources, citations, images, actions } }
30
+ function canonicalUrl(value) { const url = httpUrl(value); if (!url) return undefined; url.hash = ''; for (const key of [...url.searchParams.keys()]) if (/^(utm_|gclid$|fbclid$)/iu.test(key)) url.searchParams.delete(key); return url.toString() }
31
+ function uniqueByUrl(values) { const result = [], seen = new Set(); for (const value of values) { const key = canonicalUrl(value.url); if (!key || seen.has(key)) continue; seen.add(key); result.push(value) } return result }
32
+ export function parseHostedSearchResponse(response, requestId, maxResults = 8, retrievedAt = new Date().toISOString()) { if (response?.error) throw failure(response.error.message ?? 'Hosted Web Search failed', 'LCX_WEB_PROVIDER_ERROR'); if (response?.status !== 'completed') throw failure(`Hosted Web Search status: ${String(response?.status ?? 'missing')}`, 'WEB_RESPONSE_INCOMPLETE'); const output = typeof response.output_text === 'string' ? response.output_text : textFrom(response?.output ?? response?.content); const outputBlocks = parseWebRunOutput(output); const artifacts = responseArtifacts(response); if (!artifacts.actions.length) throw failure('Hosted Web Search completed without web_search', 'WEB_SEARCH_NOT_EXECUTED'); const allSources = [...artifacts.sources, ...outputBlocks.flatMap((block) => block.url ? [{ url: block.url, ...(block.title ? { title: block.title } : {}) }] : [])]; const sources = uniqueByUrl(allSources); const citations = uniqueByUrl(artifacts.citations); const limited = sources.slice(0, Math.max(1, maxResults)); const images = artifacts.images.slice(0, Math.max(1, maxResults)); if (!output && !limited.length && !images.length) throw failure('Hosted Web Search returned no output', 'WEB_NO_SOURCES'); return { mode: 'hosted', action: artifacts.actions[0], emulation: 'native', content: output, sources: limited, citations, images, warnings: artifacts.actions.length > 1 ? [`Multiple hosted search actions were returned: ${artifacts.actions.join(', ')}`] : [], outputBlocks, domains: outputDomains(outputBlocks), ...(outputLineRange(outputBlocks) ? { lineRange: outputLineRange(outputBlocks) } : {}), requestId, ...(typeof response.id === 'string' ? { responseId: response.id } : {}), retrievedAt, truncated: sources.length > limited.length || artifacts.images.length > images.length } }
33
+ export function renderHostedSearchResult(value) { const parts = []; if (value.content) parts.push(value.content); if (value.sources?.length) parts.push(`来源:\n${value.sources.map((s) => `- [${s.title ?? s.url}](${s.url})${s.snippet ? ` — ${s.snippet}` : ''}`).join('\n')}`); if (value.images?.length) parts.push(`图片:\n${value.images.map((i) => `- [${i.caption ?? i.imageUrl}](${i.imageUrl})`).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') }] }
@@ -1,4 +1,4 @@
1
- import { AtomicWebSearchStore } from './web-search-store.js'
1
+ import { JsonStore } from './json-store.js'
2
2
 
3
3
  const VERSION = 1
4
4
 
@@ -31,12 +31,12 @@ function unavailable(refId) {
31
31
 
32
32
  export class AlphaRefStore {
33
33
  constructor(file) {
34
- this.store = new AtomicWebSearchStore(file, {
35
- empty: () => ({ version: VERSION, sessions: {} }),
36
- validate: validData,
37
- corruptCode: 'LCX_ALPHA_REF_STORE_CORRUPT',
38
- writeCode: 'LCX_ALPHA_REF_STORE_WRITE_FAILED',
39
- })
34
+ this.store = new JsonStore(
35
+ file,
36
+ () => ({ version: VERSION, sessions: {} }),
37
+ validData,
38
+ 'LCX_ALPHA_REF_STORE_CORRUPT',
39
+ )
40
40
  }
41
41
 
42
42
  record(sessionId, routeFingerprint, refs) {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "dsh-lcx-codex",
3
- "version": "0.3.4",
4
- "description": "DSH web search and Responses native V2 compaction for Sub2API-proxied or NewAPI-relayed GPT models",
3
+ "version": "0.4.0-rc.13",
4
+ "description": "DSH Codex/Responses capabilities for existing openai-responses routes: hosted/alpha search and session-native V2 compaction",
5
5
  "keywords": [
6
6
  "deepseek-harness",
7
7
  "dsh",
@@ -35,11 +35,13 @@
35
35
  },
36
36
  "files": [
37
37
  "lib/*.js",
38
- "scripts/probe-alpha.mjs",
38
+ "scripts/*.mjs",
39
+ "assets/*.jpg",
39
40
  "cordis.patch.yml",
40
41
  "LICENSE",
41
42
  "README.md",
42
43
  "README_EN.md",
44
+ "ARCHITECTURE.md",
43
45
  "CHANGELOG.md"
44
46
  ],
45
47
  "dsh": {
@@ -54,26 +56,30 @@
54
56
  "@earendil-works/pi-ai": "0.82.1"
55
57
  },
56
58
  "devDependencies": {
57
- "@deepseek-ai/dsh-settings": "0.1.0-rc.8",
59
+ "@deepseek-ai/dsh-llm": "0.1.1-rc.2",
60
+ "@deepseek-ai/dsh-settings": "0.1.1-rc.2",
61
+ "@deepseek-ai/dsh-tools": "0.1.1-rc.2",
58
62
  "@deepseek-ai/schemastery": "^3.18.1"
59
63
  },
60
64
  "peerDependencies": {
61
65
  "@deepseek-ai/cordis": "^4.0.1",
62
- "@deepseek-ai/dsh-client-runtime": "^0.1.0-rc.8",
63
- "@deepseek-ai/dsh-client-ui-settings": "^0.1.0-rc.8",
64
- "@deepseek-ai/dsh-client-ui-slots": "^0.1.0-rc.8",
65
- "@deepseek-ai/dsh-credentials": "^0.1.0-rc.8",
66
- "@deepseek-ai/dsh-llm": "^0.1.0-rc.8",
67
- "@deepseek-ai/dsh-settings": "^0.1.0-rc.8",
68
- "@deepseek-ai/dsh-tools": "^0.1.0-rc.8",
66
+ "@deepseek-ai/dsh-attachment": "^0.1.1-rc.2",
67
+ "@deepseek-ai/dsh-client-runtime": "^0.1.1-rc.2",
68
+ "@deepseek-ai/dsh-client-ui-settings": "^0.1.1-rc.2",
69
+ "@deepseek-ai/dsh-client-ui-slots": "^0.1.1-rc.2",
70
+ "@deepseek-ai/dsh-credentials": "^0.1.1-rc.2",
71
+ "@deepseek-ai/dsh-llm": "^0.1.1-rc.2",
72
+ "@deepseek-ai/dsh-session": "^0.1.1-rc.2",
73
+ "@deepseek-ai/dsh-settings": "^0.1.1-rc.2",
74
+ "@deepseek-ai/dsh-tools": "^0.1.1-rc.2",
69
75
  "@deepseek-ai/schemastery": "^3.18.1",
70
76
  "react": "^18.2.0"
71
77
  },
72
78
  "scripts": {
73
79
  "test": "node --test tests/*.test.mjs",
74
- "test:schema": "node scripts/validate-dsh-schema.mjs",
75
- "test:e2e": "node --test tests/sub2api.e2e.test.mjs",
76
- "probe:alpha": "node scripts/probe-alpha.mjs"
80
+ "probe:alpha": "node scripts/probe-alpha.mjs",
81
+ "pack:local": "npm pack --ignore-scripts",
82
+ "test:schema": "node scripts/validate-dsh-schema.mjs"
77
83
  },
78
84
  "engines": {
79
85
  "node": ">=20"
@@ -2,90 +2,42 @@ import { readFileSync } from 'node:fs'
2
2
  import { homedir } from 'node:os'
3
3
  import { join } from 'node:path'
4
4
  import { randomUUID } from 'node:crypto'
5
- import {
6
- ALPHA_SCHEMA_FINGERPRINT,
7
- buildAlphaSearchBody,
8
- normalizeAlphaSearchArgs,
9
- parseAlphaSearchResponse,
10
- probeAlphaCapabilities,
11
- } from '../lib/web-search-alpha.js'
5
+ import { ALPHA_SCHEMA_FINGERPRINT, buildAlphaSearchBody, normalizeAlphaSearchArgs, parseAlphaSearchResponse, probeAlphaCapabilities } from '../lib/web-search-alpha.js'
12
6
  import { AlphaCapabilityStore, alphaCapabilityFingerprint } from '../lib/web-search-capability.js'
13
- import { fetchJsonWithRetry } from '../lib/transport.js'
14
7
 
15
- function required(name, value) {
16
- const normalized = String(value ?? '').trim()
17
- if (!normalized) throw new Error(`${name} is required`)
18
- return normalized
19
- }
20
-
21
- function apiKey() {
22
- const file = String(process.env.LCX_API_KEY_FILE ?? '').trim()
23
- if (file) return required('LCX_API_KEY_FILE content', readFileSync(file, 'utf8'))
24
- return required('LCX_API_KEY or LCX_API_KEY_FILE', process.env.LCX_API_KEY)
25
- }
26
-
27
- const key = apiKey()
28
8
  const baseURL = String(process.env.LCX_BASE_URL ?? 'https://api.lcxbot.com/v1').replace(/\/+$/u, '')
29
- const model = required('LCX_MODEL', process.env.LCX_MODEL)
30
- const provider = String(process.env.LCX_PROVIDER ?? 'lcx')
9
+ const model = String(process.env.LCX_MODEL ?? '').trim()
10
+ const provider = String(process.env.LCX_PROVIDER ?? 'lcx').trim()
31
11
  const profile = String(process.env.LCX_ALPHA_PROFILE ?? '')
32
12
  const group = String(process.env.LCX_ALPHA_GROUP ?? '')
33
- const sessionId = `dsh-lcx-codex-alpha-probe-${Date.now()}`
34
- const timeoutMs = Number(process.env.LCX_E2E_TIMEOUT_MS ?? 120000)
35
- const capabilityPath = String(process.env.LCX_ALPHA_CAPABILITY_PATH ?? join(process.env.DSH_HOME ?? join(homedir(), '.dsh'), 'storages', 'lcx-codex', 'web-alpha-capabilities.json'))
36
- const trustedNativeProvenance = process.env.LCX_ALPHA_TRUSTED_NATIVE === '1'
37
- const clickProbeRef = String(process.env.LCX_ALPHA_CLICK_PROBE_REF ?? 'https://en.wikipedia.org/wiki/OpenAI').trim()
38
- const screenshotProbeRef = String(process.env.LCX_ALPHA_SCREENSHOT_PROBE_REF ?? 'https://arxiv.org/pdf/1706.03762').trim()
13
+ const keyFile = String(process.env.LCX_API_KEY_FILE ?? '').trim()
14
+ if (!model) throw new Error('Set LCX_MODEL to the exact GPT model id')
15
+ if (!keyFile) throw new Error('Set LCX_API_KEY_FILE to a local file containing the bearer key')
16
+ const apiKey = readFileSync(keyFile, 'utf8').trim()
17
+ if (!apiKey) throw new Error('LCX_API_KEY_FILE is empty')
18
+ const storeFile = process.env.LCX_ALPHA_CAPABILITY_PATH ?? join(process.env.DSH_HOME ?? join(homedir(), '.dsh'), 'storages', 'lcx-codex', 'web-alpha-capabilities.json')
19
+ const sessionId = `lcx-probe-${randomUUID()}`
39
20
 
40
- const invoke = async (input) => {
41
- const args = normalizeAlphaSearchArgs(input)
21
+ async function invoke(rawArgs) {
22
+ const args = normalizeAlphaSearchArgs(rawArgs)
42
23
  const requestId = randomUUID()
43
- const response = await fetchJsonWithRetry(
44
- `${baseURL}/alpha/search`,
45
- buildAlphaSearchBody(args, model, sessionId, true),
46
- {
47
- authorization: `Bearer ${key}`,
48
- 'content-type': 'application/json',
49
- 'x-client-request-id': requestId,
50
- 'session-id': sessionId,
51
- },
52
- AbortSignal.timeout(timeoutMs),
53
- timeoutMs,
54
- { maxAttempts: 1, maxResponseBytes: 4 * 1024 * 1024 },
55
- )
56
- return parseAlphaSearchResponse(response, { action: args.action, capability: 'unknown', requestId })
24
+ const response = await fetch(`${baseURL}/alpha/search`, {
25
+ method: 'POST',
26
+ headers: { 'content-type': 'application/json', authorization: `Bearer ${apiKey}`, 'x-client-request-id': requestId, 'session-id': sessionId },
27
+ body: JSON.stringify(buildAlphaSearchBody(args, model, sessionId, true, 2500)),
28
+ })
29
+ const text = await response.text()
30
+ let body
31
+ try { body = text ? JSON.parse(text) : {} } catch { body = { output: text, results: [] } }
32
+ if (!response.ok) { const e = new Error(body?.error?.message ?? body?.message ?? `HTTP ${response.status}`); e.status = response.status; throw e }
33
+ return parseAlphaSearchResponse(body, { action: args.action, capability: 'command-capable', requestId })
57
34
  }
58
35
 
59
- const actionProbes = process.env.LCX_ALPHA_PROBE_STRUCTURED === '1' ? {
60
- image_query: { query: 'OpenAI logo' },
61
- finance: { ticker: 'AAPL', assetType: 'equity', market: 'USA' },
62
- weather: { location: 'United States, California, San Francisco', duration: 1 },
63
- sports: { fn: 'schedule', league: 'nba', numberOfGames: 1 },
64
- time: { utcOffset: '+00:00' },
36
+ const structured = process.env.LCX_ALPHA_PROBE_STRUCTURED === '1'
37
+ const actionProbes = structured ? {
38
+ image_query: { query: 'OpenAI logo' }, finance: { ticker: 'MSFT', assetType: 'equity', market: 'USA' }, weather: { location: 'San Francisco, CA' }, sports: { fn: 'standings', league: 'nba' }, time: { utcOffset: '+00:00' },
65
39
  } : {}
66
-
67
- const record = await probeAlphaCapabilities({
68
- invoke,
69
- schemaFingerprint: ALPHA_SCHEMA_FINGERPRINT,
70
- trustedNativeProvenance,
71
- actionProbes,
72
- clickProbeRef,
73
- screenshotProbeRef,
74
- })
40
+ const result = await probeAlphaCapabilities({ invoke, schemaFingerprint: ALPHA_SCHEMA_FINGERPRINT, trustedNativeProvenance: process.env.LCX_ALPHA_TRUST_NATIVE === '1', actionProbes, clickProbeRef: process.env.LCX_ALPHA_CLICK_PROBE_REF, screenshotProbeRef: process.env.LCX_ALPHA_SCREENSHOT_PROBE_REF })
75
41
  const fingerprint = alphaCapabilityFingerprint({ baseURL, provider, model, profile, group, schemaFingerprint: ALPHA_SCHEMA_FINGERPRINT })
76
- new AlphaCapabilityStore(capabilityPath).put(fingerprint, record)
77
-
78
- const endpoint = new URL(baseURL)
79
- process.stdout.write(`${JSON.stringify({
80
- endpoint: endpoint.host,
81
- provider,
82
- model,
83
- profile,
84
- group,
85
- fingerprint: fingerprint.slice(0, 12),
86
- classification: record.classification,
87
- provenance: record.provenance,
88
- actions: record.actions,
89
- probedAt: record.probedAt,
90
- capabilityPath,
91
- }, null, 2)}\n`)
42
+ new AlphaCapabilityStore(storeFile).put(fingerprint, result)
43
+ console.log(JSON.stringify({ fingerprint, storeFile, classification: result.classification, actions: result.actions, probedAt: result.probedAt, provenance: result.provenance }, null, 2))