@khanglvm/llm-router 2.0.0-beta.1 → 2.0.0
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/CHANGELOG.md +27 -0
- package/README.md +163 -426
- package/package.json +3 -3
- package/src/cli/router-module.js +2773 -2587
- package/src/cli-entry.js +32 -103
- package/src/node/activity-log.js +119 -0
- package/src/node/coding-tool-config.js +85 -11
- package/src/node/config-workflows.js +51 -12
- package/src/node/instance-state.js +1 -1
- package/src/node/litellm-context-catalog.js +184 -0
- package/src/node/local-server.js +23 -3
- package/src/node/port-reclaim.js +2 -2
- package/src/node/start-command.js +22 -22
- package/src/node/startup-manager.js +3 -3
- package/src/node/web-command.js +1 -1
- package/src/node/web-console-assets.js +1 -1
- package/src/node/web-console-client.js +34 -29
- package/src/node/web-console-server.js +420 -38
- package/src/node/web-console-styles.generated.js +1 -1
- package/src/node/web-console-ui/buffered-text-input.js +133 -0
- package/src/node/web-console-ui/config-editor-utils.js +57 -4
- package/src/node/web-console-ui/dropdown-placement.js +153 -0
- package/src/node/web-console-ui/select-search-utils.js +6 -0
- package/src/node/web-console-ui/transient-integer-input-utils.js +12 -0
- package/src/runtime/balancer.js +78 -1
- package/src/runtime/codex-request-transformer.js +16 -7
- package/src/runtime/config.js +448 -12
- package/src/runtime/handler/amp-response.js +5 -3
- package/src/runtime/handler/amp-web-search.js +2232 -0
- package/src/runtime/handler/fallback.js +30 -2
- package/src/runtime/handler/provider-call.js +353 -36
- package/src/runtime/handler/provider-translation.js +14 -0
- package/src/runtime/handler/request.js +128 -2
- package/src/runtime/handler/route-debug.js +36 -0
- package/src/runtime/handler.js +210 -20
- package/src/runtime/subscription-provider.js +1 -1
- package/src/shared/coding-tool-bindings.js +49 -0
- package/src/shared/local-router-defaults.js +62 -0
- package/src/translator/request/claude-to-openai.js +43 -0
|
@@ -0,0 +1,2232 @@
|
|
|
1
|
+
import { FORMATS } from "../../translator/index.js";
|
|
2
|
+
import { buildTimeoutSignal } from "../../shared/timeout-signal.js";
|
|
3
|
+
import { commitRouteSelection, rankRouteCandidates } from "../balancer.js";
|
|
4
|
+
import { buildCandidateKey } from "../state-store.js";
|
|
5
|
+
import { consumeCandidateRateLimits, resolveWindowRange } from "../rate-limits.js";
|
|
6
|
+
import {
|
|
7
|
+
buildProviderHeaders,
|
|
8
|
+
resolveProviderFormat,
|
|
9
|
+
resolveProviderUrl,
|
|
10
|
+
resolveRouteReference
|
|
11
|
+
} from "../config.js";
|
|
12
|
+
import { isSubscriptionProvider, makeSubscriptionProviderCall } from "../subscription-provider.js";
|
|
13
|
+
|
|
14
|
+
const SEARCH_TOOL_NAME = "web_search";
|
|
15
|
+
const READ_WEB_PAGE_TOOL_NAME = "read_web_page";
|
|
16
|
+
const DEFAULT_SEARCH_COUNT = 5;
|
|
17
|
+
const MIN_SEARCH_COUNT = 1;
|
|
18
|
+
const MAX_SEARCH_COUNT = 20;
|
|
19
|
+
const DEFAULT_TIMEOUT_MS = 15_000;
|
|
20
|
+
const MAX_READ_WEB_PAGE_TEXT_CHARS = 24_000;
|
|
21
|
+
const MAX_READ_WEB_PAGE_TABLES = 8;
|
|
22
|
+
const MAX_READ_WEB_PAGE_TABLE_ROWS = 40;
|
|
23
|
+
const SEARCH_ROUTE_KEY = "route:amp-web-search";
|
|
24
|
+
const HOSTED_WEB_SEARCH_TEST_QUERY = "Find the sunrise time in Paris today and cite the source.";
|
|
25
|
+
const SEARCH_SYSTEM_INSTRUCTION = [
|
|
26
|
+
"You just performed web searches and received real, current search results.",
|
|
27
|
+
"Synthesize the results into a direct answer with clear headings or bullets when helpful.",
|
|
28
|
+
"Mention source names, but do not include raw URLs unless the user explicitly asks for them.",
|
|
29
|
+
"Include specific dates, names, and facts when the results contain them.",
|
|
30
|
+
"Do not say that web search is unavailable."
|
|
31
|
+
].join(" ");
|
|
32
|
+
|
|
33
|
+
const AMP_WEB_SEARCH_PROVIDER_DEFINITIONS = Object.freeze([
|
|
34
|
+
Object.freeze({ id: "brave", label: "Brave", type: "api-key", defaultLimit: 1000 }),
|
|
35
|
+
Object.freeze({ id: "tavily", label: "Tavily", type: "api-key", defaultLimit: 1000 }),
|
|
36
|
+
Object.freeze({ id: "exa", label: "Exa", type: "api-key", defaultLimit: 1000 }),
|
|
37
|
+
Object.freeze({ id: "searxng", label: "SearXNG", type: "url", defaultLimit: 0 })
|
|
38
|
+
]);
|
|
39
|
+
|
|
40
|
+
const AMP_WEB_SEARCH_PROVIDER_META = new Map(
|
|
41
|
+
AMP_WEB_SEARCH_PROVIDER_DEFINITIONS.map((entry) => [entry.id, entry])
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
const SEARCH_BACKENDS = Object.freeze([
|
|
45
|
+
"brave",
|
|
46
|
+
"tavily",
|
|
47
|
+
"exa",
|
|
48
|
+
"searxng",
|
|
49
|
+
"gnews",
|
|
50
|
+
"ddghtml",
|
|
51
|
+
"bing",
|
|
52
|
+
"ddglite"
|
|
53
|
+
]);
|
|
54
|
+
|
|
55
|
+
const FREE_FALLBACK_BACKENDS = Object.freeze(["gnews", "ddghtml", "bing", "ddglite"]);
|
|
56
|
+
const inMemoryBucketUsage = new Map();
|
|
57
|
+
const inMemoryRouteCursor = new Map();
|
|
58
|
+
|
|
59
|
+
const WEB_SEARCH_FUNCTION_PARAMETERS = {
|
|
60
|
+
type: "object",
|
|
61
|
+
properties: {
|
|
62
|
+
query: {
|
|
63
|
+
type: "string",
|
|
64
|
+
description: "The search query to run against the web."
|
|
65
|
+
}
|
|
66
|
+
},
|
|
67
|
+
required: ["query"],
|
|
68
|
+
additionalProperties: false
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
const READ_WEB_PAGE_FUNCTION_PARAMETERS = {
|
|
72
|
+
type: "object",
|
|
73
|
+
properties: {
|
|
74
|
+
url: {
|
|
75
|
+
type: "string",
|
|
76
|
+
description: "The absolute URL of the web page to read."
|
|
77
|
+
}
|
|
78
|
+
},
|
|
79
|
+
required: ["url"],
|
|
80
|
+
additionalProperties: true
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
const OPENAI_WEB_SEARCH_TOOL = Object.freeze({
|
|
84
|
+
type: "function",
|
|
85
|
+
function: {
|
|
86
|
+
name: SEARCH_TOOL_NAME,
|
|
87
|
+
description: "Search the web for current information, news, documentation, or real-time facts.",
|
|
88
|
+
parameters: WEB_SEARCH_FUNCTION_PARAMETERS
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
const CLAUDE_WEB_SEARCH_TOOL = Object.freeze({
|
|
93
|
+
name: SEARCH_TOOL_NAME,
|
|
94
|
+
description: "Search the web for current information, news, documentation, or real-time facts.",
|
|
95
|
+
input_schema: WEB_SEARCH_FUNCTION_PARAMETERS
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
const OPENAI_READ_WEB_PAGE_TOOL = Object.freeze({
|
|
99
|
+
type: "function",
|
|
100
|
+
function: {
|
|
101
|
+
name: READ_WEB_PAGE_TOOL_NAME,
|
|
102
|
+
description: "Fetch and extract the readable text and table content from a web page URL.",
|
|
103
|
+
parameters: READ_WEB_PAGE_FUNCTION_PARAMETERS
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
const CLAUDE_READ_WEB_PAGE_TOOL = Object.freeze({
|
|
108
|
+
name: READ_WEB_PAGE_TOOL_NAME,
|
|
109
|
+
description: "Fetch and extract the readable text and table content from a web page URL.",
|
|
110
|
+
input_schema: READ_WEB_PAGE_FUNCTION_PARAMETERS
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
function toInteger(value, fallback, { min = Number.MIN_SAFE_INTEGER, max = Number.MAX_SAFE_INTEGER } = {}) {
|
|
114
|
+
const parsed = Number.parseInt(String(value ?? ""), 10);
|
|
115
|
+
if (!Number.isFinite(parsed)) return fallback;
|
|
116
|
+
return Math.min(max, Math.max(min, parsed));
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function toTrimmedString(value) {
|
|
120
|
+
const normalized = String(value ?? "").trim();
|
|
121
|
+
return normalized || undefined;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function parseJsonSafely(raw, fallback = null) {
|
|
125
|
+
if (typeof raw !== "string" || !raw.trim()) return fallback;
|
|
126
|
+
try {
|
|
127
|
+
return JSON.parse(raw);
|
|
128
|
+
} catch {
|
|
129
|
+
return fallback;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function buildSearchTimeoutSignal(timeoutMs = DEFAULT_TIMEOUT_MS) {
|
|
134
|
+
const timeoutControl = buildTimeoutSignal(timeoutMs);
|
|
135
|
+
return {
|
|
136
|
+
signal: timeoutControl.signal,
|
|
137
|
+
cleanup: timeoutControl.cleanup
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function runFetchWithTimeout(url, init = {}, timeoutMs = DEFAULT_TIMEOUT_MS) {
|
|
142
|
+
const timeoutControl = buildSearchTimeoutSignal(timeoutMs);
|
|
143
|
+
return fetch(url, {
|
|
144
|
+
...init,
|
|
145
|
+
...(timeoutControl.signal ? { signal: timeoutControl.signal } : {})
|
|
146
|
+
}).finally(() => timeoutControl.cleanup());
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function stripHtml(text) {
|
|
150
|
+
return String(text || "")
|
|
151
|
+
.replace(/<[^>]+>/g, " ")
|
|
152
|
+
.replace(/ /gi, " ")
|
|
153
|
+
.replace(/&/gi, "&")
|
|
154
|
+
.replace(/'|'/gi, "'")
|
|
155
|
+
.replace(/"/gi, "\"")
|
|
156
|
+
.replace(/</gi, "<")
|
|
157
|
+
.replace(/>/gi, ">")
|
|
158
|
+
.replace(/\s+/g, " ")
|
|
159
|
+
.trim();
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function clampText(value, maxChars = MAX_READ_WEB_PAGE_TEXT_CHARS) {
|
|
163
|
+
const normalized = String(value || "").trim();
|
|
164
|
+
if (!normalized) return "";
|
|
165
|
+
if (normalized.length <= maxChars) return normalized;
|
|
166
|
+
return `${normalized.slice(0, Math.max(0, maxChars - 15)).trimEnd()}\n[truncated]`;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function stripHtmlPreservingLines(text) {
|
|
170
|
+
const normalized = String(text || "")
|
|
171
|
+
.replace(/<(br|hr)\s*\/?>/gi, "\n")
|
|
172
|
+
.replace(/<\/(p|div|section|article|main|header|footer|aside|nav|li|tr|h1|h2|h3|h4|h5|h6|ul|ol|table)>/gi, "\n")
|
|
173
|
+
.replace(/<li\b[^>]*>/gi, "- ")
|
|
174
|
+
.replace(/<\/t[dh]>/gi, " | ")
|
|
175
|
+
.replace(/<t[dh]\b[^>]*>/gi, " ")
|
|
176
|
+
.replace(/<[^>]+>/g, " ");
|
|
177
|
+
return normalized
|
|
178
|
+
.replace(/ /gi, " ")
|
|
179
|
+
.replace(/&/gi, "&")
|
|
180
|
+
.replace(/'|'/gi, "'")
|
|
181
|
+
.replace(/"/gi, "\"")
|
|
182
|
+
.replace(/</gi, "<")
|
|
183
|
+
.replace(/>/gi, ">")
|
|
184
|
+
.split(/\n+/)
|
|
185
|
+
.map((line) => line.replace(/\s+/g, " ").trim())
|
|
186
|
+
.filter(Boolean)
|
|
187
|
+
.join("\n")
|
|
188
|
+
.trim();
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function extractHtmlTitle(html) {
|
|
192
|
+
return stripHtml((String(html || "").match(/<title[^>]*>([\s\S]*?)<\/title>/i) || [])[1] || "");
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function extractPreferredHtmlSection(html) {
|
|
196
|
+
const normalized = String(html || "");
|
|
197
|
+
for (const tagName of ["main", "article", "body"]) {
|
|
198
|
+
const match = normalized.match(new RegExp(`<${tagName}\\b[^>]*>([\\s\\S]*?)<\\/${tagName}>`, "i"));
|
|
199
|
+
if (match?.[1]) return match[1];
|
|
200
|
+
}
|
|
201
|
+
return normalized;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function removeHtmlNoise(html) {
|
|
205
|
+
return String(html || "")
|
|
206
|
+
.replace(/<script\b[\s\S]*?<\/script>/gi, " ")
|
|
207
|
+
.replace(/<style\b[\s\S]*?<\/style>/gi, " ")
|
|
208
|
+
.replace(/<noscript\b[\s\S]*?<\/noscript>/gi, " ")
|
|
209
|
+
.replace(/<svg\b[\s\S]*?<\/svg>/gi, " ")
|
|
210
|
+
.replace(/<template\b[\s\S]*?<\/template>/gi, " ");
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function extractHtmlTables(html) {
|
|
214
|
+
const tableBlocks = [...String(html || "").matchAll(/<table\b[\s\S]*?<\/table>/gi)].slice(0, MAX_READ_WEB_PAGE_TABLES);
|
|
215
|
+
const tables = [];
|
|
216
|
+
|
|
217
|
+
for (let index = 0; index < tableBlocks.length; index += 1) {
|
|
218
|
+
const tableHtml = tableBlocks[index]?.[0] || "";
|
|
219
|
+
const caption = stripHtml((tableHtml.match(/<caption\b[^>]*>([\s\S]*?)<\/caption>/i) || [])[1] || "");
|
|
220
|
+
const rowBlocks = [...tableHtml.matchAll(/<tr\b[\s\S]*?<\/tr>/gi)].slice(0, MAX_READ_WEB_PAGE_TABLE_ROWS);
|
|
221
|
+
const rows = rowBlocks.map((rowBlock) => {
|
|
222
|
+
const rowHtml = rowBlock?.[0] || "";
|
|
223
|
+
return [...rowHtml.matchAll(/<t[dh]\b[^>]*>([\s\S]*?)<\/t[dh]>/gi)]
|
|
224
|
+
.map((cellMatch) => stripHtml(cellMatch?.[1] || ""))
|
|
225
|
+
.filter((cell) => cell.length > 0);
|
|
226
|
+
}).filter((row) => row.length > 0);
|
|
227
|
+
|
|
228
|
+
if (rows.length === 0) continue;
|
|
229
|
+
const formattedRows = rows.map((row) => `| ${row.join(" | ")} |`).join("\n");
|
|
230
|
+
tables.push([
|
|
231
|
+
caption ? `Table ${tables.length + 1}: ${caption}` : `Table ${tables.length + 1}:`,
|
|
232
|
+
formattedRows
|
|
233
|
+
].join("\n"));
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
return tables;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function formatReadWebPageHtml(url, html) {
|
|
240
|
+
const cleanHtml = removeHtmlNoise(html);
|
|
241
|
+
const title = extractHtmlTitle(cleanHtml);
|
|
242
|
+
const mainSection = extractPreferredHtmlSection(cleanHtml);
|
|
243
|
+
const tables = extractHtmlTables(mainSection);
|
|
244
|
+
const pageText = clampText(stripHtmlPreservingLines(mainSection));
|
|
245
|
+
const sections = [
|
|
246
|
+
`URL: ${url}`
|
|
247
|
+
];
|
|
248
|
+
|
|
249
|
+
if (title) sections.push(`Title: ${title}`);
|
|
250
|
+
if (tables.length > 0) sections.push(`Tables:\n${tables.join("\n\n")}`);
|
|
251
|
+
if (pageText) sections.push(`Page text:\n${pageText}`);
|
|
252
|
+
|
|
253
|
+
return sections.join("\n\n").trim();
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function formatReadWebPageBody(url, bodyText, contentType = "") {
|
|
257
|
+
const sections = [
|
|
258
|
+
`URL: ${url}`
|
|
259
|
+
];
|
|
260
|
+
const normalizedContentType = String(contentType || "").trim();
|
|
261
|
+
if (normalizedContentType) sections.push(`Content-Type: ${normalizedContentType}`);
|
|
262
|
+
sections.push(`Page text:\n${clampText(bodyText) || "[No readable page text extracted]"}`);
|
|
263
|
+
return sections.join("\n\n").trim();
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
function looksLikeHtml(contentType, bodyText) {
|
|
267
|
+
const normalizedContentType = String(contentType || "").toLowerCase();
|
|
268
|
+
if (normalizedContentType.includes("text/html") || normalizedContentType.includes("application/xhtml+xml")) {
|
|
269
|
+
return true;
|
|
270
|
+
}
|
|
271
|
+
const sample = String(bodyText || "").trim().slice(0, 512).toLowerCase();
|
|
272
|
+
return sample.startsWith("<!doctype html") || sample.startsWith("<html") || sample.includes("<body");
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function formatSearchResults(results) {
|
|
276
|
+
const lines = [];
|
|
277
|
+
for (let index = 0; index < results.length; index += 1) {
|
|
278
|
+
const result = results[index];
|
|
279
|
+
if (!result) continue;
|
|
280
|
+
const title = String(result.title || "").trim() || "(untitled)";
|
|
281
|
+
const url = String(result.url || "").trim();
|
|
282
|
+
const snippet = String(result.snippet || "").trim() || "(no description)";
|
|
283
|
+
lines.push(`[${index + 1}] ${title}`);
|
|
284
|
+
if (url) lines.push(`URL: ${url}`);
|
|
285
|
+
lines.push(snippet);
|
|
286
|
+
lines.push("");
|
|
287
|
+
}
|
|
288
|
+
return lines.join("\n").trim();
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
function hasSearchToolType(type) {
|
|
292
|
+
const normalized = String(type || "").trim().toLowerCase();
|
|
293
|
+
if (!normalized) return false;
|
|
294
|
+
return normalized === SEARCH_TOOL_NAME
|
|
295
|
+
|| normalized.startsWith("web_search_preview")
|
|
296
|
+
|| normalized === "web_search_20250305";
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
function hasSearchToolName(name) {
|
|
300
|
+
const normalized = String(name || "").trim().toLowerCase();
|
|
301
|
+
return normalized === SEARCH_TOOL_NAME || normalized === "web_search_preview";
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
function hasReadWebPageToolName(name) {
|
|
305
|
+
return String(name || "").trim().toLowerCase() === READ_WEB_PAGE_TOOL_NAME;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
function hasInterceptableTool(tool) {
|
|
309
|
+
if (!tool || typeof tool !== "object") return false;
|
|
310
|
+
return hasSearchToolType(tool.type)
|
|
311
|
+
|| hasSearchToolName(tool.name)
|
|
312
|
+
|| hasSearchToolName(tool.function?.name)
|
|
313
|
+
|| hasReadWebPageToolName(tool.name)
|
|
314
|
+
|| hasReadWebPageToolName(tool.function?.name);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
function hasInterceptableToolName(name) {
|
|
318
|
+
return hasSearchToolName(name) || hasReadWebPageToolName(name);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
function getToolName(tool) {
|
|
322
|
+
if (!tool || typeof tool !== "object") return "";
|
|
323
|
+
if (hasReadWebPageToolName(tool.name) || hasReadWebPageToolName(tool.function?.name)) {
|
|
324
|
+
return READ_WEB_PAGE_TOOL_NAME;
|
|
325
|
+
}
|
|
326
|
+
if (hasSearchToolType(tool.type) || hasSearchToolName(tool.name) || hasSearchToolName(tool.function?.name)) {
|
|
327
|
+
return SEARCH_TOOL_NAME;
|
|
328
|
+
}
|
|
329
|
+
return "";
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
function dedupeStrings(values = []) {
|
|
333
|
+
return [...new Set(
|
|
334
|
+
(Array.isArray(values) ? values : [values])
|
|
335
|
+
.map((value) => String(value || "").trim())
|
|
336
|
+
.filter(Boolean)
|
|
337
|
+
)];
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
function buildHostedSearchProviderId(providerId, modelId) {
|
|
341
|
+
const normalizedProviderId = String(providerId || "").trim();
|
|
342
|
+
const normalizedModelId = String(modelId || "").trim();
|
|
343
|
+
if (!normalizedProviderId || !normalizedModelId) return "";
|
|
344
|
+
return `${normalizedProviderId}/${normalizedModelId}`;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
function looksLikeHostedSearchProviderId(value) {
|
|
348
|
+
const normalized = String(value || "").trim();
|
|
349
|
+
return normalized.includes("/");
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
function isHostedSearchProvider(provider) {
|
|
353
|
+
return Boolean(
|
|
354
|
+
provider
|
|
355
|
+
&& typeof provider === "object"
|
|
356
|
+
&& looksLikeHostedSearchProviderId(provider.id)
|
|
357
|
+
&& String(provider?.providerId || "").trim()
|
|
358
|
+
&& String(provider?.model || "").trim()
|
|
359
|
+
);
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
function normalizeHostedSearchProviderEntry(entry, explicitId = "") {
|
|
363
|
+
const routeId = String(
|
|
364
|
+
explicitId
|
|
365
|
+
|| entry?.id
|
|
366
|
+
|| buildHostedSearchProviderId(
|
|
367
|
+
entry?.providerId ?? entry?.provider,
|
|
368
|
+
entry?.model ?? entry?.modelId
|
|
369
|
+
)
|
|
370
|
+
).trim();
|
|
371
|
+
if (!looksLikeHostedSearchProviderId(routeId)) return null;
|
|
372
|
+
|
|
373
|
+
const providerId = String(entry?.providerId ?? entry?.provider ?? routeId.slice(0, routeId.indexOf("/"))).trim();
|
|
374
|
+
const model = String(entry?.model ?? entry?.modelId ?? routeId.slice(routeId.indexOf("/") + 1)).trim();
|
|
375
|
+
const normalizedRouteId = buildHostedSearchProviderId(providerId, model);
|
|
376
|
+
if (!normalizedRouteId || normalizedRouteId !== routeId) return null;
|
|
377
|
+
|
|
378
|
+
return {
|
|
379
|
+
id: normalizedRouteId,
|
|
380
|
+
providerId,
|
|
381
|
+
model
|
|
382
|
+
};
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
function normalizeSearchProviderId(value) {
|
|
386
|
+
const normalized = String(value || "").trim().toLowerCase();
|
|
387
|
+
return AMP_WEB_SEARCH_PROVIDER_META.has(normalized) ? normalized : "";
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
function normalizeSearchRoutingStrategy(value) {
|
|
391
|
+
const normalized = String(value || "").trim().toLowerCase();
|
|
392
|
+
if (!normalized) return "ordered";
|
|
393
|
+
if (normalized === "quota-balance" || normalized === "quota-aware-weighted-rr") return "quota-balance";
|
|
394
|
+
return "ordered";
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
function resolveProviderDefaultLimit(providerId) {
|
|
398
|
+
return AMP_WEB_SEARCH_PROVIDER_META.get(providerId)?.defaultLimit || 0;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
function hasOwnSearchProviderField(entry, keys = []) {
|
|
402
|
+
if (!entry || typeof entry !== "object" || Array.isArray(entry)) return false;
|
|
403
|
+
return keys.some((key) => Object.prototype.hasOwnProperty.call(entry, key) && entry[key] !== undefined && entry[key] !== null && String(entry[key]).trim() !== "");
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
function resolveSearchProviderCount(provider = {}, fallback = DEFAULT_SEARCH_COUNT) {
|
|
407
|
+
return toInteger(
|
|
408
|
+
provider?.count ?? provider?.resultCount ?? provider?.["result-count"] ?? provider?.resultsPerCall ?? provider?.["results-per-call"],
|
|
409
|
+
fallback,
|
|
410
|
+
{ min: MIN_SEARCH_COUNT, max: MAX_SEARCH_COUNT }
|
|
411
|
+
);
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
function normalizeSearchProviderEntry(entry, explicitId = "", env = {}, { preserveUnconfigured = true, inheritedCount = DEFAULT_SEARCH_COUNT } = {}) {
|
|
415
|
+
const hostedProvider = normalizeHostedSearchProviderEntry(entry, explicitId);
|
|
416
|
+
if (hostedProvider) return hostedProvider;
|
|
417
|
+
|
|
418
|
+
const providerId = normalizeSearchProviderId(explicitId || entry?.id || entry?.provider || entry?.backend || entry?.name);
|
|
419
|
+
if (!providerId) return null;
|
|
420
|
+
|
|
421
|
+
const defaultLimit = resolveProviderDefaultLimit(providerId);
|
|
422
|
+
const apiKeyEnvName = providerId === "brave"
|
|
423
|
+
? "BRAVE_API_KEY"
|
|
424
|
+
: providerId === "tavily"
|
|
425
|
+
? "TAVILY_API_KEY"
|
|
426
|
+
: providerId === "exa"
|
|
427
|
+
? "EXA_API_KEY"
|
|
428
|
+
: "";
|
|
429
|
+
const limitEnvName = providerId === "brave"
|
|
430
|
+
? "BRAVE_MONTHLY_LIMIT"
|
|
431
|
+
: providerId === "tavily"
|
|
432
|
+
? "TAVILY_MONTHLY_LIMIT"
|
|
433
|
+
: providerId === "exa"
|
|
434
|
+
? "EXA_MONTHLY_LIMIT"
|
|
435
|
+
: "";
|
|
436
|
+
|
|
437
|
+
const apiKey = providerId === "searxng"
|
|
438
|
+
? undefined
|
|
439
|
+
: toTrimmedString(entry?.apiKey ?? entry?.["api-key"] ?? env?.[apiKeyEnvName]);
|
|
440
|
+
const url = providerId === "searxng"
|
|
441
|
+
? toTrimmedString(entry?.url ?? entry?.baseUrl ?? entry?.["base-url"] ?? entry?.searxngUrl ?? entry?.["searxng-url"] ?? env?.WEB_SEARCH_URL)?.replace(/\/+$/, "")
|
|
442
|
+
: undefined;
|
|
443
|
+
const hasExplicitCount = hasOwnSearchProviderField(entry, ["count", "resultCount", "result-count", "resultsPerCall", "results-per-call"]);
|
|
444
|
+
const hasExplicitLimit = hasOwnSearchProviderField(entry, ["limit", "monthlyLimit", "monthly-limit"]);
|
|
445
|
+
const hasExplicitRemaining = hasOwnSearchProviderField(entry, ["remaining", "remainingQuota", "remaining-quota", "remainingQueries", "remaining-queries"]);
|
|
446
|
+
const hasCredential = providerId === "searxng" ? Boolean(url) : Boolean(apiKey);
|
|
447
|
+
if (!preserveUnconfigured && !hasCredential && !hasExplicitCount && !hasExplicitLimit && !hasExplicitRemaining) {
|
|
448
|
+
return null;
|
|
449
|
+
}
|
|
450
|
+
const count = resolveSearchProviderCount(entry, inheritedCount);
|
|
451
|
+
const includeQuotaDefaults = hasCredential || hasExplicitLimit || hasExplicitRemaining;
|
|
452
|
+
const limit = toInteger(
|
|
453
|
+
entry?.limit ?? entry?.monthlyLimit ?? entry?.["monthly-limit"] ?? env?.[limitEnvName],
|
|
454
|
+
includeQuotaDefaults ? defaultLimit : 0,
|
|
455
|
+
{ min: 0 }
|
|
456
|
+
);
|
|
457
|
+
const remaining = toInteger(
|
|
458
|
+
entry?.remaining ?? entry?.remainingQuota ?? entry?.["remaining-quota"] ?? entry?.remainingQueries ?? entry?.["remaining-queries"],
|
|
459
|
+
includeQuotaDefaults && limit > 0 ? limit : 0,
|
|
460
|
+
{ min: 0 }
|
|
461
|
+
);
|
|
462
|
+
|
|
463
|
+
return {
|
|
464
|
+
id: providerId,
|
|
465
|
+
...(apiKey ? { apiKey } : {}),
|
|
466
|
+
...(url ? { url } : {}),
|
|
467
|
+
...(count !== DEFAULT_SEARCH_COUNT ? { count } : {}),
|
|
468
|
+
...(hasExplicitLimit || (includeQuotaDefaults && limit > 0) ? { limit } : {}),
|
|
469
|
+
...(hasExplicitRemaining || (includeQuotaDefaults && (limit > 0 || remaining > 0))
|
|
470
|
+
? { remaining: limit > 0 ? Math.min(remaining, limit) : remaining }
|
|
471
|
+
: {})
|
|
472
|
+
};
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
function normalizeConfiguredSearchProviders(rawProviders, raw = {}, env = {}) {
|
|
476
|
+
const inheritedCount = resolveSearchProviderCount(raw, toInteger(
|
|
477
|
+
env.AMP_WEB_SEARCH_COUNT ?? env.WEB_SEARCH_COUNT,
|
|
478
|
+
DEFAULT_SEARCH_COUNT,
|
|
479
|
+
{ min: MIN_SEARCH_COUNT, max: MAX_SEARCH_COUNT }
|
|
480
|
+
));
|
|
481
|
+
if (Array.isArray(rawProviders) && rawProviders.length > 0) {
|
|
482
|
+
const out = [];
|
|
483
|
+
const seen = new Set();
|
|
484
|
+
for (const entry of rawProviders) {
|
|
485
|
+
const normalized = normalizeSearchProviderEntry(entry, "", env, {
|
|
486
|
+
preserveUnconfigured: true,
|
|
487
|
+
inheritedCount
|
|
488
|
+
});
|
|
489
|
+
if (!normalized || seen.has(normalized.id)) continue;
|
|
490
|
+
seen.add(normalized.id);
|
|
491
|
+
out.push(normalized);
|
|
492
|
+
}
|
|
493
|
+
return out;
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
if (rawProviders && typeof rawProviders === "object" && !Array.isArray(rawProviders)) {
|
|
497
|
+
const out = [];
|
|
498
|
+
const seen = new Set();
|
|
499
|
+
for (const [providerId, value] of Object.entries(rawProviders)) {
|
|
500
|
+
const normalized = normalizeSearchProviderEntry(value, providerId, env, {
|
|
501
|
+
preserveUnconfigured: true,
|
|
502
|
+
inheritedCount
|
|
503
|
+
});
|
|
504
|
+
if (!normalized || seen.has(normalized.id)) continue;
|
|
505
|
+
seen.add(normalized.id);
|
|
506
|
+
out.push(normalized);
|
|
507
|
+
}
|
|
508
|
+
return out;
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
const legacyPreferredBackend = normalizeSearchProviderId(
|
|
512
|
+
raw.preferredBackend ?? raw["preferred-backend"] ?? env.AMP_WEB_SEARCH_BACKEND ?? env.WEB_SEARCH_BACKEND
|
|
513
|
+
);
|
|
514
|
+
const legacyEntries = [
|
|
515
|
+
normalizeSearchProviderEntry({
|
|
516
|
+
apiKey: raw.braveApiKey ?? raw["brave-api-key"],
|
|
517
|
+
count: raw.count,
|
|
518
|
+
limit: raw.braveMonthlyLimit ?? raw["brave-monthly-limit"],
|
|
519
|
+
remaining: raw.braveRemaining ?? raw["brave-remaining"]
|
|
520
|
+
}, "brave", env, { preserveUnconfigured: false, inheritedCount }),
|
|
521
|
+
normalizeSearchProviderEntry({
|
|
522
|
+
apiKey: raw.tavilyApiKey ?? raw["tavily-api-key"],
|
|
523
|
+
count: raw.count,
|
|
524
|
+
limit: raw.tavilyMonthlyLimit ?? raw["tavily-monthly-limit"],
|
|
525
|
+
remaining: raw.tavilyRemaining ?? raw["tavily-remaining"]
|
|
526
|
+
}, "tavily", env, { preserveUnconfigured: false, inheritedCount }),
|
|
527
|
+
normalizeSearchProviderEntry({
|
|
528
|
+
apiKey: raw.exaApiKey ?? raw["exa-api-key"],
|
|
529
|
+
count: raw.count,
|
|
530
|
+
limit: raw.exaMonthlyLimit ?? raw["exa-monthly-limit"],
|
|
531
|
+
remaining: raw.exaRemaining ?? raw["exa-remaining"]
|
|
532
|
+
}, "exa", env, { preserveUnconfigured: false, inheritedCount }),
|
|
533
|
+
normalizeSearchProviderEntry({
|
|
534
|
+
count: raw.count,
|
|
535
|
+
url: raw.searxngUrl ?? raw["searxng-url"] ?? raw.url
|
|
536
|
+
}, "searxng", env, { preserveUnconfigured: false, inheritedCount })
|
|
537
|
+
].filter(Boolean);
|
|
538
|
+
|
|
539
|
+
if (!legacyPreferredBackend) return legacyEntries;
|
|
540
|
+
return [
|
|
541
|
+
...legacyEntries.filter((entry) => entry.id === legacyPreferredBackend),
|
|
542
|
+
...legacyEntries.filter((entry) => entry.id !== legacyPreferredBackend)
|
|
543
|
+
];
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
export function resolveAmpWebSearchConfig(runtimeConfig = {}, env = {}) {
|
|
547
|
+
const amp = runtimeConfig?.amp && typeof runtimeConfig.amp === "object" ? runtimeConfig.amp : {};
|
|
548
|
+
const raw = runtimeConfig?.webSearch && typeof runtimeConfig.webSearch === "object" && !Array.isArray(runtimeConfig.webSearch)
|
|
549
|
+
? runtimeConfig.webSearch
|
|
550
|
+
: (amp.webSearch && typeof amp.webSearch === "object" && !Array.isArray(amp.webSearch)
|
|
551
|
+
? amp.webSearch
|
|
552
|
+
: {});
|
|
553
|
+
const count = toInteger(
|
|
554
|
+
raw.count ?? env.AMP_WEB_SEARCH_COUNT ?? env.WEB_SEARCH_COUNT,
|
|
555
|
+
DEFAULT_SEARCH_COUNT,
|
|
556
|
+
{ min: MIN_SEARCH_COUNT, max: MAX_SEARCH_COUNT }
|
|
557
|
+
);
|
|
558
|
+
const providers = normalizeConfiguredSearchProviders(raw.providers, raw, env);
|
|
559
|
+
|
|
560
|
+
return {
|
|
561
|
+
strategy: normalizeSearchRoutingStrategy(raw.strategy ?? env.AMP_WEB_SEARCH_STRATEGY),
|
|
562
|
+
count,
|
|
563
|
+
providers
|
|
564
|
+
};
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
function isSearchProviderConfigured(provider) {
|
|
568
|
+
if (!provider || typeof provider !== "object") return false;
|
|
569
|
+
if (isHostedSearchProvider(provider)) {
|
|
570
|
+
return Boolean(String(provider.providerId || "").trim() && String(provider.model || "").trim());
|
|
571
|
+
}
|
|
572
|
+
if (provider.id === "searxng") return Boolean(String(provider.url || "").trim());
|
|
573
|
+
return Boolean(String(provider.apiKey || "").trim());
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
function getResolvedHostedSearchRoute(runtimeConfig = {}, providerEntry = {}) {
|
|
577
|
+
if (!isHostedSearchProvider(providerEntry)) return null;
|
|
578
|
+
const resolvedRoute = resolveRouteReference(runtimeConfig, providerEntry.id);
|
|
579
|
+
if (!resolvedRoute?.provider || !resolvedRoute?.model) return null;
|
|
580
|
+
if (String(resolvedRoute.provider?.id || "").trim() !== String(providerEntry.providerId || "").trim()) return null;
|
|
581
|
+
if (String(resolvedRoute.model?.id || "").trim() !== String(providerEntry.model || "").trim()) return null;
|
|
582
|
+
return resolvedRoute;
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
function getResolvedHostedSearchModelFormats(provider, model) {
|
|
586
|
+
const modelId = String(model?.id || "").trim();
|
|
587
|
+
if (!modelId) return [];
|
|
588
|
+
|
|
589
|
+
const preferredFormat = String(provider?.lastProbe?.modelPreferredFormat?.[modelId] || "").trim();
|
|
590
|
+
if (preferredFormat === FORMATS.OPENAI || preferredFormat === FORMATS.CLAUDE) {
|
|
591
|
+
return [preferredFormat];
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
const probedFormats = dedupeStrings(provider?.lastProbe?.modelSupport?.[modelId] || [])
|
|
595
|
+
.filter((value) => value === FORMATS.OPENAI || value === FORMATS.CLAUDE);
|
|
596
|
+
if (probedFormats.length > 0) return probedFormats;
|
|
597
|
+
|
|
598
|
+
return dedupeStrings([...(model?.formats || []), model?.format])
|
|
599
|
+
.filter((value) => value === FORMATS.OPENAI || value === FORMATS.CLAUDE);
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
function supportsResolvedHostedSearchRoute(provider, model) {
|
|
603
|
+
const providerFormats = dedupeStrings([...(provider?.formats || []), provider?.format]);
|
|
604
|
+
if (!providerFormats.includes(FORMATS.OPENAI)) return false;
|
|
605
|
+
const modelFormats = getResolvedHostedSearchModelFormats(provider, model);
|
|
606
|
+
return modelFormats.length === 0 || modelFormats.includes(FORMATS.OPENAI);
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
function buildHostedSearchProviderLabel(providerEntry, resolvedRoute = null) {
|
|
610
|
+
const providerLabel = String(
|
|
611
|
+
resolvedRoute?.provider?.name
|
|
612
|
+
|| resolvedRoute?.provider?.id
|
|
613
|
+
|| providerEntry?.providerId
|
|
614
|
+
|| providerEntry?.id
|
|
615
|
+
|| "Search provider"
|
|
616
|
+
).trim();
|
|
617
|
+
const modelLabel = String(
|
|
618
|
+
resolvedRoute?.model?.id
|
|
619
|
+
|| providerEntry?.model
|
|
620
|
+
|| ""
|
|
621
|
+
).trim();
|
|
622
|
+
return modelLabel ? `${providerLabel} · ${modelLabel}` : providerLabel;
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
function extractAssistantTextFragments(payload) {
|
|
626
|
+
const fragments = [];
|
|
627
|
+
if (!payload || typeof payload !== "object") return fragments;
|
|
628
|
+
|
|
629
|
+
if (typeof payload.output_text === "string" && payload.output_text.trim()) {
|
|
630
|
+
fragments.push(payload.output_text.trim());
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
if (Array.isArray(payload.choices)) {
|
|
634
|
+
for (const choice of payload.choices) {
|
|
635
|
+
const content = choice?.message?.content;
|
|
636
|
+
if (typeof content === "string" && content.trim()) {
|
|
637
|
+
fragments.push(content.trim());
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
if (payload.type === "message" && Array.isArray(payload.content)) {
|
|
643
|
+
for (const block of payload.content) {
|
|
644
|
+
if (typeof block?.text === "string" && block.text.trim()) {
|
|
645
|
+
fragments.push(block.text.trim());
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
if (Array.isArray(payload.output)) {
|
|
651
|
+
for (const item of payload.output) {
|
|
652
|
+
if (item?.type !== "message" || item.role !== "assistant" || !Array.isArray(item.content)) continue;
|
|
653
|
+
for (const block of item.content) {
|
|
654
|
+
if (typeof block?.text === "string" && block.text.trim()) {
|
|
655
|
+
fragments.push(block.text.trim());
|
|
656
|
+
continue;
|
|
657
|
+
}
|
|
658
|
+
if (typeof block?.refusal === "string" && block.refusal.trim()) {
|
|
659
|
+
fragments.push(block.refusal.trim());
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
return fragments;
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
function payloadHasHostedWebSearchEvidence(payload) {
|
|
669
|
+
if (!payload || typeof payload !== "object") return false;
|
|
670
|
+
|
|
671
|
+
if (Array.isArray(payload.output)) {
|
|
672
|
+
if (payload.output.some((item) => item?.type === "web_search_call")) return true;
|
|
673
|
+
if (payload.output.some((item) => item?.type === "function_call" && hasSearchToolName(item?.name))) return true;
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
if (Array.isArray(payload.choices)) {
|
|
677
|
+
for (const choice of payload.choices) {
|
|
678
|
+
const toolCalls = Array.isArray(choice?.message?.tool_calls) ? choice.message.tool_calls : [];
|
|
679
|
+
if (toolCalls.some((toolCall) => hasSearchToolName(toolCall?.function?.name))) {
|
|
680
|
+
return true;
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
const contentBlocks = Array.isArray(payload?.content) ? payload.content : [];
|
|
686
|
+
if (contentBlocks.some((block) => block?.type === "tool_use" && hasSearchToolName(block?.name))) {
|
|
687
|
+
return true;
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
return false;
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
function readHostedSearchResponseText(payload) {
|
|
694
|
+
return extractAssistantTextFragments(payload).join("\n\n").trim();
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
async function readSearchProviderError(response) {
|
|
698
|
+
if (!(response instanceof Response)) return "Search provider request failed.";
|
|
699
|
+
try {
|
|
700
|
+
const raw = await response.text();
|
|
701
|
+
const parsed = parseJsonSafely(raw, null);
|
|
702
|
+
return String(
|
|
703
|
+
parsed?.error?.message
|
|
704
|
+
|| parsed?.error?.code
|
|
705
|
+
|| parsed?.error?.type
|
|
706
|
+
|| parsed?.error
|
|
707
|
+
|| parsed?.message
|
|
708
|
+
|| raw
|
|
709
|
+
|| `Search provider request failed with status ${response.status}.`
|
|
710
|
+
).trim() || `Search provider request failed with status ${response.status}.`;
|
|
711
|
+
} catch {
|
|
712
|
+
return `Search provider request failed with status ${response.status}.`;
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
function createInMemorySearchStateStore() {
|
|
717
|
+
return {
|
|
718
|
+
async getRouteCursor(routeKey) {
|
|
719
|
+
const key = String(routeKey || "").trim();
|
|
720
|
+
return key ? (inMemoryRouteCursor.get(key) || 0) : 0;
|
|
721
|
+
},
|
|
722
|
+
|
|
723
|
+
async setRouteCursor(routeKey, value) {
|
|
724
|
+
const key = String(routeKey || "").trim();
|
|
725
|
+
const normalized = Math.max(0, Math.floor(Number(value) || 0));
|
|
726
|
+
if (key) inMemoryRouteCursor.set(key, normalized);
|
|
727
|
+
return normalized;
|
|
728
|
+
},
|
|
729
|
+
|
|
730
|
+
async getCandidateState() {
|
|
731
|
+
return null;
|
|
732
|
+
},
|
|
733
|
+
|
|
734
|
+
async setCandidateState() {
|
|
735
|
+
return null;
|
|
736
|
+
},
|
|
737
|
+
|
|
738
|
+
async readBucketUsage(bucketKey, windowKey) {
|
|
739
|
+
const compositeKey = `${String(bucketKey || "").trim()}::${String(windowKey || "").trim()}`;
|
|
740
|
+
return inMemoryBucketUsage.get(compositeKey)?.count || 0;
|
|
741
|
+
},
|
|
742
|
+
|
|
743
|
+
async incrementBucketUsage(bucketKey, windowKey, amount = 1, options = {}) {
|
|
744
|
+
const compositeKey = `${String(bucketKey || "").trim()}::${String(windowKey || "").trim()}`;
|
|
745
|
+
const current = inMemoryBucketUsage.get(compositeKey) || { count: 0, expiresAt: 0 };
|
|
746
|
+
const next = {
|
|
747
|
+
count: current.count + Math.max(0, Math.floor(Number(amount) || 0)),
|
|
748
|
+
expiresAt: Number(options?.expiresAt) || current.expiresAt || 0
|
|
749
|
+
};
|
|
750
|
+
inMemoryBucketUsage.set(compositeKey, next);
|
|
751
|
+
return next.count;
|
|
752
|
+
},
|
|
753
|
+
|
|
754
|
+
async pruneExpired(now = Date.now()) {
|
|
755
|
+
const currentTime = Number(now) || Date.now();
|
|
756
|
+
let prunedBuckets = 0;
|
|
757
|
+
for (const [key, value] of inMemoryBucketUsage.entries()) {
|
|
758
|
+
const expiresAt = Number(value?.expiresAt) || 0;
|
|
759
|
+
if (expiresAt > 0 && expiresAt <= currentTime) {
|
|
760
|
+
inMemoryBucketUsage.delete(key);
|
|
761
|
+
prunedBuckets += 1;
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
return {
|
|
765
|
+
prunedBuckets,
|
|
766
|
+
prunedCandidateStates: 0
|
|
767
|
+
};
|
|
768
|
+
},
|
|
769
|
+
|
|
770
|
+
async close() {
|
|
771
|
+
return undefined;
|
|
772
|
+
}
|
|
773
|
+
};
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
function resolveSearchStateStore(stateStore) {
|
|
777
|
+
return stateStore || createInMemorySearchStateStore();
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
function buildSearchProviderBucketKey(providerId) {
|
|
781
|
+
return `amp-web-search:${String(providerId || "").trim()}`;
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
function buildSearchProviderWindow(provider, now = Date.now()) {
|
|
785
|
+
const monthlyWindow = resolveWindowRange({ unit: "month", size: 1 }, now);
|
|
786
|
+
const syncSeed = `${provider?.remaining ?? provider?.limit ?? 0}`;
|
|
787
|
+
return {
|
|
788
|
+
...monthlyWindow,
|
|
789
|
+
key: `${monthlyWindow.key}:sync=${syncSeed}`
|
|
790
|
+
};
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
async function buildSearchProviderEvaluation(provider, stateStore, now = Date.now()) {
|
|
794
|
+
const configuredRemaining = Number.isFinite(Number(provider?.remaining)) ? Math.max(0, Math.floor(Number(provider.remaining))) : 0;
|
|
795
|
+
const limit = Number.isFinite(Number(provider?.limit)) ? Math.max(0, Math.floor(Number(provider.limit))) : 0;
|
|
796
|
+
const window = buildSearchProviderWindow(provider, now);
|
|
797
|
+
const hasQuota = limit > 0;
|
|
798
|
+
const bucketKey = buildSearchProviderBucketKey(provider?.id);
|
|
799
|
+
const usedSinceSync = hasQuota
|
|
800
|
+
? Math.max(0, Math.floor(Number(await stateStore.readBucketUsage(bucketKey, window.key)) || 0))
|
|
801
|
+
: 0;
|
|
802
|
+
const currentRemaining = hasQuota
|
|
803
|
+
? Math.max(0, configuredRemaining - usedSinceSync)
|
|
804
|
+
: Number.POSITIVE_INFINITY;
|
|
805
|
+
const remainingCapacityRatio = hasQuota
|
|
806
|
+
? (limit > 0 ? (currentRemaining / limit) : 0)
|
|
807
|
+
: 1;
|
|
808
|
+
const eligible = !hasQuota || currentRemaining > 0;
|
|
809
|
+
const candidate = {
|
|
810
|
+
providerId: "amp-web-search",
|
|
811
|
+
modelId: provider.id,
|
|
812
|
+
requestModelId: `amp-web-search/${provider.id}`,
|
|
813
|
+
routeWeight: hasQuota ? Math.max(1, configuredRemaining || limit) : 1,
|
|
814
|
+
targetFormat: FORMATS.OPENAI
|
|
815
|
+
};
|
|
816
|
+
const bucket = hasQuota
|
|
817
|
+
? {
|
|
818
|
+
providerId: "amp-web-search",
|
|
819
|
+
modelId: provider.id,
|
|
820
|
+
bucketId: provider.id,
|
|
821
|
+
bucketKey,
|
|
822
|
+
window,
|
|
823
|
+
windowKey: window.key,
|
|
824
|
+
requests: Math.max(1, configuredRemaining || limit),
|
|
825
|
+
models: [provider.id],
|
|
826
|
+
metadata: {
|
|
827
|
+
providerId: provider.id,
|
|
828
|
+
syncSeed: configuredRemaining
|
|
829
|
+
},
|
|
830
|
+
used: usedSinceSync,
|
|
831
|
+
remaining: currentRemaining,
|
|
832
|
+
remainingRatio: remainingCapacityRatio,
|
|
833
|
+
exhausted: currentRemaining <= 0
|
|
834
|
+
}
|
|
835
|
+
: null;
|
|
836
|
+
|
|
837
|
+
return {
|
|
838
|
+
candidate,
|
|
839
|
+
candidateKey: buildCandidateKey(candidate),
|
|
840
|
+
eligible,
|
|
841
|
+
remainingCapacityRatio,
|
|
842
|
+
buckets: bucket ? [bucket] : [],
|
|
843
|
+
exhaustedBuckets: bucket?.exhausted ? [bucket] : [],
|
|
844
|
+
usedSinceSync,
|
|
845
|
+
currentRemaining
|
|
846
|
+
};
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
function buildSearchProviderStatus(provider, evaluation, runtimeConfig = {}) {
|
|
850
|
+
const hostedRoute = getResolvedHostedSearchRoute(runtimeConfig, provider);
|
|
851
|
+
const hostedRouteReady = hostedRoute
|
|
852
|
+
? supportsResolvedHostedSearchRoute(hostedRoute.provider, hostedRoute.model)
|
|
853
|
+
: false;
|
|
854
|
+
const definition = AMP_WEB_SEARCH_PROVIDER_META.get(provider.id) || { label: buildHostedSearchProviderLabel(provider, hostedRoute) };
|
|
855
|
+
const limit = Number.isFinite(Number(provider?.limit)) ? Math.max(0, Math.floor(Number(provider.limit))) : 0;
|
|
856
|
+
const configured = isSearchProviderConfigured(provider);
|
|
857
|
+
return {
|
|
858
|
+
...provider,
|
|
859
|
+
label: definition.label,
|
|
860
|
+
configured,
|
|
861
|
+
ready: isHostedSearchProvider(provider) ? hostedRouteReady : configured,
|
|
862
|
+
count: resolveSearchProviderCount(provider),
|
|
863
|
+
limit,
|
|
864
|
+
configuredRemaining: Number.isFinite(Number(provider?.remaining)) ? Math.max(0, Math.floor(Number(provider.remaining))) : Math.max(0, limit),
|
|
865
|
+
usedSinceSync: evaluation.usedSinceSync,
|
|
866
|
+
currentRemaining: Number.isFinite(evaluation.currentRemaining) ? evaluation.currentRemaining : null,
|
|
867
|
+
remainingCapacityRatio: evaluation.remainingCapacityRatio,
|
|
868
|
+
exhausted: evaluation.eligible === false,
|
|
869
|
+
hostedRoute: hostedRoute
|
|
870
|
+
? {
|
|
871
|
+
providerId: hostedRoute.provider.id,
|
|
872
|
+
providerName: hostedRoute.provider.name || hostedRoute.provider.id,
|
|
873
|
+
modelId: hostedRoute.model.id
|
|
874
|
+
}
|
|
875
|
+
: null,
|
|
876
|
+
evaluation
|
|
877
|
+
};
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
export async function buildAmpWebSearchSnapshot(runtimeConfig = {}, { env = {}, stateStore = null, now = Date.now() } = {}) {
|
|
881
|
+
const settings = resolveAmpWebSearchConfig(runtimeConfig, env);
|
|
882
|
+
const effectiveStateStore = resolveSearchStateStore(stateStore);
|
|
883
|
+
const providers = [];
|
|
884
|
+
|
|
885
|
+
for (const provider of settings.providers) {
|
|
886
|
+
const evaluation = await buildSearchProviderEvaluation(provider, effectiveStateStore, now);
|
|
887
|
+
providers.push(buildSearchProviderStatus(provider, evaluation, runtimeConfig));
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
return {
|
|
891
|
+
strategy: settings.strategy,
|
|
892
|
+
count: settings.count,
|
|
893
|
+
providers,
|
|
894
|
+
configuredProviderCount: providers.filter((provider) => provider.configured).length,
|
|
895
|
+
interceptEnabled: providers.some((provider) => provider.ready)
|
|
896
|
+
};
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
async function rankConfiguredSearchProviders(providerStatuses, strategy, stateStore, now = Date.now()) {
|
|
900
|
+
if (!Array.isArray(providerStatuses) || providerStatuses.length === 0) {
|
|
901
|
+
return [];
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
const evaluations = new Map();
|
|
905
|
+
const candidates = [];
|
|
906
|
+
for (const status of providerStatuses) {
|
|
907
|
+
evaluations.set(status.evaluation.candidateKey, status.evaluation);
|
|
908
|
+
candidates.push(status.evaluation.candidate);
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
const ranking = await rankRouteCandidates({
|
|
912
|
+
route: {
|
|
913
|
+
routeType: "amp-web-search",
|
|
914
|
+
routeRef: "webSearch",
|
|
915
|
+
routeStrategy: strategy === "quota-balance" ? "quota-aware-weighted-rr" : "ordered"
|
|
916
|
+
},
|
|
917
|
+
routeKey: SEARCH_ROUTE_KEY,
|
|
918
|
+
strategy: strategy === "quota-balance" ? "quota-aware-weighted-rr" : "ordered",
|
|
919
|
+
candidates,
|
|
920
|
+
stateStore,
|
|
921
|
+
config: { providers: [] },
|
|
922
|
+
rateLimitEvaluations: evaluations,
|
|
923
|
+
now
|
|
924
|
+
});
|
|
925
|
+
|
|
926
|
+
if (ranking.shouldAdvanceCursor) {
|
|
927
|
+
await commitRouteSelection(stateStore, ranking, {
|
|
928
|
+
amount: 0,
|
|
929
|
+
now
|
|
930
|
+
});
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
const statusById = new Map(providerStatuses.map((status) => [status.id, status]));
|
|
934
|
+
return ranking.entries
|
|
935
|
+
.filter((entry) => entry?.eligible)
|
|
936
|
+
.map((entry) => statusById.get(entry?.candidate?.modelId))
|
|
937
|
+
.filter(Boolean);
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
function buildSearchTag(providerStatus) {
|
|
941
|
+
const label = String(providerStatus?.label || providerStatus?.id || "Search").trim();
|
|
942
|
+
const limit = Number(providerStatus?.limit) || 0;
|
|
943
|
+
if (limit > 0 && Number.isFinite(providerStatus?.currentRemaining)) {
|
|
944
|
+
return `${label} (${providerStatus.currentRemaining}/${limit} remaining)`;
|
|
945
|
+
}
|
|
946
|
+
return label;
|
|
947
|
+
}
|
|
948
|
+
|
|
949
|
+
function decodeDuckDuckGoRedirect(url) {
|
|
950
|
+
const normalized = String(url || "").trim();
|
|
951
|
+
const uddgMatch = normalized.match(/[?&]uddg=([^&]+)/i);
|
|
952
|
+
return uddgMatch ? decodeURIComponent(uddgMatch[1]) : normalized;
|
|
953
|
+
}
|
|
954
|
+
|
|
955
|
+
async function searchBrave(query, count, provider) {
|
|
956
|
+
if (!provider?.apiKey) return null;
|
|
957
|
+
const url = `https://api.search.brave.com/res/v1/web/search?q=${encodeURIComponent(query)}&count=${count}&text_decorations=false`;
|
|
958
|
+
const response = await runFetchWithTimeout(url, {
|
|
959
|
+
headers: {
|
|
960
|
+
Accept: "application/json",
|
|
961
|
+
"X-Subscription-Token": provider.apiKey
|
|
962
|
+
}
|
|
963
|
+
});
|
|
964
|
+
if (!response.ok) return null;
|
|
965
|
+
const payload = await response.json();
|
|
966
|
+
const results = Array.isArray(payload?.web?.results) ? payload.web.results.slice(0, count) : [];
|
|
967
|
+
if (results.length === 0) return null;
|
|
968
|
+
return formatSearchResults(results.map((item) => ({
|
|
969
|
+
title: item?.title,
|
|
970
|
+
url: item?.url,
|
|
971
|
+
snippet: item?.description
|
|
972
|
+
})));
|
|
973
|
+
}
|
|
974
|
+
|
|
975
|
+
async function searchTavily(query, count, provider) {
|
|
976
|
+
if (!provider?.apiKey) return null;
|
|
977
|
+
const response = await runFetchWithTimeout("https://api.tavily.com/search", {
|
|
978
|
+
method: "POST",
|
|
979
|
+
headers: {
|
|
980
|
+
"Content-Type": "application/json"
|
|
981
|
+
},
|
|
982
|
+
body: JSON.stringify({
|
|
983
|
+
api_key: provider.apiKey,
|
|
984
|
+
query,
|
|
985
|
+
max_results: count,
|
|
986
|
+
include_answer: true,
|
|
987
|
+
search_depth: "basic"
|
|
988
|
+
})
|
|
989
|
+
});
|
|
990
|
+
if (!response.ok) return null;
|
|
991
|
+
const payload = await response.json();
|
|
992
|
+
const answer = String(payload?.answer || "").trim();
|
|
993
|
+
const results = Array.isArray(payload?.results) ? payload.results.slice(0, count) : [];
|
|
994
|
+
if (!answer && results.length === 0) return null;
|
|
995
|
+
const formatted = formatSearchResults(results.map((item) => ({
|
|
996
|
+
title: item?.title,
|
|
997
|
+
url: item?.url,
|
|
998
|
+
snippet: item?.content
|
|
999
|
+
})));
|
|
1000
|
+
return [answer ? `AI Summary: ${answer}` : "", formatted].filter(Boolean).join("\n\n").trim();
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
async function searchExa(query, count, provider) {
|
|
1004
|
+
if (!provider?.apiKey) return null;
|
|
1005
|
+
const response = await runFetchWithTimeout("https://api.exa.ai/search", {
|
|
1006
|
+
method: "POST",
|
|
1007
|
+
headers: {
|
|
1008
|
+
"Content-Type": "application/json",
|
|
1009
|
+
"x-api-key": provider.apiKey
|
|
1010
|
+
},
|
|
1011
|
+
body: JSON.stringify({
|
|
1012
|
+
query,
|
|
1013
|
+
numResults: count,
|
|
1014
|
+
type: "auto",
|
|
1015
|
+
contents: {
|
|
1016
|
+
text: {
|
|
1017
|
+
maxCharacters: 500
|
|
1018
|
+
}
|
|
1019
|
+
}
|
|
1020
|
+
})
|
|
1021
|
+
});
|
|
1022
|
+
if (!response.ok) return null;
|
|
1023
|
+
const payload = await response.json();
|
|
1024
|
+
const results = Array.isArray(payload?.results) ? payload.results.slice(0, count) : [];
|
|
1025
|
+
if (results.length === 0) return null;
|
|
1026
|
+
return formatSearchResults(results.map((item) => ({
|
|
1027
|
+
title: item?.title,
|
|
1028
|
+
url: item?.url,
|
|
1029
|
+
snippet: item?.text || item?.snippet
|
|
1030
|
+
})));
|
|
1031
|
+
}
|
|
1032
|
+
|
|
1033
|
+
async function searchSearXng(query, count, provider) {
|
|
1034
|
+
if (!provider?.url) return null;
|
|
1035
|
+
const url = `${provider.url}/search?q=${encodeURIComponent(query)}&format=json&categories=general&language=auto`;
|
|
1036
|
+
const response = await runFetchWithTimeout(url, {
|
|
1037
|
+
headers: {
|
|
1038
|
+
Accept: "application/json",
|
|
1039
|
+
"User-Agent": "llm-router"
|
|
1040
|
+
}
|
|
1041
|
+
});
|
|
1042
|
+
if (!response.ok) return null;
|
|
1043
|
+
const payload = await response.json();
|
|
1044
|
+
const results = Array.isArray(payload?.results) ? payload.results.slice(0, count) : [];
|
|
1045
|
+
if (results.length === 0) return null;
|
|
1046
|
+
return formatSearchResults(results.map((item) => ({
|
|
1047
|
+
title: item?.title,
|
|
1048
|
+
url: item?.url,
|
|
1049
|
+
snippet: item?.content
|
|
1050
|
+
})));
|
|
1051
|
+
}
|
|
1052
|
+
|
|
1053
|
+
async function searchGoogleNews(query, count) {
|
|
1054
|
+
const url = `https://news.google.com/rss/search?q=${encodeURIComponent(query)}&hl=en-US&gl=US&ceid=US:en`;
|
|
1055
|
+
const response = await runFetchWithTimeout(url, {
|
|
1056
|
+
headers: {
|
|
1057
|
+
"User-Agent": "llm-router"
|
|
1058
|
+
}
|
|
1059
|
+
});
|
|
1060
|
+
if (!response.ok) return null;
|
|
1061
|
+
const xml = await response.text();
|
|
1062
|
+
const matches = [...xml.matchAll(/<item>([\s\S]*?)<\/item>/g)].slice(0, count);
|
|
1063
|
+
if (matches.length === 0) return null;
|
|
1064
|
+
const results = matches.map((match) => {
|
|
1065
|
+
const block = match[1] || "";
|
|
1066
|
+
const title = stripHtml((block.match(/<title>([\s\S]*?)<\/title>/i) || [])[1] || "");
|
|
1067
|
+
const urlValue = String((block.match(/<link>([\s\S]*?)<\/link>/i) || [])[1] || "").trim();
|
|
1068
|
+
const source = stripHtml((block.match(/<source[^>]*>([\s\S]*?)<\/source>/i) || [])[1] || "");
|
|
1069
|
+
const published = stripHtml((block.match(/<pubDate>([\s\S]*?)<\/pubDate>/i) || [])[1] || "");
|
|
1070
|
+
return {
|
|
1071
|
+
title,
|
|
1072
|
+
url: urlValue,
|
|
1073
|
+
snippet: [source, published].filter(Boolean).join(" - ")
|
|
1074
|
+
};
|
|
1075
|
+
}).filter((entry) => entry.title || entry.url);
|
|
1076
|
+
if (results.length === 0) return null;
|
|
1077
|
+
return formatSearchResults(results);
|
|
1078
|
+
}
|
|
1079
|
+
|
|
1080
|
+
async function searchDuckDuckGoHtml(query, count) {
|
|
1081
|
+
const response = await runFetchWithTimeout("https://html.duckduckgo.com/html/", {
|
|
1082
|
+
method: "POST",
|
|
1083
|
+
headers: {
|
|
1084
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
1085
|
+
Accept: "text/html",
|
|
1086
|
+
"User-Agent": "Mozilla/5.0"
|
|
1087
|
+
},
|
|
1088
|
+
body: `q=${encodeURIComponent(query)}`
|
|
1089
|
+
});
|
|
1090
|
+
if (!response.ok) return null;
|
|
1091
|
+
const html = await response.text();
|
|
1092
|
+
const linkMatches = [...html.matchAll(/class="result__a"[^>]*href="([^"]+)"[^>]*>([\s\S]*?)<\/a>/g)].slice(0, count);
|
|
1093
|
+
if (linkMatches.length === 0) return null;
|
|
1094
|
+
const snippetMatches = [...html.matchAll(/class="result__snippet"[^>]*>([\s\S]*?)<\/a>/g)];
|
|
1095
|
+
const results = linkMatches.map((match, index) => ({
|
|
1096
|
+
title: stripHtml(match[2]),
|
|
1097
|
+
url: decodeDuckDuckGoRedirect(match[1]),
|
|
1098
|
+
snippet: stripHtml(snippetMatches[index]?.[1] || "")
|
|
1099
|
+
}));
|
|
1100
|
+
return formatSearchResults(results);
|
|
1101
|
+
}
|
|
1102
|
+
|
|
1103
|
+
async function searchBing(query, count) {
|
|
1104
|
+
const url = `https://www.bing.com/search?q=${encodeURIComponent(query)}&count=${count}`;
|
|
1105
|
+
const response = await runFetchWithTimeout(url, {
|
|
1106
|
+
headers: {
|
|
1107
|
+
Accept: "text/html",
|
|
1108
|
+
"Accept-Language": "en-US,en;q=0.9",
|
|
1109
|
+
"User-Agent": "Mozilla/5.0"
|
|
1110
|
+
}
|
|
1111
|
+
});
|
|
1112
|
+
if (!response.ok) return null;
|
|
1113
|
+
const html = await response.text();
|
|
1114
|
+
const matches = [...html.matchAll(/<li class="b_algo"[^>]*>([\s\S]*?)<\/li>/g)].slice(0, count);
|
|
1115
|
+
if (matches.length === 0) return null;
|
|
1116
|
+
const results = matches.map((match) => {
|
|
1117
|
+
const block = match[1] || "";
|
|
1118
|
+
const linkMatch = block.match(/<a[^>]*href="(https?:\/\/[^"]+)"[^>]*>([\s\S]*?)<\/a>/i);
|
|
1119
|
+
const snippetMatch = block.match(/<p[^>]*>([\s\S]*?)<\/p>/i);
|
|
1120
|
+
if (!linkMatch) return null;
|
|
1121
|
+
return {
|
|
1122
|
+
title: stripHtml(linkMatch[2]),
|
|
1123
|
+
url: linkMatch[1],
|
|
1124
|
+
snippet: stripHtml(snippetMatch?.[1] || "")
|
|
1125
|
+
};
|
|
1126
|
+
}).filter(Boolean);
|
|
1127
|
+
if (results.length === 0) return null;
|
|
1128
|
+
return formatSearchResults(results);
|
|
1129
|
+
}
|
|
1130
|
+
|
|
1131
|
+
async function searchDuckDuckGoLite(query, count) {
|
|
1132
|
+
const url = `https://lite.duckduckgo.com/lite/?q=${encodeURIComponent(query)}`;
|
|
1133
|
+
const response = await runFetchWithTimeout(url, {
|
|
1134
|
+
headers: {
|
|
1135
|
+
Accept: "text/html",
|
|
1136
|
+
"User-Agent": "Mozilla/5.0"
|
|
1137
|
+
}
|
|
1138
|
+
});
|
|
1139
|
+
if (!response.ok) return null;
|
|
1140
|
+
const html = await response.text();
|
|
1141
|
+
const linkMatches = [...html.matchAll(/href="([^"]+)"[^>]*class='result-link'>([\s\S]*?)<\/a>/g)].slice(0, count);
|
|
1142
|
+
if (linkMatches.length === 0) return null;
|
|
1143
|
+
const snippetMatches = [...html.matchAll(/class='result-snippet'>([\s\S]*?)<\/td>/g)];
|
|
1144
|
+
const results = linkMatches.map((match, index) => ({
|
|
1145
|
+
title: stripHtml(match[2]),
|
|
1146
|
+
url: decodeDuckDuckGoRedirect(match[1]),
|
|
1147
|
+
snippet: stripHtml(snippetMatches[index]?.[1] || "")
|
|
1148
|
+
}));
|
|
1149
|
+
return formatSearchResults(results);
|
|
1150
|
+
}
|
|
1151
|
+
|
|
1152
|
+
async function searchHostedProviderRoute(query, count, provider, runtimeConfig = {}, env = {}) {
|
|
1153
|
+
void count;
|
|
1154
|
+
if (!isHostedSearchProvider(provider)) return null;
|
|
1155
|
+
const result = await runHostedSearchProviderQuery(provider, query, runtimeConfig, env);
|
|
1156
|
+
return String(result?.text || "").trim() || null;
|
|
1157
|
+
}
|
|
1158
|
+
|
|
1159
|
+
const backendSearchers = Object.freeze({
|
|
1160
|
+
brave: searchBrave,
|
|
1161
|
+
tavily: searchTavily,
|
|
1162
|
+
exa: searchExa,
|
|
1163
|
+
searxng: searchSearXng,
|
|
1164
|
+
gnews: searchGoogleNews,
|
|
1165
|
+
ddghtml: searchDuckDuckGoHtml,
|
|
1166
|
+
bing: searchBing,
|
|
1167
|
+
ddglite: searchDuckDuckGoLite
|
|
1168
|
+
});
|
|
1169
|
+
|
|
1170
|
+
export async function executeAmpWebSearch(query, runtimeConfig = {}, env = {}, options = {}) {
|
|
1171
|
+
const normalizedQuery = String(query || "").trim();
|
|
1172
|
+
if (!normalizedQuery) {
|
|
1173
|
+
return {
|
|
1174
|
+
text: "[Empty search query]",
|
|
1175
|
+
backend: "",
|
|
1176
|
+
tag: ""
|
|
1177
|
+
};
|
|
1178
|
+
}
|
|
1179
|
+
|
|
1180
|
+
const snapshot = await buildAmpWebSearchSnapshot(runtimeConfig, {
|
|
1181
|
+
env,
|
|
1182
|
+
stateStore: options.stateStore,
|
|
1183
|
+
now: options.now
|
|
1184
|
+
});
|
|
1185
|
+
const stateStore = resolveSearchStateStore(options.stateStore);
|
|
1186
|
+
const configuredProviders = snapshot.providers.filter((provider) => provider.ready);
|
|
1187
|
+
const rankedConfiguredProviders = await rankConfiguredSearchProviders(
|
|
1188
|
+
configuredProviders,
|
|
1189
|
+
snapshot.strategy,
|
|
1190
|
+
stateStore,
|
|
1191
|
+
options.now
|
|
1192
|
+
);
|
|
1193
|
+
|
|
1194
|
+
for (const providerStatus of rankedConfiguredProviders) {
|
|
1195
|
+
try {
|
|
1196
|
+
const searcher = isHostedSearchProvider(providerStatus)
|
|
1197
|
+
? searchHostedProviderRoute
|
|
1198
|
+
: backendSearchers[providerStatus.id];
|
|
1199
|
+
if (typeof searcher !== "function") continue;
|
|
1200
|
+
const providerCount = resolveSearchProviderCount(providerStatus, snapshot.count);
|
|
1201
|
+
const result = isHostedSearchProvider(providerStatus)
|
|
1202
|
+
? await searcher(normalizedQuery, providerCount, providerStatus, runtimeConfig, env)
|
|
1203
|
+
: await searcher(normalizedQuery, providerCount, providerStatus);
|
|
1204
|
+
if (!result || !String(result).trim()) continue;
|
|
1205
|
+
await consumeCandidateRateLimits(stateStore, providerStatus.evaluation, {
|
|
1206
|
+
amount: 1,
|
|
1207
|
+
now: options.now
|
|
1208
|
+
});
|
|
1209
|
+
const refreshedSnapshot = await buildAmpWebSearchSnapshot(runtimeConfig, {
|
|
1210
|
+
env,
|
|
1211
|
+
stateStore,
|
|
1212
|
+
now: options.now
|
|
1213
|
+
});
|
|
1214
|
+
const refreshedProvider = refreshedSnapshot.providers.find((entry) => entry.id === providerStatus.id) || providerStatus;
|
|
1215
|
+
return {
|
|
1216
|
+
text: String(result).trim(),
|
|
1217
|
+
backend: providerStatus.id,
|
|
1218
|
+
providerId: providerStatus.id,
|
|
1219
|
+
tag: buildSearchTag(refreshedProvider)
|
|
1220
|
+
};
|
|
1221
|
+
} catch {
|
|
1222
|
+
continue;
|
|
1223
|
+
}
|
|
1224
|
+
}
|
|
1225
|
+
|
|
1226
|
+
for (const backend of FREE_FALLBACK_BACKENDS) {
|
|
1227
|
+
const searcher = backendSearchers[backend];
|
|
1228
|
+
if (typeof searcher !== "function") continue;
|
|
1229
|
+
try {
|
|
1230
|
+
const result = await searcher(normalizedQuery, snapshot.count);
|
|
1231
|
+
if (!result || !String(result).trim()) continue;
|
|
1232
|
+
return {
|
|
1233
|
+
text: String(result).trim(),
|
|
1234
|
+
backend,
|
|
1235
|
+
providerId: backend,
|
|
1236
|
+
tag: backend
|
|
1237
|
+
};
|
|
1238
|
+
} catch {
|
|
1239
|
+
continue;
|
|
1240
|
+
}
|
|
1241
|
+
}
|
|
1242
|
+
|
|
1243
|
+
return {
|
|
1244
|
+
text: "[No search results available]",
|
|
1245
|
+
backend: "",
|
|
1246
|
+
tag: ""
|
|
1247
|
+
};
|
|
1248
|
+
}
|
|
1249
|
+
|
|
1250
|
+
export function shouldInterceptAmpWebSearch({ clientType, originalBody, runtimeConfig, env }) {
|
|
1251
|
+
const tools = Array.isArray(originalBody?.tools) ? originalBody.tools : [];
|
|
1252
|
+
const requestedToolNames = dedupeStrings(tools.map((tool) => getToolName(tool)).filter(Boolean));
|
|
1253
|
+
if (requestedToolNames.length === 0) {
|
|
1254
|
+
return false;
|
|
1255
|
+
}
|
|
1256
|
+
const readyProviders = resolveAmpWebSearchConfig(runtimeConfig, env).providers.filter((provider) => {
|
|
1257
|
+
if (!isSearchProviderConfigured(provider)) return false;
|
|
1258
|
+
if (!isHostedSearchProvider(provider)) return true;
|
|
1259
|
+
const resolvedRoute = getResolvedHostedSearchRoute(runtimeConfig, provider);
|
|
1260
|
+
return Boolean(resolvedRoute && supportsResolvedHostedSearchRoute(resolvedRoute.provider, resolvedRoute.model));
|
|
1261
|
+
});
|
|
1262
|
+
if (readyProviders.length === 0) {
|
|
1263
|
+
return clientType === "amp" && requestedToolNames.includes(READ_WEB_PAGE_TOOL_NAME);
|
|
1264
|
+
}
|
|
1265
|
+
if (clientType === "amp") {
|
|
1266
|
+
if (requestedToolNames.includes(READ_WEB_PAGE_TOOL_NAME)) return true;
|
|
1267
|
+
return true;
|
|
1268
|
+
}
|
|
1269
|
+
return true;
|
|
1270
|
+
}
|
|
1271
|
+
|
|
1272
|
+
export function rewriteProviderBodyForAmpWebSearch(providerBody, targetFormat) {
|
|
1273
|
+
const tools = Array.isArray(providerBody?.tools) ? providerBody.tools : [];
|
|
1274
|
+
if (tools.length === 0) {
|
|
1275
|
+
return {
|
|
1276
|
+
providerBody,
|
|
1277
|
+
hasWebSearch: false
|
|
1278
|
+
};
|
|
1279
|
+
}
|
|
1280
|
+
|
|
1281
|
+
const interceptedToolNames = new Set();
|
|
1282
|
+
const nextTools = [];
|
|
1283
|
+
for (const tool of tools) {
|
|
1284
|
+
if (!tool || typeof tool !== "object") {
|
|
1285
|
+
nextTools.push(tool);
|
|
1286
|
+
continue;
|
|
1287
|
+
}
|
|
1288
|
+
const toolName = getToolName(tool);
|
|
1289
|
+
if (toolName) {
|
|
1290
|
+
interceptedToolNames.add(toolName);
|
|
1291
|
+
continue;
|
|
1292
|
+
}
|
|
1293
|
+
nextTools.push(tool);
|
|
1294
|
+
}
|
|
1295
|
+
|
|
1296
|
+
if (interceptedToolNames.size === 0) {
|
|
1297
|
+
return {
|
|
1298
|
+
providerBody,
|
|
1299
|
+
hasWebSearch: false
|
|
1300
|
+
};
|
|
1301
|
+
}
|
|
1302
|
+
|
|
1303
|
+
if (targetFormat === FORMATS.OPENAI) {
|
|
1304
|
+
if (interceptedToolNames.has(SEARCH_TOOL_NAME)) nextTools.push(OPENAI_WEB_SEARCH_TOOL);
|
|
1305
|
+
if (interceptedToolNames.has(READ_WEB_PAGE_TOOL_NAME)) nextTools.push(OPENAI_READ_WEB_PAGE_TOOL);
|
|
1306
|
+
} else if (targetFormat === FORMATS.CLAUDE) {
|
|
1307
|
+
if (interceptedToolNames.has(SEARCH_TOOL_NAME)) nextTools.push(CLAUDE_WEB_SEARCH_TOOL);
|
|
1308
|
+
if (interceptedToolNames.has(READ_WEB_PAGE_TOOL_NAME)) nextTools.push(CLAUDE_READ_WEB_PAGE_TOOL);
|
|
1309
|
+
}
|
|
1310
|
+
|
|
1311
|
+
return {
|
|
1312
|
+
hasWebSearch: true,
|
|
1313
|
+
providerBody: {
|
|
1314
|
+
...providerBody,
|
|
1315
|
+
tools: nextTools
|
|
1316
|
+
}
|
|
1317
|
+
};
|
|
1318
|
+
}
|
|
1319
|
+
|
|
1320
|
+
function extractOpenAIChatProbe(payload) {
|
|
1321
|
+
const choice = Array.isArray(payload?.choices) ? payload.choices[0] : null;
|
|
1322
|
+
const message = choice?.message && typeof choice.message === "object" ? choice.message : null;
|
|
1323
|
+
const toolCalls = Array.isArray(message?.tool_calls)
|
|
1324
|
+
? message.tool_calls.filter((item) => hasInterceptableToolName(item?.function?.name))
|
|
1325
|
+
: [];
|
|
1326
|
+
|
|
1327
|
+
return {
|
|
1328
|
+
hasWebSearchCalls: toolCalls.length > 0,
|
|
1329
|
+
toolCalls,
|
|
1330
|
+
assistantMessage: message ? {
|
|
1331
|
+
role: "assistant",
|
|
1332
|
+
content: typeof message.content === "string" ? message.content : (message.content || ""),
|
|
1333
|
+
...(Array.isArray(message.tool_calls) && message.tool_calls.length > 0 ? { tool_calls: message.tool_calls } : {})
|
|
1334
|
+
} : null
|
|
1335
|
+
};
|
|
1336
|
+
}
|
|
1337
|
+
|
|
1338
|
+
function normalizeResponseInput(input) {
|
|
1339
|
+
if (Array.isArray(input)) return input.slice();
|
|
1340
|
+
const normalized = String(input || "").trim();
|
|
1341
|
+
if (!normalized) return [];
|
|
1342
|
+
return [{
|
|
1343
|
+
type: "message",
|
|
1344
|
+
role: "user",
|
|
1345
|
+
content: [{
|
|
1346
|
+
type: "input_text",
|
|
1347
|
+
text: normalized
|
|
1348
|
+
}]
|
|
1349
|
+
}];
|
|
1350
|
+
}
|
|
1351
|
+
|
|
1352
|
+
function extractOpenAIResponsesProbe(payload) {
|
|
1353
|
+
const output = Array.isArray(payload?.output) ? payload.output : [];
|
|
1354
|
+
const toolCalls = output.filter((item) => item?.type === "function_call" && hasInterceptableToolName(item?.name));
|
|
1355
|
+
const assistantInputItems = output
|
|
1356
|
+
.filter((item) => item && item.type !== "reasoning")
|
|
1357
|
+
.map((item) => {
|
|
1358
|
+
if (item.type === "message") {
|
|
1359
|
+
return {
|
|
1360
|
+
type: "message",
|
|
1361
|
+
role: item.role || "assistant",
|
|
1362
|
+
content: Array.isArray(item.content) ? item.content : []
|
|
1363
|
+
};
|
|
1364
|
+
}
|
|
1365
|
+
if (item.type === "function_call") {
|
|
1366
|
+
return {
|
|
1367
|
+
type: "function_call",
|
|
1368
|
+
call_id: item.call_id || item.id || `call_${Date.now()}`,
|
|
1369
|
+
name: item.name || SEARCH_TOOL_NAME,
|
|
1370
|
+
arguments: typeof item.arguments === "string" ? item.arguments : JSON.stringify(item.arguments || {})
|
|
1371
|
+
};
|
|
1372
|
+
}
|
|
1373
|
+
return null;
|
|
1374
|
+
})
|
|
1375
|
+
.filter(Boolean);
|
|
1376
|
+
|
|
1377
|
+
return {
|
|
1378
|
+
hasWebSearchCalls: toolCalls.length > 0,
|
|
1379
|
+
toolCalls,
|
|
1380
|
+
assistantInputItems
|
|
1381
|
+
};
|
|
1382
|
+
}
|
|
1383
|
+
|
|
1384
|
+
function extractClaudeProbe(payload) {
|
|
1385
|
+
const content = Array.isArray(payload?.content) ? payload.content : [];
|
|
1386
|
+
const assistantContent = content.filter((item) => item?.type !== "thinking" && item?.type !== "redacted_thinking");
|
|
1387
|
+
const toolCalls = assistantContent.filter((item) => item?.type === "tool_use" && hasInterceptableToolName(item?.name));
|
|
1388
|
+
|
|
1389
|
+
return {
|
|
1390
|
+
hasWebSearchCalls: toolCalls.length > 0,
|
|
1391
|
+
toolCalls,
|
|
1392
|
+
assistantContent
|
|
1393
|
+
};
|
|
1394
|
+
}
|
|
1395
|
+
|
|
1396
|
+
export function extractAmpWebSearchProbe(payload, { targetFormat, requestKind }) {
|
|
1397
|
+
if (targetFormat === FORMATS.CLAUDE) {
|
|
1398
|
+
return extractClaudeProbe(payload);
|
|
1399
|
+
}
|
|
1400
|
+
if (targetFormat === FORMATS.OPENAI && requestKind === "responses") {
|
|
1401
|
+
return extractOpenAIResponsesProbe(payload);
|
|
1402
|
+
}
|
|
1403
|
+
if (targetFormat === FORMATS.OPENAI) {
|
|
1404
|
+
return extractOpenAIChatProbe(payload);
|
|
1405
|
+
}
|
|
1406
|
+
return {
|
|
1407
|
+
hasWebSearchCalls: false,
|
|
1408
|
+
toolCalls: []
|
|
1409
|
+
};
|
|
1410
|
+
}
|
|
1411
|
+
|
|
1412
|
+
function extractQueryFromToolCall(toolCall) {
|
|
1413
|
+
if (!toolCall || typeof toolCall !== "object") return "";
|
|
1414
|
+
if (typeof toolCall?.input?.query === "string") return toolCall.input.query.trim();
|
|
1415
|
+
const parsedArguments = parseJsonSafely(toolCall?.arguments, {});
|
|
1416
|
+
if (typeof parsedArguments?.query === "string") return parsedArguments.query.trim();
|
|
1417
|
+
const parsedFunctionArguments = parseJsonSafely(toolCall?.function?.arguments, {});
|
|
1418
|
+
if (typeof parsedFunctionArguments?.query === "string") return parsedFunctionArguments.query.trim();
|
|
1419
|
+
if (typeof toolCall?.function?.query === "string") return toolCall.function.query.trim();
|
|
1420
|
+
return "";
|
|
1421
|
+
}
|
|
1422
|
+
|
|
1423
|
+
function extractUrlFromToolCall(toolCall) {
|
|
1424
|
+
if (!toolCall || typeof toolCall !== "object") return "";
|
|
1425
|
+
for (const candidate of [
|
|
1426
|
+
toolCall?.input?.url,
|
|
1427
|
+
toolCall?.input?.uri,
|
|
1428
|
+
toolCall?.input?.href
|
|
1429
|
+
]) {
|
|
1430
|
+
if (typeof candidate === "string" && candidate.trim()) return candidate.trim();
|
|
1431
|
+
}
|
|
1432
|
+
const parsedArguments = parseJsonSafely(toolCall?.arguments, {});
|
|
1433
|
+
for (const candidate of [parsedArguments?.url, parsedArguments?.uri, parsedArguments?.href]) {
|
|
1434
|
+
if (typeof candidate === "string" && candidate.trim()) return candidate.trim();
|
|
1435
|
+
}
|
|
1436
|
+
const parsedFunctionArguments = parseJsonSafely(toolCall?.function?.arguments, {});
|
|
1437
|
+
for (const candidate of [parsedFunctionArguments?.url, parsedFunctionArguments?.uri, parsedFunctionArguments?.href]) {
|
|
1438
|
+
if (typeof candidate === "string" && candidate.trim()) return candidate.trim();
|
|
1439
|
+
}
|
|
1440
|
+
for (const candidate of [toolCall?.function?.url, toolCall?.function?.uri, toolCall?.function?.href]) {
|
|
1441
|
+
if (typeof candidate === "string" && candidate.trim()) return candidate.trim();
|
|
1442
|
+
}
|
|
1443
|
+
return "";
|
|
1444
|
+
}
|
|
1445
|
+
|
|
1446
|
+
function getToolCallName(toolCall) {
|
|
1447
|
+
return getToolName(toolCall);
|
|
1448
|
+
}
|
|
1449
|
+
|
|
1450
|
+
function buildToolResultText(query, searchText) {
|
|
1451
|
+
const normalizedQuery = String(query || "").trim();
|
|
1452
|
+
const normalizedResults = String(searchText || "").trim() || "[No search results available]";
|
|
1453
|
+
if (!normalizedQuery) return normalizedResults;
|
|
1454
|
+
return `Web search results for "${normalizedQuery}":\n\n${normalizedResults}`;
|
|
1455
|
+
}
|
|
1456
|
+
|
|
1457
|
+
function buildReadWebPageResultText(url, pageText) {
|
|
1458
|
+
const normalizedUrl = String(url || "").trim();
|
|
1459
|
+
const normalizedPageText = String(pageText || "").trim() || "[Unable to extract web page content]";
|
|
1460
|
+
if (!normalizedUrl) return normalizedPageText;
|
|
1461
|
+
return `Web page content from "${normalizedUrl}":\n\n${normalizedPageText}`;
|
|
1462
|
+
}
|
|
1463
|
+
|
|
1464
|
+
async function executeAmpReadWebPage(url) {
|
|
1465
|
+
const normalizedUrl = String(url || "").trim();
|
|
1466
|
+
if (!normalizedUrl) {
|
|
1467
|
+
return {
|
|
1468
|
+
text: "[Missing URL for read_web_page]",
|
|
1469
|
+
providerId: READ_WEB_PAGE_TOOL_NAME,
|
|
1470
|
+
backend: READ_WEB_PAGE_TOOL_NAME,
|
|
1471
|
+
tag: READ_WEB_PAGE_TOOL_NAME
|
|
1472
|
+
};
|
|
1473
|
+
}
|
|
1474
|
+
|
|
1475
|
+
let parsedUrl;
|
|
1476
|
+
try {
|
|
1477
|
+
parsedUrl = new URL(normalizedUrl);
|
|
1478
|
+
} catch {
|
|
1479
|
+
return {
|
|
1480
|
+
text: `[Invalid URL for read_web_page: ${normalizedUrl}]`,
|
|
1481
|
+
providerId: READ_WEB_PAGE_TOOL_NAME,
|
|
1482
|
+
backend: READ_WEB_PAGE_TOOL_NAME,
|
|
1483
|
+
tag: READ_WEB_PAGE_TOOL_NAME
|
|
1484
|
+
};
|
|
1485
|
+
}
|
|
1486
|
+
|
|
1487
|
+
if (!["http:", "https:"].includes(parsedUrl.protocol)) {
|
|
1488
|
+
return {
|
|
1489
|
+
text: `[Unsupported URL protocol for read_web_page: ${parsedUrl.protocol}]`,
|
|
1490
|
+
providerId: READ_WEB_PAGE_TOOL_NAME,
|
|
1491
|
+
backend: READ_WEB_PAGE_TOOL_NAME,
|
|
1492
|
+
tag: READ_WEB_PAGE_TOOL_NAME
|
|
1493
|
+
};
|
|
1494
|
+
}
|
|
1495
|
+
|
|
1496
|
+
try {
|
|
1497
|
+
const response = await runFetchWithTimeout(parsedUrl.toString(), {
|
|
1498
|
+
headers: {
|
|
1499
|
+
Accept: "text/html,application/xhtml+xml,text/plain,application/json;q=0.9,*/*;q=0.8",
|
|
1500
|
+
"User-Agent": "llm-router"
|
|
1501
|
+
}
|
|
1502
|
+
});
|
|
1503
|
+
if (!response.ok) {
|
|
1504
|
+
return {
|
|
1505
|
+
text: `[Failed to read web page: ${await readSearchProviderError(response)}]`,
|
|
1506
|
+
providerId: READ_WEB_PAGE_TOOL_NAME,
|
|
1507
|
+
backend: READ_WEB_PAGE_TOOL_NAME,
|
|
1508
|
+
tag: READ_WEB_PAGE_TOOL_NAME
|
|
1509
|
+
};
|
|
1510
|
+
}
|
|
1511
|
+
|
|
1512
|
+
const contentType = String(response.headers.get("content-type") || "").trim();
|
|
1513
|
+
const bodyText = await response.text();
|
|
1514
|
+
const formattedText = looksLikeHtml(contentType, bodyText)
|
|
1515
|
+
? formatReadWebPageHtml(parsedUrl.toString(), bodyText)
|
|
1516
|
+
: formatReadWebPageBody(parsedUrl.toString(), bodyText, contentType);
|
|
1517
|
+
|
|
1518
|
+
return {
|
|
1519
|
+
text: formattedText,
|
|
1520
|
+
providerId: READ_WEB_PAGE_TOOL_NAME,
|
|
1521
|
+
backend: READ_WEB_PAGE_TOOL_NAME,
|
|
1522
|
+
tag: READ_WEB_PAGE_TOOL_NAME
|
|
1523
|
+
};
|
|
1524
|
+
} catch (error) {
|
|
1525
|
+
return {
|
|
1526
|
+
text: `[Failed to read web page: ${error instanceof Error ? error.message : String(error)}]`,
|
|
1527
|
+
providerId: READ_WEB_PAGE_TOOL_NAME,
|
|
1528
|
+
backend: READ_WEB_PAGE_TOOL_NAME,
|
|
1529
|
+
tag: READ_WEB_PAGE_TOOL_NAME
|
|
1530
|
+
};
|
|
1531
|
+
}
|
|
1532
|
+
}
|
|
1533
|
+
|
|
1534
|
+
async function executeAmpInterceptedToolCall(toolCall, runtimeConfig, env, options = {}) {
|
|
1535
|
+
const toolName = getToolCallName(toolCall);
|
|
1536
|
+
if (toolName === READ_WEB_PAGE_TOOL_NAME) {
|
|
1537
|
+
return executeAmpReadWebPage(extractUrlFromToolCall(toolCall));
|
|
1538
|
+
}
|
|
1539
|
+
return executeAmpWebSearch(
|
|
1540
|
+
extractQueryFromToolCall(toolCall),
|
|
1541
|
+
runtimeConfig,
|
|
1542
|
+
env,
|
|
1543
|
+
options
|
|
1544
|
+
);
|
|
1545
|
+
}
|
|
1546
|
+
|
|
1547
|
+
function mergeClaudeSystemInstruction(system, instruction) {
|
|
1548
|
+
if (typeof system === "string" && system.trim()) {
|
|
1549
|
+
return `${system.trim()}\n\n${instruction}`;
|
|
1550
|
+
}
|
|
1551
|
+
if (Array.isArray(system) && system.length > 0) {
|
|
1552
|
+
return [
|
|
1553
|
+
...system,
|
|
1554
|
+
{
|
|
1555
|
+
type: "text",
|
|
1556
|
+
text: instruction
|
|
1557
|
+
}
|
|
1558
|
+
];
|
|
1559
|
+
}
|
|
1560
|
+
return instruction;
|
|
1561
|
+
}
|
|
1562
|
+
|
|
1563
|
+
function mergeOpenAIInstructions(originalInstructions, instruction) {
|
|
1564
|
+
const existing = String(originalInstructions || "").trim();
|
|
1565
|
+
return existing ? `${existing}\n\n${instruction}` : instruction;
|
|
1566
|
+
}
|
|
1567
|
+
|
|
1568
|
+
export function buildAmpWebSearchFollowUp(providerBody, probePayload, probe, searchResultsByCall, { targetFormat, requestKind, stream }) {
|
|
1569
|
+
const toolCalls = Array.isArray(probe?.toolCalls) ? probe.toolCalls : [];
|
|
1570
|
+
const normalizedToolResults = Array.isArray(searchResultsByCall)
|
|
1571
|
+
? searchResultsByCall
|
|
1572
|
+
: [];
|
|
1573
|
+
|
|
1574
|
+
if (targetFormat === FORMATS.CLAUDE) {
|
|
1575
|
+
const toolResults = toolCalls.map((toolCall, index) => ({
|
|
1576
|
+
type: "tool_result",
|
|
1577
|
+
tool_use_id: toolCall.id || `tool_${index + 1}`,
|
|
1578
|
+
content: getToolCallName(toolCall) === READ_WEB_PAGE_TOOL_NAME
|
|
1579
|
+
? buildReadWebPageResultText(
|
|
1580
|
+
extractUrlFromToolCall(toolCall),
|
|
1581
|
+
normalizedToolResults[index]?.text
|
|
1582
|
+
)
|
|
1583
|
+
: buildToolResultText(
|
|
1584
|
+
extractQueryFromToolCall(toolCall),
|
|
1585
|
+
normalizedToolResults[index]?.text
|
|
1586
|
+
)
|
|
1587
|
+
}));
|
|
1588
|
+
return {
|
|
1589
|
+
...providerBody,
|
|
1590
|
+
stream: Boolean(stream),
|
|
1591
|
+
system: mergeClaudeSystemInstruction(providerBody.system, SEARCH_SYSTEM_INSTRUCTION),
|
|
1592
|
+
messages: [
|
|
1593
|
+
...(Array.isArray(providerBody.messages) ? providerBody.messages : []),
|
|
1594
|
+
{
|
|
1595
|
+
role: "assistant",
|
|
1596
|
+
content: Array.isArray(probe.assistantContent) ? probe.assistantContent : []
|
|
1597
|
+
},
|
|
1598
|
+
{
|
|
1599
|
+
role: "user",
|
|
1600
|
+
content: toolResults
|
|
1601
|
+
}
|
|
1602
|
+
]
|
|
1603
|
+
};
|
|
1604
|
+
}
|
|
1605
|
+
|
|
1606
|
+
if (targetFormat === FORMATS.OPENAI && requestKind === "responses") {
|
|
1607
|
+
const toolOutputs = toolCalls.map((toolCall, index) => ({
|
|
1608
|
+
type: "function_call_output",
|
|
1609
|
+
call_id: toolCall.call_id || toolCall.id || `call_${index + 1}`,
|
|
1610
|
+
output: getToolCallName(toolCall) === READ_WEB_PAGE_TOOL_NAME
|
|
1611
|
+
? buildReadWebPageResultText(
|
|
1612
|
+
extractUrlFromToolCall(toolCall),
|
|
1613
|
+
normalizedToolResults[index]?.text
|
|
1614
|
+
)
|
|
1615
|
+
: buildToolResultText(
|
|
1616
|
+
extractQueryFromToolCall(toolCall),
|
|
1617
|
+
normalizedToolResults[index]?.text
|
|
1618
|
+
)
|
|
1619
|
+
}));
|
|
1620
|
+
return {
|
|
1621
|
+
...providerBody,
|
|
1622
|
+
stream: Boolean(stream),
|
|
1623
|
+
input: [
|
|
1624
|
+
...normalizeResponseInput(providerBody.input),
|
|
1625
|
+
...(Array.isArray(probe.assistantInputItems) ? probe.assistantInputItems : []),
|
|
1626
|
+
...toolOutputs
|
|
1627
|
+
],
|
|
1628
|
+
instructions: mergeOpenAIInstructions(providerBody.instructions, SEARCH_SYSTEM_INSTRUCTION)
|
|
1629
|
+
};
|
|
1630
|
+
}
|
|
1631
|
+
|
|
1632
|
+
const assistantMessage = probe?.assistantMessage && typeof probe.assistantMessage === "object"
|
|
1633
|
+
? probe.assistantMessage
|
|
1634
|
+
: {
|
|
1635
|
+
role: "assistant",
|
|
1636
|
+
content: ""
|
|
1637
|
+
};
|
|
1638
|
+
const toolMessages = toolCalls.map((toolCall, index) => ({
|
|
1639
|
+
role: "tool",
|
|
1640
|
+
tool_call_id: toolCall.id || `call_${index + 1}`,
|
|
1641
|
+
content: getToolCallName(toolCall) === READ_WEB_PAGE_TOOL_NAME
|
|
1642
|
+
? buildReadWebPageResultText(
|
|
1643
|
+
extractUrlFromToolCall(toolCall),
|
|
1644
|
+
normalizedToolResults[index]?.text
|
|
1645
|
+
)
|
|
1646
|
+
: buildToolResultText(
|
|
1647
|
+
extractQueryFromToolCall(toolCall),
|
|
1648
|
+
normalizedToolResults[index]?.text
|
|
1649
|
+
)
|
|
1650
|
+
}));
|
|
1651
|
+
const nextMessages = Array.isArray(providerBody.messages) ? providerBody.messages.slice() : [];
|
|
1652
|
+
const hasLeadingSystem = nextMessages[0]?.role === "system";
|
|
1653
|
+
if (hasLeadingSystem && typeof nextMessages[0].content === "string") {
|
|
1654
|
+
nextMessages[0] = {
|
|
1655
|
+
...nextMessages[0],
|
|
1656
|
+
content: mergeOpenAIInstructions(nextMessages[0].content, SEARCH_SYSTEM_INSTRUCTION)
|
|
1657
|
+
};
|
|
1658
|
+
} else {
|
|
1659
|
+
nextMessages.unshift({
|
|
1660
|
+
role: "system",
|
|
1661
|
+
content: SEARCH_SYSTEM_INSTRUCTION
|
|
1662
|
+
});
|
|
1663
|
+
}
|
|
1664
|
+
nextMessages.push(assistantMessage, ...toolMessages);
|
|
1665
|
+
return {
|
|
1666
|
+
...providerBody,
|
|
1667
|
+
stream: Boolean(stream),
|
|
1668
|
+
messages: nextMessages
|
|
1669
|
+
};
|
|
1670
|
+
}
|
|
1671
|
+
|
|
1672
|
+
export function stripAmpWebSearchFollowUpTools(providerBody) {
|
|
1673
|
+
const nextBody = {
|
|
1674
|
+
...providerBody
|
|
1675
|
+
};
|
|
1676
|
+
delete nextBody.tool_choice;
|
|
1677
|
+
delete nextBody.parallel_tool_calls;
|
|
1678
|
+
delete nextBody.max_tool_calls;
|
|
1679
|
+
if (Array.isArray(nextBody.tools)) {
|
|
1680
|
+
delete nextBody.tools;
|
|
1681
|
+
}
|
|
1682
|
+
return nextBody;
|
|
1683
|
+
}
|
|
1684
|
+
|
|
1685
|
+
function splitSseEventBlocks(rawText) {
|
|
1686
|
+
return String(rawText || "")
|
|
1687
|
+
.replace(/\r\n/g, "\n")
|
|
1688
|
+
.split("\n\n")
|
|
1689
|
+
.map((block) => block.trim())
|
|
1690
|
+
.filter(Boolean);
|
|
1691
|
+
}
|
|
1692
|
+
|
|
1693
|
+
function parseSseDataLines(block) {
|
|
1694
|
+
const dataLines = [];
|
|
1695
|
+
for (const line of String(block || "").split("\n")) {
|
|
1696
|
+
if (!line.startsWith("data:")) continue;
|
|
1697
|
+
dataLines.push(line.slice(5).trimStart());
|
|
1698
|
+
}
|
|
1699
|
+
return dataLines.join("\n").trim();
|
|
1700
|
+
}
|
|
1701
|
+
|
|
1702
|
+
function parseOpenAIChatStreamPayload(rawText) {
|
|
1703
|
+
const toolCalls = [];
|
|
1704
|
+
const toolCallsByIndex = new Map();
|
|
1705
|
+
let responseId = "";
|
|
1706
|
+
let model = "";
|
|
1707
|
+
let content = "";
|
|
1708
|
+
let finishReason = "stop";
|
|
1709
|
+
|
|
1710
|
+
for (const block of splitSseEventBlocks(rawText)) {
|
|
1711
|
+
const dataText = parseSseDataLines(block);
|
|
1712
|
+
if (!dataText || dataText === "[DONE]") continue;
|
|
1713
|
+
|
|
1714
|
+
let payload;
|
|
1715
|
+
try {
|
|
1716
|
+
payload = JSON.parse(dataText);
|
|
1717
|
+
} catch {
|
|
1718
|
+
continue;
|
|
1719
|
+
}
|
|
1720
|
+
|
|
1721
|
+
responseId = responseId || String(payload?.id || "").trim();
|
|
1722
|
+
model = model || String(payload?.model || "").trim();
|
|
1723
|
+
|
|
1724
|
+
for (const choice of (Array.isArray(payload?.choices) ? payload.choices : [])) {
|
|
1725
|
+
const delta = choice?.delta || {};
|
|
1726
|
+
if (typeof delta?.content === "string") {
|
|
1727
|
+
content += delta.content;
|
|
1728
|
+
}
|
|
1729
|
+
|
|
1730
|
+
if (Array.isArray(delta?.tool_calls)) {
|
|
1731
|
+
for (const call of delta.tool_calls) {
|
|
1732
|
+
const callIndex = Number.isFinite(Number(call?.index)) ? Number(call.index) : toolCalls.length;
|
|
1733
|
+
const current = toolCallsByIndex.get(callIndex) || {
|
|
1734
|
+
id: call?.id || `call_${callIndex + 1}`,
|
|
1735
|
+
type: "function",
|
|
1736
|
+
function: {
|
|
1737
|
+
name: "",
|
|
1738
|
+
arguments: ""
|
|
1739
|
+
}
|
|
1740
|
+
};
|
|
1741
|
+
if (call?.id) current.id = call.id;
|
|
1742
|
+
if (call?.function?.name) current.function.name += call.function.name;
|
|
1743
|
+
if (call?.function?.arguments) current.function.arguments += call.function.arguments;
|
|
1744
|
+
toolCallsByIndex.set(callIndex, current);
|
|
1745
|
+
}
|
|
1746
|
+
}
|
|
1747
|
+
|
|
1748
|
+
if (delta?.function_call && typeof delta.function_call === "object") {
|
|
1749
|
+
const current = toolCallsByIndex.get(0) || {
|
|
1750
|
+
id: payload?.id ? `${payload.id}_tool_1` : "call_1",
|
|
1751
|
+
type: "function",
|
|
1752
|
+
function: {
|
|
1753
|
+
name: "",
|
|
1754
|
+
arguments: ""
|
|
1755
|
+
}
|
|
1756
|
+
};
|
|
1757
|
+
if (delta.function_call.name) current.function.name += delta.function_call.name;
|
|
1758
|
+
if (delta.function_call.arguments) current.function.arguments += delta.function_call.arguments;
|
|
1759
|
+
toolCallsByIndex.set(0, current);
|
|
1760
|
+
}
|
|
1761
|
+
|
|
1762
|
+
if (choice?.finish_reason) {
|
|
1763
|
+
finishReason = String(choice.finish_reason).trim() || finishReason;
|
|
1764
|
+
}
|
|
1765
|
+
}
|
|
1766
|
+
}
|
|
1767
|
+
|
|
1768
|
+
const orderedIndexes = [...toolCallsByIndex.keys()].sort((left, right) => left - right);
|
|
1769
|
+
for (const index of orderedIndexes) {
|
|
1770
|
+
toolCalls.push(toolCallsByIndex.get(index));
|
|
1771
|
+
}
|
|
1772
|
+
|
|
1773
|
+
return {
|
|
1774
|
+
id: responseId || `chatcmpl_${Date.now()}`,
|
|
1775
|
+
object: "chat.completion",
|
|
1776
|
+
model: model || "unknown",
|
|
1777
|
+
choices: [
|
|
1778
|
+
{
|
|
1779
|
+
index: 0,
|
|
1780
|
+
message: {
|
|
1781
|
+
role: "assistant",
|
|
1782
|
+
content,
|
|
1783
|
+
...(toolCalls.length > 0 ? { tool_calls: toolCalls } : {})
|
|
1784
|
+
},
|
|
1785
|
+
finish_reason: finishReason
|
|
1786
|
+
}
|
|
1787
|
+
]
|
|
1788
|
+
};
|
|
1789
|
+
}
|
|
1790
|
+
|
|
1791
|
+
function parseOpenAIResponsesStreamPayload(rawText) {
|
|
1792
|
+
let completedResponse = null;
|
|
1793
|
+
let responseId = "";
|
|
1794
|
+
let createdAt = Math.floor(Date.now() / 1000);
|
|
1795
|
+
let model = "";
|
|
1796
|
+
let usage = null;
|
|
1797
|
+
const output = [];
|
|
1798
|
+
const messageItems = new Map();
|
|
1799
|
+
const functionItems = new Map();
|
|
1800
|
+
|
|
1801
|
+
function ensureMessageItem(itemId) {
|
|
1802
|
+
const current = messageItems.get(itemId) || {
|
|
1803
|
+
type: "message",
|
|
1804
|
+
id: itemId,
|
|
1805
|
+
role: "assistant",
|
|
1806
|
+
status: "completed",
|
|
1807
|
+
content: [{ type: "output_text", text: "" }]
|
|
1808
|
+
};
|
|
1809
|
+
messageItems.set(itemId, current);
|
|
1810
|
+
return current;
|
|
1811
|
+
}
|
|
1812
|
+
|
|
1813
|
+
function ensureFunctionItem(itemId, callId = itemId) {
|
|
1814
|
+
const current = functionItems.get(itemId) || {
|
|
1815
|
+
type: "function_call",
|
|
1816
|
+
id: itemId,
|
|
1817
|
+
call_id: callId,
|
|
1818
|
+
name: SEARCH_TOOL_NAME,
|
|
1819
|
+
arguments: "",
|
|
1820
|
+
status: "completed"
|
|
1821
|
+
};
|
|
1822
|
+
functionItems.set(itemId, current);
|
|
1823
|
+
return current;
|
|
1824
|
+
}
|
|
1825
|
+
|
|
1826
|
+
for (const block of splitSseEventBlocks(rawText)) {
|
|
1827
|
+
const dataText = parseSseDataLines(block);
|
|
1828
|
+
if (!dataText || dataText === "[DONE]") continue;
|
|
1829
|
+
|
|
1830
|
+
let payload;
|
|
1831
|
+
try {
|
|
1832
|
+
payload = JSON.parse(dataText);
|
|
1833
|
+
} catch {
|
|
1834
|
+
continue;
|
|
1835
|
+
}
|
|
1836
|
+
|
|
1837
|
+
if (payload?.type === "response.completed" && payload?.response && typeof payload.response === "object") {
|
|
1838
|
+
completedResponse = payload.response;
|
|
1839
|
+
break;
|
|
1840
|
+
}
|
|
1841
|
+
|
|
1842
|
+
if (payload?.response && typeof payload.response === "object") {
|
|
1843
|
+
responseId = responseId || String(payload.response.id || "").trim();
|
|
1844
|
+
model = model || String(payload.response.model || "").trim();
|
|
1845
|
+
if (Number.isFinite(payload.response.created_at)) createdAt = Number(payload.response.created_at);
|
|
1846
|
+
if (payload.response.usage && typeof payload.response.usage === "object") usage = payload.response.usage;
|
|
1847
|
+
}
|
|
1848
|
+
|
|
1849
|
+
if (payload?.type === "response.output_item.added") {
|
|
1850
|
+
const item = payload.item || payload.output_item;
|
|
1851
|
+
if (item?.type === "message") {
|
|
1852
|
+
messageItems.set(item.id || `msg_${messageItems.size + 1}`, {
|
|
1853
|
+
...item,
|
|
1854
|
+
content: Array.isArray(item.content) ? item.content.slice() : []
|
|
1855
|
+
});
|
|
1856
|
+
}
|
|
1857
|
+
if (item?.type === "function_call") {
|
|
1858
|
+
functionItems.set(item.id || `fc_${functionItems.size + 1}`, {
|
|
1859
|
+
...item,
|
|
1860
|
+
arguments: typeof item.arguments === "string" ? item.arguments : JSON.stringify(item.arguments || {}),
|
|
1861
|
+
status: item.status || "completed"
|
|
1862
|
+
});
|
|
1863
|
+
}
|
|
1864
|
+
continue;
|
|
1865
|
+
}
|
|
1866
|
+
|
|
1867
|
+
if (payload?.type === "response.output_text.delta") {
|
|
1868
|
+
const messageItem = ensureMessageItem(String(payload.item_id || "msg_1"));
|
|
1869
|
+
const currentText = typeof messageItem.content?.[0]?.text === "string" ? messageItem.content[0].text : "";
|
|
1870
|
+
messageItem.content = [{ type: "output_text", text: `${currentText}${payload.delta || ""}` }];
|
|
1871
|
+
continue;
|
|
1872
|
+
}
|
|
1873
|
+
|
|
1874
|
+
if (payload?.type === "response.function_call_arguments.delta") {
|
|
1875
|
+
const functionItem = ensureFunctionItem(String(payload.item_id || "fc_1"));
|
|
1876
|
+
functionItem.arguments += String(payload.delta || "");
|
|
1877
|
+
continue;
|
|
1878
|
+
}
|
|
1879
|
+
|
|
1880
|
+
if (payload?.type === "response.output_item.done") {
|
|
1881
|
+
const item = payload.item || payload.output_item;
|
|
1882
|
+
if (item?.type === "message") {
|
|
1883
|
+
messageItems.set(item.id || `msg_${messageItems.size + 1}`, item);
|
|
1884
|
+
}
|
|
1885
|
+
if (item?.type === "function_call") {
|
|
1886
|
+
functionItems.set(item.id || `fc_${functionItems.size + 1}`, {
|
|
1887
|
+
...item,
|
|
1888
|
+
arguments: typeof item.arguments === "string" ? item.arguments : JSON.stringify(item.arguments || {}),
|
|
1889
|
+
status: item.status || "completed"
|
|
1890
|
+
});
|
|
1891
|
+
}
|
|
1892
|
+
}
|
|
1893
|
+
}
|
|
1894
|
+
|
|
1895
|
+
if (completedResponse) return completedResponse;
|
|
1896
|
+
|
|
1897
|
+
for (const item of messageItems.values()) output.push(item);
|
|
1898
|
+
for (const item of functionItems.values()) output.push(item);
|
|
1899
|
+
|
|
1900
|
+
return {
|
|
1901
|
+
id: responseId || `resp_${Date.now()}`,
|
|
1902
|
+
object: "response",
|
|
1903
|
+
created_at: createdAt,
|
|
1904
|
+
model: model || "unknown",
|
|
1905
|
+
status: "completed",
|
|
1906
|
+
output,
|
|
1907
|
+
usage
|
|
1908
|
+
};
|
|
1909
|
+
}
|
|
1910
|
+
|
|
1911
|
+
function parseClaudeStreamPayload(rawText) {
|
|
1912
|
+
let messageId = "";
|
|
1913
|
+
let model = "";
|
|
1914
|
+
let stopReason = "end_turn";
|
|
1915
|
+
let usage = null;
|
|
1916
|
+
const activeBlocks = new Map();
|
|
1917
|
+
const orderedBlocks = [];
|
|
1918
|
+
|
|
1919
|
+
function ensureBlock(index, contentBlock = {}) {
|
|
1920
|
+
if (activeBlocks.has(index)) return activeBlocks.get(index);
|
|
1921
|
+
const blockType = String(contentBlock?.type || "").trim();
|
|
1922
|
+
const block = blockType === "tool_use"
|
|
1923
|
+
? {
|
|
1924
|
+
type: "tool_use",
|
|
1925
|
+
id: contentBlock.id || `tool_${index + 1}`,
|
|
1926
|
+
name: contentBlock.name || SEARCH_TOOL_NAME,
|
|
1927
|
+
input: contentBlock.input && typeof contentBlock.input === "object" ? contentBlock.input : {}
|
|
1928
|
+
}
|
|
1929
|
+
: blockType === "thinking" || blockType === "redacted_thinking"
|
|
1930
|
+
? {
|
|
1931
|
+
type: blockType,
|
|
1932
|
+
thinking: contentBlock.thinking || ""
|
|
1933
|
+
}
|
|
1934
|
+
: {
|
|
1935
|
+
type: "text",
|
|
1936
|
+
text: contentBlock.text || ""
|
|
1937
|
+
};
|
|
1938
|
+
activeBlocks.set(index, block);
|
|
1939
|
+
orderedBlocks[index] = block;
|
|
1940
|
+
return block;
|
|
1941
|
+
}
|
|
1942
|
+
|
|
1943
|
+
for (const block of splitSseEventBlocks(rawText)) {
|
|
1944
|
+
const dataText = parseSseDataLines(block);
|
|
1945
|
+
if (!dataText || dataText === "[DONE]") continue;
|
|
1946
|
+
|
|
1947
|
+
let payload;
|
|
1948
|
+
try {
|
|
1949
|
+
payload = JSON.parse(dataText);
|
|
1950
|
+
} catch {
|
|
1951
|
+
continue;
|
|
1952
|
+
}
|
|
1953
|
+
|
|
1954
|
+
const type = String(payload?.type || "").trim();
|
|
1955
|
+
if (!type) continue;
|
|
1956
|
+
|
|
1957
|
+
if (type === "message_start") {
|
|
1958
|
+
const message = payload.message || {};
|
|
1959
|
+
messageId = messageId || String(message.id || "").trim();
|
|
1960
|
+
model = model || String(message.model || "").trim();
|
|
1961
|
+
if (message.usage && typeof message.usage === "object") {
|
|
1962
|
+
usage = {
|
|
1963
|
+
input_tokens: Number(message.usage.input_tokens) || 0,
|
|
1964
|
+
output_tokens: Number(message.usage.output_tokens) || 0
|
|
1965
|
+
};
|
|
1966
|
+
}
|
|
1967
|
+
continue;
|
|
1968
|
+
}
|
|
1969
|
+
|
|
1970
|
+
if (type === "content_block_start") {
|
|
1971
|
+
const index = Number(payload.index);
|
|
1972
|
+
ensureBlock(index, payload.content_block || {});
|
|
1973
|
+
continue;
|
|
1974
|
+
}
|
|
1975
|
+
|
|
1976
|
+
if (type === "content_block_delta") {
|
|
1977
|
+
const index = Number(payload.index);
|
|
1978
|
+
const current = ensureBlock(index, payload.content_block || {});
|
|
1979
|
+
const delta = payload.delta || {};
|
|
1980
|
+
if (delta.type === "text_delta" && typeof delta.text === "string" && current.type === "text") {
|
|
1981
|
+
current.text += delta.text;
|
|
1982
|
+
}
|
|
1983
|
+
if (delta.type === "input_json_delta" && typeof delta.partial_json === "string" && current.type === "tool_use") {
|
|
1984
|
+
const existing = typeof current.__input_json === "string" ? current.__input_json : "";
|
|
1985
|
+
current.__input_json = `${existing || ""}${delta.partial_json}`;
|
|
1986
|
+
}
|
|
1987
|
+
if (delta.type === "thinking_delta" && typeof delta.thinking === "string" && (current.type === "thinking" || current.type === "redacted_thinking")) {
|
|
1988
|
+
current.thinking += delta.thinking;
|
|
1989
|
+
}
|
|
1990
|
+
continue;
|
|
1991
|
+
}
|
|
1992
|
+
|
|
1993
|
+
if (type === "content_block_stop") {
|
|
1994
|
+
const index = Number(payload.index);
|
|
1995
|
+
const current = activeBlocks.get(index);
|
|
1996
|
+
if (current?.type === "tool_use" && typeof current.__input_json === "string") {
|
|
1997
|
+
current.input = parseJsonSafely(current.__input_json, current.input || {});
|
|
1998
|
+
delete current.__input_json;
|
|
1999
|
+
}
|
|
2000
|
+
continue;
|
|
2001
|
+
}
|
|
2002
|
+
|
|
2003
|
+
if (type === "message_delta") {
|
|
2004
|
+
if (payload.delta && typeof payload.delta === "object") {
|
|
2005
|
+
stopReason = String(payload.delta.stop_reason || stopReason).trim() || stopReason;
|
|
2006
|
+
}
|
|
2007
|
+
if (payload.usage && typeof payload.usage === "object") {
|
|
2008
|
+
usage = {
|
|
2009
|
+
...(usage || {}),
|
|
2010
|
+
output_tokens: Number(payload.usage.output_tokens) || Number(usage?.output_tokens) || 0
|
|
2011
|
+
};
|
|
2012
|
+
}
|
|
2013
|
+
}
|
|
2014
|
+
}
|
|
2015
|
+
|
|
2016
|
+
return {
|
|
2017
|
+
id: messageId || `msg_${Date.now()}`,
|
|
2018
|
+
type: "message",
|
|
2019
|
+
role: "assistant",
|
|
2020
|
+
model: model || "unknown",
|
|
2021
|
+
content: orderedBlocks.filter(Boolean),
|
|
2022
|
+
stop_reason: stopReason,
|
|
2023
|
+
usage
|
|
2024
|
+
};
|
|
2025
|
+
}
|
|
2026
|
+
|
|
2027
|
+
async function collectAmpWebSearchProbePayload(response, { targetFormat, requestKind }) {
|
|
2028
|
+
if (!(response instanceof Response)) return null;
|
|
2029
|
+
const contentType = String(response.headers.get("content-type") || "").toLowerCase();
|
|
2030
|
+
|
|
2031
|
+
if (!contentType.includes("text/event-stream")) {
|
|
2032
|
+
try {
|
|
2033
|
+
return await response.clone().json();
|
|
2034
|
+
} catch {
|
|
2035
|
+
return null;
|
|
2036
|
+
}
|
|
2037
|
+
}
|
|
2038
|
+
|
|
2039
|
+
let rawText = "";
|
|
2040
|
+
try {
|
|
2041
|
+
rawText = await response.clone().text();
|
|
2042
|
+
} catch {
|
|
2043
|
+
return null;
|
|
2044
|
+
}
|
|
2045
|
+
|
|
2046
|
+
if (targetFormat === FORMATS.CLAUDE) {
|
|
2047
|
+
return parseClaudeStreamPayload(rawText);
|
|
2048
|
+
}
|
|
2049
|
+
if (targetFormat === FORMATS.OPENAI && requestKind === "responses") {
|
|
2050
|
+
return parseOpenAIResponsesStreamPayload(rawText);
|
|
2051
|
+
}
|
|
2052
|
+
if (targetFormat === FORMATS.OPENAI) {
|
|
2053
|
+
return parseOpenAIChatStreamPayload(rawText);
|
|
2054
|
+
}
|
|
2055
|
+
return null;
|
|
2056
|
+
}
|
|
2057
|
+
|
|
2058
|
+
async function executeHostedSearchProviderRequest(resolvedRoute, body, env = {}) {
|
|
2059
|
+
const provider = resolvedRoute?.provider;
|
|
2060
|
+
if (!provider || typeof provider !== "object") {
|
|
2061
|
+
throw new Error("Hosted web search provider is not configured.");
|
|
2062
|
+
}
|
|
2063
|
+
|
|
2064
|
+
if (isSubscriptionProvider(provider)) {
|
|
2065
|
+
const subscriptionType = String(provider?.subscriptionType || provider?.subscription_type || "").trim().toLowerCase();
|
|
2066
|
+
const subscriptionResult = await makeSubscriptionProviderCall({
|
|
2067
|
+
provider,
|
|
2068
|
+
body,
|
|
2069
|
+
stream: subscriptionType === "chatgpt-codex"
|
|
2070
|
+
});
|
|
2071
|
+
if (!subscriptionResult?.ok || !(subscriptionResult.response instanceof Response)) {
|
|
2072
|
+
const message = await readSearchProviderError(subscriptionResult?.response);
|
|
2073
|
+
throw new Error(message || "Hosted web search subscription request failed.");
|
|
2074
|
+
}
|
|
2075
|
+
return subscriptionResult.response;
|
|
2076
|
+
}
|
|
2077
|
+
|
|
2078
|
+
const targetFormat = FORMATS.OPENAI;
|
|
2079
|
+
const providerUrl = resolveProviderUrl(provider, targetFormat, "responses");
|
|
2080
|
+
if (!providerUrl) {
|
|
2081
|
+
throw new Error(`Provider '${provider.id}' does not expose an OpenAI Responses endpoint.`);
|
|
2082
|
+
}
|
|
2083
|
+
|
|
2084
|
+
const response = await runFetchWithTimeout(providerUrl, {
|
|
2085
|
+
method: "POST",
|
|
2086
|
+
headers: buildProviderHeaders(provider, env, targetFormat),
|
|
2087
|
+
body: JSON.stringify(body)
|
|
2088
|
+
});
|
|
2089
|
+
if (!response.ok) {
|
|
2090
|
+
throw new Error(await readSearchProviderError(response.clone()));
|
|
2091
|
+
}
|
|
2092
|
+
return response;
|
|
2093
|
+
}
|
|
2094
|
+
|
|
2095
|
+
async function runHostedSearchProviderQuery(providerEntry, query, runtimeConfig = {}, env = {}) {
|
|
2096
|
+
const resolvedRoute = getResolvedHostedSearchRoute(runtimeConfig, providerEntry);
|
|
2097
|
+
if (!resolvedRoute?.provider || !resolvedRoute?.model) {
|
|
2098
|
+
throw new Error(`Hosted web search route '${providerEntry?.id || providerEntry?.providerId || "unknown"}' is not configured.`);
|
|
2099
|
+
}
|
|
2100
|
+
if (!supportsResolvedHostedSearchRoute(resolvedRoute.provider, resolvedRoute.model)) {
|
|
2101
|
+
throw new Error(`Hosted web search route '${providerEntry.id}' is not OpenAI-compatible.`);
|
|
2102
|
+
}
|
|
2103
|
+
|
|
2104
|
+
const requestBody = {
|
|
2105
|
+
model: resolvedRoute.model.id,
|
|
2106
|
+
input: String(query || "").trim() || HOSTED_WEB_SEARCH_TEST_QUERY,
|
|
2107
|
+
tools: [{ type: "web_search" }],
|
|
2108
|
+
tool_choice: "auto"
|
|
2109
|
+
};
|
|
2110
|
+
const response = await executeHostedSearchProviderRequest(resolvedRoute, requestBody, env);
|
|
2111
|
+
const payload = await collectAmpWebSearchProbePayload(response, {
|
|
2112
|
+
targetFormat: FORMATS.OPENAI,
|
|
2113
|
+
requestKind: "responses"
|
|
2114
|
+
});
|
|
2115
|
+
if (!payload) {
|
|
2116
|
+
throw new Error(`Hosted web search route '${providerEntry.id}' returned an unreadable response payload.`);
|
|
2117
|
+
}
|
|
2118
|
+
|
|
2119
|
+
const text = readHostedSearchResponseText(payload);
|
|
2120
|
+
if (!text) {
|
|
2121
|
+
throw new Error(`Hosted web search route '${providerEntry.id}' did not return assistant text.`);
|
|
2122
|
+
}
|
|
2123
|
+
|
|
2124
|
+
return {
|
|
2125
|
+
routeId: providerEntry.id,
|
|
2126
|
+
providerId: resolvedRoute.provider.id,
|
|
2127
|
+
providerName: resolvedRoute.provider.name || resolvedRoute.provider.id,
|
|
2128
|
+
modelId: resolvedRoute.model.id,
|
|
2129
|
+
label: buildHostedSearchProviderLabel(providerEntry, resolvedRoute),
|
|
2130
|
+
payload,
|
|
2131
|
+
usedWebSearch: payloadHasHostedWebSearchEvidence(payload),
|
|
2132
|
+
text
|
|
2133
|
+
};
|
|
2134
|
+
}
|
|
2135
|
+
|
|
2136
|
+
export async function testHostedWebSearchProviderRoute({
|
|
2137
|
+
runtimeConfig = {},
|
|
2138
|
+
routeId = "",
|
|
2139
|
+
env = {},
|
|
2140
|
+
query = HOSTED_WEB_SEARCH_TEST_QUERY
|
|
2141
|
+
} = {}) {
|
|
2142
|
+
const resolvedRouteId = String(routeId || "").trim();
|
|
2143
|
+
if (!resolvedRouteId) {
|
|
2144
|
+
throw new Error("Hosted web search route id is required.");
|
|
2145
|
+
}
|
|
2146
|
+
return runHostedSearchProviderQuery({
|
|
2147
|
+
id: resolvedRouteId,
|
|
2148
|
+
providerId: resolvedRouteId.slice(0, resolvedRouteId.indexOf("/")),
|
|
2149
|
+
model: resolvedRouteId.slice(resolvedRouteId.indexOf("/") + 1)
|
|
2150
|
+
}, query, runtimeConfig, env);
|
|
2151
|
+
}
|
|
2152
|
+
|
|
2153
|
+
export async function maybeInterceptAmpWebSearch({
|
|
2154
|
+
response,
|
|
2155
|
+
providerBody,
|
|
2156
|
+
targetFormat,
|
|
2157
|
+
requestKind,
|
|
2158
|
+
stream,
|
|
2159
|
+
runtimeConfig,
|
|
2160
|
+
env,
|
|
2161
|
+
stateStore,
|
|
2162
|
+
executeProviderRequest
|
|
2163
|
+
} = {}) {
|
|
2164
|
+
if (!(response instanceof Response)) {
|
|
2165
|
+
return {
|
|
2166
|
+
intercepted: false,
|
|
2167
|
+
response
|
|
2168
|
+
};
|
|
2169
|
+
}
|
|
2170
|
+
|
|
2171
|
+
const probePayload = await collectAmpWebSearchProbePayload(response, {
|
|
2172
|
+
targetFormat,
|
|
2173
|
+
requestKind
|
|
2174
|
+
});
|
|
2175
|
+
if (!probePayload) {
|
|
2176
|
+
return {
|
|
2177
|
+
intercepted: false,
|
|
2178
|
+
response
|
|
2179
|
+
};
|
|
2180
|
+
}
|
|
2181
|
+
|
|
2182
|
+
const probe = extractAmpWebSearchProbe(probePayload, {
|
|
2183
|
+
targetFormat,
|
|
2184
|
+
requestKind
|
|
2185
|
+
});
|
|
2186
|
+
if (!probe.hasWebSearchCalls) {
|
|
2187
|
+
return {
|
|
2188
|
+
intercepted: false,
|
|
2189
|
+
response
|
|
2190
|
+
};
|
|
2191
|
+
}
|
|
2192
|
+
|
|
2193
|
+
const searchResultsByCall = [];
|
|
2194
|
+
for (const toolCall of (probe.toolCalls || [])) {
|
|
2195
|
+
searchResultsByCall.push(await executeAmpInterceptedToolCall(
|
|
2196
|
+
toolCall,
|
|
2197
|
+
runtimeConfig,
|
|
2198
|
+
env,
|
|
2199
|
+
{
|
|
2200
|
+
stateStore
|
|
2201
|
+
}
|
|
2202
|
+
));
|
|
2203
|
+
}
|
|
2204
|
+
|
|
2205
|
+
const followUpBody = stripAmpWebSearchFollowUpTools(buildAmpWebSearchFollowUp(
|
|
2206
|
+
providerBody,
|
|
2207
|
+
probePayload,
|
|
2208
|
+
probe,
|
|
2209
|
+
searchResultsByCall,
|
|
2210
|
+
{
|
|
2211
|
+
targetFormat,
|
|
2212
|
+
requestKind,
|
|
2213
|
+
stream
|
|
2214
|
+
}
|
|
2215
|
+
));
|
|
2216
|
+
|
|
2217
|
+
const followUpResponse = await executeProviderRequest(followUpBody);
|
|
2218
|
+
if (!(followUpResponse instanceof Response) || !followUpResponse.ok) {
|
|
2219
|
+
return {
|
|
2220
|
+
intercepted: false,
|
|
2221
|
+
response
|
|
2222
|
+
};
|
|
2223
|
+
}
|
|
2224
|
+
|
|
2225
|
+
return {
|
|
2226
|
+
intercepted: true,
|
|
2227
|
+
response: followUpResponse,
|
|
2228
|
+
probePayload,
|
|
2229
|
+
probe,
|
|
2230
|
+
searchResultsByCall
|
|
2231
|
+
};
|
|
2232
|
+
}
|