dsh-lcx-codex 0.4.2 → 0.4.3-pre.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.
Files changed (47) hide show
  1. package/README.md +116 -202
  2. package/cordis.patch.yml +3 -20
  3. package/lib/client.js +273 -146
  4. package/lib/compact-v2.js +218 -199
  5. package/lib/dsh-compat.js +227 -100
  6. package/lib/dsh-responses.js +445 -277
  7. package/lib/index.js +851 -757
  8. package/lib/json-store.js +50 -31
  9. package/lib/native-checkpoint.js +520 -194
  10. package/lib/responses-request.js +106 -121
  11. package/lib/responses-stream.js +972 -443
  12. package/lib/route.js +229 -355
  13. package/lib/service-mutex.js +73 -64
  14. package/lib/token-budget.js +176 -108
  15. package/lib/transport.js +277 -67
  16. package/lib/types/client/index.d.ts +6 -0
  17. package/lib/types/compact-v2.d.ts +104 -0
  18. package/lib/types/dsh-compat.d.ts +78 -0
  19. package/lib/types/dsh-responses.d.ts +82 -0
  20. package/lib/types/index.d.ts +83 -0
  21. package/lib/types/json-store.d.ts +10 -0
  22. package/lib/types/native-checkpoint.d.ts +213 -0
  23. package/lib/types/responses-request.d.ts +58 -0
  24. package/lib/types/responses-stream.d.ts +51 -0
  25. package/lib/types/route.d.ts +132 -0
  26. package/lib/types/service-mutex.d.ts +14 -0
  27. package/lib/types/token-budget.d.ts +50 -0
  28. package/lib/types/transport.d.ts +20 -0
  29. package/lib/types/web-run-output.d.ts +29 -0
  30. package/lib/types/web-search-alpha.d.ts +286 -0
  31. package/lib/types/web-search-capability.d.ts +26 -0
  32. package/lib/types/web-search-hosted.d.ts +246 -0
  33. package/lib/types/web-search-ref-store.d.ts +22 -0
  34. package/lib/web-run-output.js +167 -18
  35. package/lib/web-search-alpha.js +865 -163
  36. package/lib/web-search-capability.js +55 -65
  37. package/lib/web-search-hosted.js +210 -32
  38. package/lib/web-search-ref-store.js +63 -59
  39. package/package.json +79 -27
  40. package/ARCHITECTURE.md +0 -117
  41. package/CHANGELOG.md +0 -224
  42. package/README_EN.md +0 -277
  43. package/assets/dsh-lcx-codex-banner.jpg +0 -0
  44. package/lib/legacy-v3.js +0 -20
  45. package/lib/responses-replay.js +0 -68
  46. package/scripts/probe-alpha.mjs +0 -43
  47. package/scripts/validate-dsh-schema.mjs +0 -31
@@ -1,78 +1,68 @@
1
- import { createHash } from 'node:crypto'
2
- import { ALPHA_PROBE_VERSION } from './web-search-alpha.js'
3
- import { JsonStore } from './json-store.js'
4
-
5
- const VERSION = 1
6
- const CLASSIFICATIONS = new Set(['native', 'command-capable', 'emulated-search-only', 'unsupported', 'unknown'])
7
- const ACTION_STATES = new Set(['supported', 'unsupported', 'unknown'])
8
-
1
+ import { createHash } from "node:crypto";
2
+ import { ALPHA_PROBE_VERSION } from "./web-search-alpha.js";
3
+ import { JsonStore } from "./json-store.js";
4
+ const VERSION = 1;
5
+ const CLASSIFICATIONS = new Set(["native", "command-capable", "emulated-search-only", "unsupported", "unknown"]);
6
+ const ACTION_STATES = new Set(["supported", "unsupported", "unknown"]);
9
7
  function isRecord(value) {
10
- return value !== null && typeof value === 'object' && !Array.isArray(value)
8
+ return value !== null && typeof value === "object" && !Array.isArray(value);
11
9
  }
12
-
10
+ function isClassification(value) { return typeof value === "string" && CLASSIFICATIONS.has(value); }
11
+ function isActionState(value) { return typeof value === "string" && ACTION_STATES.has(value); }
12
+ function isProvenance(value) { return value === "trusted-native" || value === "unavailable"; }
13
+ function isPositiveSafeInteger(value) { return typeof value === "number" && Number.isSafeInteger(value) && value > 0; }
13
14
  function validRecord(record) {
14
- return isRecord(record) && CLASSIFICATIONS.has(record.classification) &&
15
- isRecord(record.actions) && Object.values(record.actions).every((value) => ACTION_STATES.has(value)) &&
16
- typeof record.probedAt === 'string' && !Number.isNaN(Date.parse(record.probedAt)) &&
17
- typeof record.schemaFingerprint === 'string' && record.schemaFingerprint.length > 0 &&
18
- (record.probeVersion === undefined || (Number.isSafeInteger(record.probeVersion) && record.probeVersion > 0)) &&
19
- (record.provenance === undefined || ['trusted-native', 'unavailable'].includes(record.provenance)) &&
20
- (record.classification !== 'native' || record.provenance === 'trusted-native')
15
+ if (!isRecord(record) || !isClassification(record.classification) || !isRecord(record.actions) ||
16
+ !Object.values(record.actions).every(isActionState) || typeof record.probedAt !== "string" ||
17
+ Number.isNaN(Date.parse(record.probedAt)) || typeof record.schemaFingerprint !== "string" || !record.schemaFingerprint ||
18
+ (record.probeVersion !== undefined && !isPositiveSafeInteger(record.probeVersion)) ||
19
+ (record.provenance !== undefined && !isProvenance(record.provenance)))
20
+ return false;
21
+ return record.classification !== "native" || record.provenance === "trusted-native";
21
22
  }
22
-
23
23
  function validData(data) {
24
- return data?.version === VERSION && isRecord(data.capabilities) &&
25
- Object.entries(data.capabilities).every(([key, value]) => /^[a-f0-9]{64}$/u.test(key) && validRecord(value))
24
+ return isRecord(data) && data.version === VERSION && isRecord(data.capabilities) &&
25
+ Object.entries(data.capabilities).every(([key, value]) => /^[a-f0-9]{64}$/u.test(key) && validRecord(value));
26
26
  }
27
-
28
27
  export function alphaCapabilityFingerprint(config) {
29
- const rawBaseURL = String(config.baseURL ?? '').replace(/\/+$/u, '')
30
- let baseURL = rawBaseURL
31
- try {
32
- const url = new URL(rawBaseURL)
33
- url.hash = ''
34
- baseURL = url.toString().replace(/\/+$/u, '')
35
- } catch {}
36
- const canonical = {
37
- baseURL,
38
- provider: String(config.provider ?? ''),
39
- model: String(config.model ?? ''),
40
- profile: String(config.profile ?? ''),
41
- group: String(config.group ?? ''),
42
- schemaFingerprint: String(config.schemaFingerprint ?? ''),
43
- }
44
- return createHash('sha256').update(JSON.stringify(canonical), 'utf8').digest('hex')
28
+ const rawBaseURL = String(config.baseURL ?? "").replace(/\/+$/u, "");
29
+ let baseURL = rawBaseURL;
30
+ try {
31
+ const url = new URL(rawBaseURL);
32
+ url.hash = "";
33
+ baseURL = url.toString().replace(/\/+$/u, "");
34
+ }
35
+ catch { }
36
+ return createHash("sha256").update(JSON.stringify({
37
+ baseURL,
38
+ provider: String(config.provider ?? ""),
39
+ model: String(config.model ?? ""),
40
+ profile: String(config.profile ?? ""),
41
+ group: String(config.group ?? ""),
42
+ schemaFingerprint: String(config.schemaFingerprint ?? ""),
43
+ }), "utf8").digest("hex");
45
44
  }
46
-
47
45
  export function alphaCapabilityUsable(record) {
48
- return record?.probeVersion === ALPHA_PROBE_VERSION && (record.classification === 'native' || record.classification === 'command-capable')
46
+ return validRecord(record) && record.probeVersion === ALPHA_PROBE_VERSION &&
47
+ (record.classification === "native" || record.classification === "command-capable");
49
48
  }
50
-
51
49
  export class AlphaCapabilityStore {
52
- constructor(file) {
53
- this.store = new JsonStore(
54
- file,
55
- () => ({ version: VERSION, capabilities: {} }),
56
- validData,
57
- 'LCX_ALPHA_CAPABILITY_STORE_CORRUPT',
58
- )
59
- }
60
-
61
- get(fingerprint) {
62
- this.store.refresh()
63
- const value = this.store.data.capabilities[fingerprint]
64
- return value?.probeVersion === ALPHA_PROBE_VERSION ? structuredClone(value) : undefined
65
- }
66
-
67
- put(fingerprint, record) {
68
- if (!/^[a-f0-9]{64}$/u.test(String(fingerprint)) || record?.probeVersion !== ALPHA_PROBE_VERSION || !validRecord(record)) {
69
- const error = new Error('Invalid Alpha capability record')
70
- error.code = 'LCX_ALPHA_CAPABILITY_INVALID'
71
- throw error
50
+ store;
51
+ constructor(file) {
52
+ this.store = new JsonStore(file, () => ({ version: VERSION, capabilities: {} }), validData, "LCX_ALPHA_CAPABILITY_STORE_CORRUPT");
53
+ }
54
+ get(fingerprint) {
55
+ this.store.refresh();
56
+ const value = this.store.data.capabilities[fingerprint];
57
+ return value?.probeVersion === ALPHA_PROBE_VERSION ? structuredClone(value) : undefined;
58
+ }
59
+ put(fingerprint, record) {
60
+ if (typeof fingerprint !== "string" || !/^[a-f0-9]{64}$/u.test(fingerprint) || !validRecord(record) || record.probeVersion !== ALPHA_PROBE_VERSION) {
61
+ throw Object.assign(new Error("Invalid Alpha capability record"), { code: "LCX_ALPHA_CAPABILITY_INVALID" });
62
+ }
63
+ this.store.update((current) => ({
64
+ version: VERSION,
65
+ capabilities: { ...current.capabilities, [fingerprint]: structuredClone(record) },
66
+ }));
72
67
  }
73
- this.store.update((current) => ({
74
- version: VERSION,
75
- capabilities: { ...current.capabilities, [fingerprint]: structuredClone(record) },
76
- }))
77
- }
78
68
  }
@@ -1,33 +1,211 @@
1
- import { outputDomains, outputLineRange, parseWebRunOutput } from './web-run-output.js'
2
-
3
- export const HOSTED_SEARCH_PARAMETERS = {
4
- type: 'object',
5
- properties: {
6
- query: { type: 'string', description: 'Advanced Responses Hosted Web Search query. Use DSH web_search for ordinary searches.' },
7
- searchContextSize: { type: 'string', enum: ['low', 'medium', 'high'] },
8
- allowedDomains: { type: 'array', items: { type: 'string' } },
9
- blockedDomains: { type: 'array', items: { type: 'string' } },
10
- userLocation: { type: 'object', properties: { country: { type: 'string' }, city: { type: 'string' }, region: { type: 'string' }, timezone: { type: 'string' } }, additionalProperties: false },
11
- externalWebAccess: { type: 'boolean' },
12
- returnTokenBudget: { type: 'string', enum: ['default', 'unlimited'] },
13
- searchContentTypes: { type: 'array', items: { type: 'string', enum: ['text', 'image'] } },
14
- imageSettings: { type: 'object', properties: { maxResults: { type: 'integer' }, caption: { type: 'boolean' } }, additionalProperties: false },
15
- },
16
- required: ['query'], additionalProperties: false,
1
+ import { outputDomains, outputLineRange, parseWebRunOutput } from "./web-run-output.js";
2
+ export const HOSTED_SEARCH_PARAMETERS = { type: "object", properties: { query: { type: "string", description: "Advanced Responses Hosted Web Search query. Use DSH web_search for ordinary searches." }, searchContextSize: { type: "string", enum: ["low", "medium", "high"] }, allowedDomains: { type: "array", items: { type: "string" } }, blockedDomains: { type: "array", items: { type: "string" } }, userLocation: { type: "object", properties: { country: { type: "string" }, city: { type: "string" }, region: { type: "string" }, timezone: { type: "string" } }, additionalProperties: false }, externalWebAccess: { type: "boolean" }, returnTokenBudget: { type: "string", enum: ["default", "unlimited"] }, searchContentTypes: { type: "array", items: { type: "string", enum: ["text", "image"] } }, imageSettings: { type: "object", properties: { maxResults: { type: "integer" }, caption: { type: "boolean" } }, additionalProperties: false } }, required: ["query"], additionalProperties: false };
3
+ 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 };
4
+ function failure(message, code = "WEB_INVALID_REQUEST") { return Object.assign(new Error(message), { code }); }
5
+ function isRecord(value) { return value !== null && typeof value === "object" && !Array.isArray(value); }
6
+ function isStringArray(value) { return Array.isArray(value) && value.every((item) => typeof item === "string"); }
7
+ function isInteger(value) { return typeof value === "number" && Number.isInteger(value); }
8
+ function httpUrl(value) { if (typeof value !== "string")
9
+ return undefined; try {
10
+ const url = new URL(value);
11
+ return ["http:", "https:"].includes(url.protocol) ? url : undefined;
17
12
  }
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') }] }
13
+ catch {
14
+ return undefined;
15
+ } }
16
+ function normalizeDomains(value, field) {
17
+ if (value === undefined)
18
+ return undefined;
19
+ if (!isStringArray(value) || value.length < 1 || value.length > 100)
20
+ throw failure(`websearch_gpt_advanced.${field} must contain 1 to 100 domains`);
21
+ const result = value.map((item) => {
22
+ const domain = item.trim().toLowerCase();
23
+ if (!domain || domain.length > 253 || domain.includes("/") || domain.includes(":") || domain.endsWith(".") || domain.split(".").length < 2)
24
+ throw failure(`websearch_gpt_advanced.${field} contains an invalid domain`);
25
+ return domain;
26
+ });
27
+ if (new Set(result).size !== result.length)
28
+ throw failure(`websearch_gpt_advanced.${field} contains duplicates`);
29
+ return result;
30
+ }
31
+ function normalizeLocation(value) {
32
+ if (!isRecord(value))
33
+ throw failure("websearch_gpt_advanced.userLocation must be an object");
34
+ const result = {};
35
+ if (value.country !== undefined) {
36
+ if (typeof value.country !== "string" || !/^[a-z]{2}$/iu.test(value.country.trim()))
37
+ throw failure("userLocation.country must be ISO alpha-2");
38
+ result.country = value.country.trim().toUpperCase();
39
+ }
40
+ for (const field of ["city", "region"])
41
+ if (value[field] !== undefined) {
42
+ const text = value[field];
43
+ if (typeof text !== "string" || !text.trim() || text.length > 200)
44
+ throw failure(`userLocation.${field} is invalid`);
45
+ result[field] = text.trim();
46
+ }
47
+ if (value.timezone !== undefined) {
48
+ if (typeof value.timezone !== "string")
49
+ throw failure("userLocation.timezone must be an IANA timezone");
50
+ try {
51
+ new Intl.DateTimeFormat("en-US", { timeZone: value.timezone }).format();
52
+ }
53
+ catch {
54
+ throw failure("userLocation.timezone must be an IANA timezone");
55
+ }
56
+ result.timezone = value.timezone;
57
+ }
58
+ if (!Object.keys(result).length)
59
+ throw failure("userLocation is empty");
60
+ return result;
61
+ }
62
+ export function normalizeHostedSearchArgs(args) {
63
+ if (!isRecord(args) || typeof args.query !== "string" || !args.query.trim())
64
+ throw failure("websearch_gpt_advanced.query must be non-empty");
65
+ const query = args.query.trim();
66
+ if (query.length > 16_000)
67
+ throw failure("query is too long");
68
+ const result = { query };
69
+ if (args.searchContextSize !== undefined) {
70
+ if (!["low", "medium", "high"].includes(String(args.searchContextSize)))
71
+ throw failure("searchContextSize is invalid");
72
+ result.searchContextSize = args.searchContextSize;
73
+ }
74
+ const allowedDomains = normalizeDomains(args.allowedDomains, "allowedDomains");
75
+ const blockedDomains = normalizeDomains(args.blockedDomains, "blockedDomains");
76
+ if (allowedDomains)
77
+ result.allowedDomains = allowedDomains;
78
+ if (blockedDomains)
79
+ result.blockedDomains = blockedDomains;
80
+ if (allowedDomains && blockedDomains && allowedDomains.some((domain) => blockedDomains.includes(domain)))
81
+ throw failure("domain filters conflict");
82
+ if (args.userLocation !== undefined)
83
+ result.userLocation = normalizeLocation(args.userLocation);
84
+ if (args.externalWebAccess !== undefined) {
85
+ if (typeof args.externalWebAccess !== "boolean")
86
+ throw failure("externalWebAccess must be boolean");
87
+ result.externalWebAccess = args.externalWebAccess;
88
+ }
89
+ if (args.returnTokenBudget !== undefined) {
90
+ if (args.returnTokenBudget !== "default" && args.returnTokenBudget !== "unlimited")
91
+ throw failure("returnTokenBudget is invalid");
92
+ result.returnTokenBudget = args.returnTokenBudget;
93
+ }
94
+ if (args.searchContentTypes !== undefined) {
95
+ if (!isStringArray(args.searchContentTypes) || !args.searchContentTypes.length || args.searchContentTypes.length > 2 || args.searchContentTypes.some((value) => value !== "text" && value !== "image"))
96
+ throw failure("searchContentTypes is invalid");
97
+ result.searchContentTypes = [...new Set(args.searchContentTypes)];
98
+ }
99
+ if (args.imageSettings !== undefined) {
100
+ if (!result.searchContentTypes?.includes("image") || !isRecord(args.imageSettings))
101
+ throw failure("imageSettings requires image search");
102
+ const imageSettings = {};
103
+ if (args.imageSettings.maxResults !== undefined) {
104
+ const maxResults = args.imageSettings.maxResults;
105
+ if (!isInteger(maxResults) || maxResults < 1 || maxResults > 100)
106
+ throw failure("imageSettings.maxResults is invalid");
107
+ imageSettings.maxResults = maxResults;
108
+ }
109
+ if (args.imageSettings.caption !== undefined) {
110
+ if (typeof args.imageSettings.caption !== "boolean")
111
+ throw failure("imageSettings.caption must be boolean");
112
+ imageSettings.caption = args.imageSettings.caption;
113
+ }
114
+ result.imageSettings = imageSettings;
115
+ }
116
+ return result;
117
+ }
118
+ export function buildHostedSearchBody(args, model, options = {}) {
119
+ 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 } : {}) } } : {}) };
120
+ 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 } : {}) };
121
+ }
122
+ function textFrom(value, seen = new Set()) {
123
+ if (typeof value === "string")
124
+ return value;
125
+ if (!value || typeof value !== "object" || seen.has(value))
126
+ return "";
127
+ seen.add(value);
128
+ if (Array.isArray(value))
129
+ return value.map((item) => textFrom(item, seen)).filter(Boolean).join("\n");
130
+ if (!isRecord(value))
131
+ return "";
132
+ if (typeof value.text === "string" && ["output_text", "text", "input_text"].includes(value.type))
133
+ return value.text;
134
+ return Array.isArray(value.content) ? textFrom(value.content, seen) : "";
135
+ }
136
+ function sourceFrom(value) { if (!isRecord(value))
137
+ return undefined; const url = httpUrl(value.url); if (!url)
138
+ return undefined; return { url: 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 } : {}) }; }
139
+ function imageFrom(value) { if (!isRecord(value) || value.type !== "image_result")
140
+ return undefined; const imageUrl = httpUrl(value.image_url); if (!imageUrl)
141
+ return undefined; const thumbnailUrl = httpUrl(value.thumbnail_url); const sourceWebsiteUrl = httpUrl(value.source_website_url); return { imageUrl: imageUrl.toString(), ...(thumbnailUrl ? { thumbnailUrl: thumbnailUrl.toString() } : {}), ...(sourceWebsiteUrl ? { sourceWebsiteUrl: sourceWebsiteUrl.toString() } : {}), ...(typeof value.caption === "string" ? { caption: value.caption } : {}) }; }
142
+ function responseArtifacts(response) { const sources = []; const citations = []; const images = []; const actions = []; if (!isRecord(response) || !Array.isArray(response.output))
143
+ return { sources, citations, images, actions }; for (const item of response.output) {
144
+ if (!isRecord(item))
145
+ continue;
146
+ if (item.type === "web_search_call") {
147
+ const action = isRecord(item.action) && typeof item.action.type === "string" ? item.action.type : "search";
148
+ actions.push(action);
149
+ if (isRecord(item.action) && Array.isArray(item.action.sources))
150
+ for (const value of item.action.sources) {
151
+ const source = sourceFrom(value);
152
+ if (source)
153
+ sources.push(source);
154
+ }
155
+ if (Array.isArray(item.results))
156
+ for (const value of item.results) {
157
+ const image = imageFrom(value);
158
+ if (image)
159
+ images.push(image);
160
+ }
161
+ }
162
+ if (item.type === "message" && Array.isArray(item.content))
163
+ for (const part of item.content)
164
+ if (isRecord(part) && part.type === "output_text" && Array.isArray(part.annotations))
165
+ for (const annotation of part.annotations)
166
+ if (isRecord(annotation) && annotation.type === "url_citation") {
167
+ const source = sourceFrom(annotation);
168
+ if (source) {
169
+ citations.push(source);
170
+ sources.push(source);
171
+ }
172
+ }
173
+ } return { sources, citations, images, actions }; }
174
+ function canonicalUrl(value) { const url = httpUrl(value); if (!url)
175
+ return undefined; url.hash = ""; for (const key of [...url.searchParams.keys()])
176
+ if (/^(utm_|gclid$|fbclid$)/iu.test(key))
177
+ url.searchParams.delete(key); return url.toString(); }
178
+ function uniqueByUrl(values) { const result = []; const seen = new Set(); for (const value of values) {
179
+ const key = canonicalUrl(value.url);
180
+ if (!key || seen.has(key))
181
+ continue;
182
+ seen.add(key);
183
+ result.push(value);
184
+ } return result; }
185
+ export function parseHostedSearchResponse(response, requestId, maxResults = 8, retrievedAt = new Date().toISOString()) {
186
+ if (!isRecord(response))
187
+ throw failure("Hosted Web Search returned an invalid response", "WEB_RESPONSE_INCOMPLETE");
188
+ if (isRecord(response.error))
189
+ throw failure(typeof response.error.message === "string" ? response.error.message : "Hosted Web Search failed", "LCX_WEB_PROVIDER_ERROR");
190
+ if (response.status !== "completed")
191
+ throw failure(`Hosted Web Search status: ${String(response.status ?? "missing")}`, "WEB_RESPONSE_INCOMPLETE");
192
+ if (Array.isArray(response.output) && response.output.some(item => isRecord(item) && item.type === "web_search_call" && item.status !== undefined && item.status !== "completed"))
193
+ throw failure("Hosted Web Search did not complete its search action", "LCX_WEB_PROVIDER_ERROR");
194
+ const output = typeof response.output_text === "string" ? response.output_text : textFrom(response.output ?? response.content);
195
+ const outputBlocks = parseWebRunOutput(output);
196
+ const artifacts = responseArtifacts(response);
197
+ if (!artifacts.actions.length)
198
+ throw failure("Hosted Web Search completed without web_search", "WEB_SEARCH_NOT_EXECUTED");
199
+ const sources = uniqueByUrl([...artifacts.sources, ...outputBlocks.flatMap((block) => block.url ? [{ url: block.url, ...(block.title ? { title: block.title } : {}) }] : [])]);
200
+ const citations = uniqueByUrl(artifacts.citations);
201
+ const limited = sources.slice(0, Math.max(1, maxResults));
202
+ const images = artifacts.images.slice(0, Math.max(1, maxResults));
203
+ if (!output && !limited.length && !images.length)
204
+ throw failure("Hosted Web Search returned no output", "WEB_NO_SOURCES");
205
+ 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 };
206
+ }
207
+ export function renderHostedSearchResult(value) { const data = isRecord(value) ? value : {}; const parts = []; if (typeof data.content === "string" && data.content)
208
+ parts.push(data.content); if (Array.isArray(data.sources) && data.sources.length)
209
+ parts.push(`来源:\n${data.sources.filter(isRecord).map((source) => `- [${String(source.title ?? source.url ?? "")}](${String(source.url ?? "")})${source.snippet ? ` — ${String(source.snippet)}` : ""}`).join("\n")}`); if (Array.isArray(data.images) && data.images.length)
210
+ parts.push(`图片:\n${data.images.filter(isRecord).map((image) => `- [${String(image.caption ?? image.imageUrl ?? "")}](${String(image.imageUrl ?? "")})`).join("\n")}`); if (Array.isArray(data.warnings) && data.warnings.length)
211
+ parts.push(data.warnings.map((warning) => `警告:${String(warning)}`).join("\n")); parts.push(`检索时间:${String(data.retrievedAt ?? "")}`); return [{ type: "text", text: parts.filter(Boolean).join("\n\n") }]; }
@@ -1,70 +1,74 @@
1
- import { JsonStore } from './json-store.js'
2
-
3
- const VERSION = 1
4
-
1
+ import { JsonStore } from "./json-store.js";
2
+ const VERSION = 1;
5
3
  function isRecord(value) {
6
- return value !== null && typeof value === 'object' && !Array.isArray(value)
4
+ return value !== null && typeof value === "object" && !Array.isArray(value);
7
5
  }
8
-
9
6
  function validHttpUrl(value) {
10
- if (value === undefined) return true
11
- try {
12
- return ['http:', 'https:'].includes(new URL(value).protocol)
13
- } catch {
14
- return false
15
- }
7
+ if (value === undefined)
8
+ return true;
9
+ if (typeof value !== "string")
10
+ return false;
11
+ try {
12
+ return ["http:", "https:"].includes(new URL(value).protocol);
13
+ }
14
+ catch {
15
+ return false;
16
+ }
17
+ }
18
+ function validRef(refId, value) {
19
+ return isRecord(value) && value.refId === refId && validHttpUrl(value.url);
20
+ }
21
+ function validSession(value) {
22
+ return isRecord(value) &&
23
+ typeof value.routeFingerprint === "string" && value.routeFingerprint.length > 0 &&
24
+ typeof value.updatedAt === "string" && !Number.isNaN(Date.parse(value.updatedAt)) &&
25
+ isRecord(value.refs) && Object.entries(value.refs).every(([refId, ref]) => refId.length > 0 && validRef(refId, ref));
16
26
  }
17
-
18
27
  function validData(data) {
19
- if (data?.version !== VERSION || !isRecord(data.sessions)) return false
20
- return Object.entries(data.sessions).every(([sessionId, session]) =>
21
- sessionId.length > 0 && isRecord(session) && typeof session.routeFingerprint === 'string' && session.routeFingerprint.length > 0 &&
22
- typeof session.updatedAt === 'string' && !Number.isNaN(Date.parse(session.updatedAt)) && isRecord(session.refs) &&
23
- Object.entries(session.refs).every(([refId, ref]) => refId.length > 0 && isRecord(ref) && ref.refId === refId && validHttpUrl(ref.url)))
28
+ return isRecord(data) && data.version === VERSION && isRecord(data.sessions) &&
29
+ Object.entries(data.sessions).every(([sessionId, session]) => sessionId.length > 0 && validSession(session));
24
30
  }
25
-
26
31
  function unavailable(refId) {
27
- const error = new Error(`Alpha reference is unavailable in this session and route: ${String(refId)}`)
28
- error.code = 'LCX_ALPHA_REF_UNAVAILABLE'
29
- return error
32
+ return Object.assign(new Error(`Alpha reference is unavailable in this session and route: ${refId}`), {
33
+ code: "LCX_ALPHA_REF_UNAVAILABLE",
34
+ });
30
35
  }
31
-
32
36
  export class AlphaRefStore {
33
- constructor(file) {
34
- this.store = new JsonStore(
35
- file,
36
- () => ({ version: VERSION, sessions: {} }),
37
- validData,
38
- 'LCX_ALPHA_REF_STORE_CORRUPT',
39
- )
40
- }
41
-
42
- record(sessionId, routeFingerprint, refs) {
43
- if (typeof sessionId !== 'string' || sessionId.length === 0 || typeof routeFingerprint !== 'string' || routeFingerprint.length === 0 || !Array.isArray(refs)) {
44
- throw unavailable('invalid-record')
37
+ store;
38
+ constructor(file) {
39
+ this.store = new JsonStore(file, () => ({ version: VERSION, sessions: {} }), validData, "LCX_ALPHA_REF_STORE_CORRUPT");
40
+ }
41
+ record(sessionId, routeFingerprint, refs) {
42
+ if (typeof sessionId !== "string" || !sessionId || typeof routeFingerprint !== "string" || !routeFingerprint || !Array.isArray(refs)) {
43
+ throw unavailable("invalid-record");
44
+ }
45
+ this.store.update((current) => {
46
+ const previous = current.sessions[sessionId];
47
+ const previousRefs = previous?.routeFingerprint === routeFingerprint ? previous.refs : {};
48
+ const nextRefs = { ...previousRefs };
49
+ for (const value of refs) {
50
+ if (!isRecord(value) || typeof value.refId !== "string" || !value.refId || !validHttpUrl(value.url))
51
+ continue;
52
+ nextRefs[value.refId] = { refId: value.refId, ...(value.url ? { url: value.url } : {}) };
53
+ }
54
+ const sessions = {
55
+ ...current.sessions,
56
+ [sessionId]: { routeFingerprint, refs: nextRefs, updatedAt: new Date().toISOString() },
57
+ };
58
+ const ordered = Object.entries(sessions)
59
+ .sort((left, right) => Date.parse(right[1].updatedAt) - Date.parse(left[1].updatedAt))
60
+ .slice(0, 256);
61
+ return { version: VERSION, sessions: Object.fromEntries(ordered) };
62
+ });
63
+ }
64
+ assertUsable(sessionId, routeFingerprint, refId) {
65
+ this.store.refresh();
66
+ if (typeof sessionId !== "string" || typeof refId !== "string")
67
+ throw unavailable(String(refId));
68
+ const session = this.store.data.sessions[sessionId];
69
+ const ref = session?.routeFingerprint === routeFingerprint ? session.refs[refId] : undefined;
70
+ if (!ref)
71
+ throw unavailable(refId);
72
+ return structuredClone(ref);
45
73
  }
46
- this.store.update((current) => {
47
- const previous = current.sessions[sessionId]
48
- const previousRefs = previous?.routeFingerprint === routeFingerprint ? previous.refs : {}
49
- const nextRefs = { ...previousRefs }
50
- for (const value of refs) {
51
- if (!isRecord(value) || typeof value.refId !== 'string' || value.refId.length === 0 || !validHttpUrl(value.url)) continue
52
- nextRefs[value.refId] = { refId: value.refId, ...(value.url ? { url: value.url } : {}) }
53
- }
54
- const sessions = {
55
- ...current.sessions,
56
- [sessionId]: { routeFingerprint, refs: nextRefs, updatedAt: new Date().toISOString() },
57
- }
58
- const ordered = Object.entries(sessions).sort((left, right) => Date.parse(right[1].updatedAt) - Date.parse(left[1].updatedAt)).slice(0, 256)
59
- return { version: VERSION, sessions: Object.fromEntries(ordered) }
60
- })
61
- }
62
-
63
- assertUsable(sessionId, routeFingerprint, refId) {
64
- this.store.refresh()
65
- const session = this.store.data.sessions[sessionId]
66
- const ref = session?.routeFingerprint === routeFingerprint ? session.refs?.[refId] : undefined
67
- if (!ref) throw unavailable(refId)
68
- return structuredClone(ref)
69
- }
70
74
  }