dsh-lcx-codex 0.4.2 → 0.4.3-pre.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +75 -224
- package/THIRD_PARTY_NOTICES.md +64 -0
- package/cordis.patch.yml +3 -20
- package/lib/auxiliary-usage.js +63 -0
- package/lib/client.js +1398 -167
- package/lib/compact-v2.js +218 -199
- package/lib/dsh-compat.js +294 -100
- package/lib/dsh-responses.js +512 -277
- package/lib/grok-native-search.js +391 -0
- package/lib/index.js +1066 -758
- package/lib/invocation-policy-scope.js +261 -0
- package/lib/json-store.js +57 -31
- package/lib/native-checkpoint.js +520 -194
- package/lib/pi-responses-runtime.js +1571 -0
- package/lib/responses-request.js +109 -121
- package/lib/responses-stream.js +1280 -447
- package/lib/route.js +425 -369
- package/lib/search-accounting.js +86 -0
- package/lib/search-usage.js +86 -0
- package/lib/service-mutex.js +73 -64
- package/lib/token-budget.js +176 -108
- package/lib/transport.js +308 -68
- package/lib/types/client/index.d.ts +18 -0
- package/lib/types/client/search-media.d.ts +16 -0
- package/lib/types/index.d.ts +83 -0
- package/lib/web-run-output.js +189 -18
- package/lib/web-search-alpha.js +1067 -163
- package/lib/web-search-capability.js +80 -65
- package/lib/web-search-hosted.js +321 -33
- package/lib/web-search-ref-store.js +145 -60
- package/package.json +112 -32
- package/ARCHITECTURE.md +0 -117
- package/CHANGELOG.md +0 -224
- package/README_EN.md +0 -277
- package/assets/dsh-lcx-codex-banner.jpg +0 -0
- package/lib/legacy-v3.js +0 -20
- package/lib/responses-replay.js +0 -68
- package/scripts/probe-alpha.mjs +0 -43
- package/scripts/validate-dsh-schema.mjs +0 -31
|
@@ -1,78 +1,93 @@
|
|
|
1
|
-
import { createHash } from
|
|
2
|
-
import { ALPHA_PROBE_VERSION } from
|
|
3
|
-
import { JsonStore } from
|
|
4
|
-
|
|
5
|
-
const
|
|
6
|
-
const
|
|
7
|
-
const ACTION_STATES = new Set(['supported', 'unsupported', 'unknown'])
|
|
8
|
-
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { ALPHA_ACTIONS, ALPHA_PROBE_VERSION, ALPHA_SEARCH_PARAMETERS } 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
|
-
|
|
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
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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
|
-
|
|
25
|
-
|
|
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
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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
|
-
|
|
46
|
+
return validRecord(record) && record.probeVersion === ALPHA_PROBE_VERSION &&
|
|
47
|
+
(record.classification === "native" || record.classification === "command-capable");
|
|
48
|
+
}
|
|
49
|
+
export function alphaActionState(record, action) {
|
|
50
|
+
if (!validRecord(record) || typeof action !== "string" || !action)
|
|
51
|
+
return undefined;
|
|
52
|
+
const state = record.actions[action];
|
|
53
|
+
return isActionState(state) ? state : undefined;
|
|
54
|
+
}
|
|
55
|
+
export function assertAlphaActionAllowed(record, action) {
|
|
56
|
+
if (alphaActionState(record, action) === "unsupported") {
|
|
57
|
+
throw Object.assign(new Error(`Alpha action is unsupported for this verified route: ${String(action)}`), { code: "LCX_ALPHA_ACTION_UNSUPPORTED" });
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
export function alphaAdvertisedActions(record) {
|
|
61
|
+
if (!validRecord(record))
|
|
62
|
+
return [...ALPHA_ACTIONS];
|
|
63
|
+
return ALPHA_ACTIONS.filter((action) => record.actions[action] !== "unsupported");
|
|
64
|
+
}
|
|
65
|
+
export function alphaSearchParametersFor(record) {
|
|
66
|
+
return {
|
|
67
|
+
...ALPHA_SEARCH_PARAMETERS,
|
|
68
|
+
properties: {
|
|
69
|
+
...ALPHA_SEARCH_PARAMETERS.properties,
|
|
70
|
+
action: { type: "string", enum: alphaAdvertisedActions(record) },
|
|
71
|
+
},
|
|
72
|
+
};
|
|
49
73
|
}
|
|
50
|
-
|
|
51
74
|
export class AlphaCapabilityStore {
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
const error = new Error('Invalid Alpha capability record')
|
|
70
|
-
error.code = 'LCX_ALPHA_CAPABILITY_INVALID'
|
|
71
|
-
throw error
|
|
75
|
+
store;
|
|
76
|
+
constructor(file) {
|
|
77
|
+
this.store = new JsonStore(file, () => ({ version: VERSION, capabilities: {} }), validData, "LCX_ALPHA_CAPABILITY_STORE_CORRUPT");
|
|
78
|
+
}
|
|
79
|
+
get(fingerprint) {
|
|
80
|
+
this.store.refresh();
|
|
81
|
+
const value = this.store.data.capabilities[fingerprint];
|
|
82
|
+
return value?.probeVersion === ALPHA_PROBE_VERSION ? structuredClone(value) : undefined;
|
|
83
|
+
}
|
|
84
|
+
put(fingerprint, record) {
|
|
85
|
+
if (typeof fingerprint !== "string" || !/^[a-f0-9]{64}$/u.test(fingerprint) || !validRecord(record) || record.probeVersion !== ALPHA_PROBE_VERSION) {
|
|
86
|
+
throw Object.assign(new Error("Invalid Alpha capability record"), { code: "LCX_ALPHA_CAPABILITY_INVALID" });
|
|
87
|
+
}
|
|
88
|
+
this.store.update((current) => ({
|
|
89
|
+
version: VERSION,
|
|
90
|
+
capabilities: { ...current.capabilities, [fingerprint]: structuredClone(record) },
|
|
91
|
+
}));
|
|
72
92
|
}
|
|
73
|
-
this.store.update((current) => ({
|
|
74
|
-
version: VERSION,
|
|
75
|
-
capabilities: { ...current.capabilities, [fingerprint]: structuredClone(record) },
|
|
76
|
-
}))
|
|
77
|
-
}
|
|
78
93
|
}
|
package/lib/web-search-hosted.js
CHANGED
|
@@ -1,33 +1,321 @@
|
|
|
1
|
-
import { outputDomains, outputLineRange, parseWebRunOutput } from
|
|
2
|
-
|
|
3
|
-
export const
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
function
|
|
32
|
-
|
|
33
|
-
|
|
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" }, usage: { type: "object", properties: { inputTokens: { type: "number" }, outputTokens: { type: "number" }, totalTokens: { type: "number" }, cachedInputTokens: { type: "number" }, actionCount: { type: "number" }, serverWebSearchCalls: { type: "number" } }, additionalProperties: false } }, 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;
|
|
12
|
+
}
|
|
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
|
+
function nonNegativeInt(value) {
|
|
186
|
+
return typeof value === "number" && Number.isSafeInteger(value) && value >= 0 ? value : undefined;
|
|
187
|
+
}
|
|
188
|
+
function hostedUsageFrom(response, actionCount) {
|
|
189
|
+
const usage = isRecord(response) && isRecord(response.usage) ? response.usage : undefined;
|
|
190
|
+
const inputDetails = usage && isRecord(usage.input_tokens_details)
|
|
191
|
+
? usage.input_tokens_details
|
|
192
|
+
: usage && isRecord(usage.prompt_tokens_details)
|
|
193
|
+
? usage.prompt_tokens_details
|
|
194
|
+
: undefined;
|
|
195
|
+
const serverDetails = usage && isRecord(usage.server_side_tool_usage_details)
|
|
196
|
+
? usage.server_side_tool_usage_details
|
|
197
|
+
: undefined;
|
|
198
|
+
const result = {};
|
|
199
|
+
const inputTokens = usage ? nonNegativeInt(usage.input_tokens ?? usage.prompt_tokens) : undefined;
|
|
200
|
+
const outputTokens = usage ? nonNegativeInt(usage.output_tokens ?? usage.completion_tokens) : undefined;
|
|
201
|
+
const totalTokens = usage ? nonNegativeInt(usage.total_tokens) : undefined;
|
|
202
|
+
const cachedInputTokens = inputDetails ? nonNegativeInt(inputDetails.cached_tokens) : undefined;
|
|
203
|
+
const serverWebSearchCalls = serverDetails ? nonNegativeInt(serverDetails.web_search_calls) : undefined;
|
|
204
|
+
if (inputTokens !== undefined)
|
|
205
|
+
result.inputTokens = inputTokens;
|
|
206
|
+
if (outputTokens !== undefined)
|
|
207
|
+
result.outputTokens = outputTokens;
|
|
208
|
+
if (totalTokens !== undefined)
|
|
209
|
+
result.totalTokens = totalTokens;
|
|
210
|
+
// cachedInputTokens is a subset of inputTokens (cache hits), not an extra addend.
|
|
211
|
+
if (cachedInputTokens !== undefined)
|
|
212
|
+
result.cachedInputTokens = cachedInputTokens;
|
|
213
|
+
if (actionCount > 0)
|
|
214
|
+
result.actionCount = actionCount;
|
|
215
|
+
if (serverWebSearchCalls !== undefined)
|
|
216
|
+
result.serverWebSearchCalls = serverWebSearchCalls;
|
|
217
|
+
return Object.keys(result).length ? result : undefined;
|
|
218
|
+
}
|
|
219
|
+
function sourceLine(source) {
|
|
220
|
+
return `- [${String(source.title ?? source.url ?? "")}](${String(source.url ?? "")})${source.snippet ? ` — ${String(source.snippet)}` : ""}`;
|
|
221
|
+
}
|
|
222
|
+
function usageLine(usage) {
|
|
223
|
+
const parts = [];
|
|
224
|
+
if (usage.inputTokens !== undefined)
|
|
225
|
+
parts.push(`input=${String(usage.inputTokens)}`);
|
|
226
|
+
if (usage.outputTokens !== undefined)
|
|
227
|
+
parts.push(`output=${String(usage.outputTokens)}`);
|
|
228
|
+
if (usage.totalTokens !== undefined)
|
|
229
|
+
parts.push(`total=${String(usage.totalTokens)}`);
|
|
230
|
+
if (usage.cachedInputTokens !== undefined)
|
|
231
|
+
parts.push(`cachedInput=${String(usage.cachedInputTokens)}`);
|
|
232
|
+
if (usage.actionCount !== undefined)
|
|
233
|
+
parts.push(`actionCount=${String(usage.actionCount)}`);
|
|
234
|
+
if (usage.serverWebSearchCalls !== undefined)
|
|
235
|
+
parts.push(`serverWebSearchCalls=${String(usage.serverWebSearchCalls)}`);
|
|
236
|
+
return parts.length ? `用量:${parts.join(" ")}` : "";
|
|
237
|
+
}
|
|
238
|
+
export function parseHostedSearchResponse(response, requestId, maxResults = 8, retrievedAt = new Date().toISOString()) {
|
|
239
|
+
if (!isRecord(response))
|
|
240
|
+
throw failure("Hosted Web Search returned an invalid response", "WEB_RESPONSE_INCOMPLETE");
|
|
241
|
+
if (isRecord(response.error))
|
|
242
|
+
throw failure(typeof response.error.message === "string" ? response.error.message : "Hosted Web Search failed", "LCX_WEB_PROVIDER_ERROR");
|
|
243
|
+
if (response.status !== "completed")
|
|
244
|
+
throw failure(`Hosted Web Search status: ${String(response.status ?? "missing")}`, "WEB_RESPONSE_INCOMPLETE");
|
|
245
|
+
if (Array.isArray(response.output) && response.output.some(item => isRecord(item) && item.type === "web_search_call" && item.status !== undefined && item.status !== "completed"))
|
|
246
|
+
throw failure("Hosted Web Search did not complete its search action", "LCX_WEB_PROVIDER_ERROR");
|
|
247
|
+
const output = typeof response.output_text === "string" ? response.output_text : textFrom(response.output ?? response.content);
|
|
248
|
+
const outputBlocks = parseWebRunOutput(output);
|
|
249
|
+
const artifacts = responseArtifacts(response);
|
|
250
|
+
if (!artifacts.actions.length)
|
|
251
|
+
throw failure("Hosted Web Search completed without web_search", "WEB_SEARCH_NOT_EXECUTED");
|
|
252
|
+
const citations = uniqueByUrl(artifacts.citations);
|
|
253
|
+
const discovered = uniqueByUrl([...artifacts.sources, ...outputBlocks.flatMap((block) => block.url ? [{ url: block.url, ...(block.title ? { title: block.title } : {}) }] : [])]);
|
|
254
|
+
const ranked = uniqueByUrl([...citations, ...discovered]);
|
|
255
|
+
const limited = ranked.slice(0, Math.max(1, maxResults));
|
|
256
|
+
const images = artifacts.images.slice(0, Math.max(1, maxResults));
|
|
257
|
+
const usage = hostedUsageFrom(response, artifacts.actions.length);
|
|
258
|
+
if (!output && !limited.length && !images.length)
|
|
259
|
+
throw failure("Hosted Web Search returned no output", "WEB_NO_SOURCES");
|
|
260
|
+
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: ranked.length > limited.length || artifacts.images.length > images.length, ...(usage ? { usage } : {}) };
|
|
261
|
+
}
|
|
262
|
+
/** Project Hosted image results into LCX-owned, tool-private, replayable UI metadata. */
|
|
263
|
+
export function hostedMediaPresentationMeta(value, tool, base = {}) {
|
|
264
|
+
const inherited = base !== null && typeof base === "object" && !Array.isArray(base) ? base : {};
|
|
265
|
+
if (!isRecord(value) || !Array.isArray(value.images))
|
|
266
|
+
return inherited;
|
|
267
|
+
const candidates = value.images.flatMap((entry) => {
|
|
268
|
+
if (!isRecord(entry))
|
|
269
|
+
return [];
|
|
270
|
+
const imageUrl = httpUrl(entry.imageUrl);
|
|
271
|
+
if (!imageUrl)
|
|
272
|
+
return [];
|
|
273
|
+
const previewUrl = httpUrl(entry.thumbnailUrl);
|
|
274
|
+
const sourceUrl = httpUrl(entry.sourceWebsiteUrl);
|
|
275
|
+
return [{
|
|
276
|
+
kind: "image",
|
|
277
|
+
url: imageUrl.toString(),
|
|
278
|
+
...(previewUrl ? { previewUrl: previewUrl.toString() } : {}),
|
|
279
|
+
...(sourceUrl ? { sourceUrl: sourceUrl.toString() } : {}),
|
|
280
|
+
...(typeof entry.caption === "string" && entry.caption ? { caption: entry.caption } : {}),
|
|
281
|
+
structured: true,
|
|
282
|
+
}];
|
|
283
|
+
});
|
|
284
|
+
return candidates.length
|
|
285
|
+
? { ...inherited, lcxHostedMedia: { version: 1, tool, candidates } }
|
|
286
|
+
: inherited;
|
|
287
|
+
}
|
|
288
|
+
export function renderHostedSearchResult(value) {
|
|
289
|
+
const data = isRecord(value) ? value : {};
|
|
290
|
+
const parts = [];
|
|
291
|
+
if (typeof data.content === "string" && data.content)
|
|
292
|
+
parts.push(data.content);
|
|
293
|
+
const citations = Array.isArray(data.citations) ? data.citations.filter(isRecord) : [];
|
|
294
|
+
const citationUrls = new Set(citations.map((source) => String(source.url ?? "")).filter(Boolean));
|
|
295
|
+
if (citations.length)
|
|
296
|
+
parts.push(`引用:\n${citations.map(sourceLine).join("\n")}`);
|
|
297
|
+
const extraSources = Array.isArray(data.sources)
|
|
298
|
+
? data.sources.filter(isRecord).filter((source) => !citationUrls.has(String(source.url ?? "")))
|
|
299
|
+
: [];
|
|
300
|
+
if (extraSources.length)
|
|
301
|
+
parts.push(`来源:\n${extraSources.map(sourceLine).join("\n")}`);
|
|
302
|
+
if (Array.isArray(data.images) && data.images.length) {
|
|
303
|
+
parts.push(`图片:\n${data.images.filter(isRecord).map((image) => {
|
|
304
|
+
const caption = String(image.caption ?? image.imageUrl ?? "");
|
|
305
|
+
const imageUrl = String(image.imageUrl ?? "");
|
|
306
|
+
const sourcePage = typeof image.sourceWebsiteUrl === "string" && image.sourceWebsiteUrl
|
|
307
|
+
? ` 来源页: ${image.sourceWebsiteUrl}`
|
|
308
|
+
: "";
|
|
309
|
+
return `- [${caption}](${imageUrl})${sourcePage}`;
|
|
310
|
+
}).join("\n")}`);
|
|
311
|
+
}
|
|
312
|
+
if (Array.isArray(data.warnings) && data.warnings.length)
|
|
313
|
+
parts.push(data.warnings.map((warning) => `警告:${String(warning)}`).join("\n"));
|
|
314
|
+
if (isRecord(data.usage)) {
|
|
315
|
+
const renderedUsage = usageLine(data.usage);
|
|
316
|
+
if (renderedUsage)
|
|
317
|
+
parts.push(renderedUsage);
|
|
318
|
+
}
|
|
319
|
+
parts.push(`检索时间:${String(data.retrievedAt ?? "")}`);
|
|
320
|
+
return [{ type: "text", text: parts.filter(Boolean).join("\n\n") }];
|
|
321
|
+
}
|