@dbx-tools/appkit-web-search 0.9.131 → 0.9.136
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +50 -46
- package/index.ts +2 -1
- package/lib/index.d.ts +2 -1
- package/lib/index.js +2 -2
- package/lib/src/fetch.d.ts +31 -0
- package/lib/src/fetch.js +93 -19
- package/lib/src/plugin.d.ts +5 -0
- package/lib/src/plugin.js +6 -2
- package/lib/src/runtime.d.ts +5 -1
- package/lib/src/runtime.js +17 -5
- package/lib/src/search.js +79 -32
- package/package.json +7 -7
- package/src/fetch.ts +151 -22
- package/src/plugin.ts +18 -1
- package/src/runtime.ts +20 -3
- package/src/search.ts +97 -33
package/package.json
CHANGED
|
@@ -27,12 +27,12 @@
|
|
|
27
27
|
},
|
|
28
28
|
"dependencies": {
|
|
29
29
|
"@databricks/appkit": "0.81.0",
|
|
30
|
-
"@dbx-tools/appkit": "^0.9.
|
|
31
|
-
"@dbx-tools/core": "^0.9.
|
|
32
|
-
"@dbx-tools/model": "^0.9.
|
|
33
|
-
"@dbx-tools/path": "^0.9.
|
|
34
|
-
"@dbx-tools/shared-core": "^0.9.
|
|
35
|
-
"@dbx-tools/shared-model": "^0.9.
|
|
30
|
+
"@dbx-tools/appkit": "^0.9.136",
|
|
31
|
+
"@dbx-tools/core": "^0.9.136",
|
|
32
|
+
"@dbx-tools/model": "^0.9.136",
|
|
33
|
+
"@dbx-tools/path": "^0.9.136",
|
|
34
|
+
"@dbx-tools/shared-core": "^0.9.136",
|
|
35
|
+
"@dbx-tools/shared-model": "^0.9.136",
|
|
36
36
|
"@mastra/core": "1.71.0",
|
|
37
37
|
"cheerio": "^1.2.0",
|
|
38
38
|
"entities": "^4.5.0",
|
|
@@ -106,7 +106,7 @@
|
|
|
106
106
|
"./package.json": "./package.json"
|
|
107
107
|
}
|
|
108
108
|
},
|
|
109
|
-
"version": "0.9.
|
|
109
|
+
"version": "0.9.136",
|
|
110
110
|
"types": "./lib/index.d.ts",
|
|
111
111
|
"dbxToolsConfig": {
|
|
112
112
|
"tags": [
|
package/src/fetch.ts
CHANGED
|
@@ -20,10 +20,43 @@ import { gotScraping } from "got-scraping";
|
|
|
20
20
|
import { assertFetchUrlAllowed } from "./_fetch-url.ts";
|
|
21
21
|
import { toCallSettings, webFetchExecuteDefaults } from "./defaults.ts";
|
|
22
22
|
import { decodeHtmlEntities, htmlToText } from "./html-text.ts";
|
|
23
|
-
import {
|
|
23
|
+
import {
|
|
24
|
+
executeRead,
|
|
25
|
+
toWebSearchRuntime,
|
|
26
|
+
type WebSearchRuntime,
|
|
27
|
+
type WebSearchRuntimeInput,
|
|
28
|
+
} from "./runtime.ts";
|
|
24
29
|
import type { WebFetchRequest, WebFetchResult } from "./schema.ts";
|
|
25
30
|
|
|
26
31
|
const logger = log.logger("web-search/fetch");
|
|
32
|
+
const DEFAULT_IMAGE_MAX_BYTES = 5 * 1024 * 1024;
|
|
33
|
+
const DEFAULT_ASSET_PAGE_MAX_LENGTH = 1_500_000;
|
|
34
|
+
|
|
35
|
+
/** Input for a policy-checked HTML fetch used by in-process asset discovery. */
|
|
36
|
+
export interface WebAssetPageFetchRequest {
|
|
37
|
+
/** Public HTTPS page URL. */
|
|
38
|
+
url: string;
|
|
39
|
+
/** Maximum returned markup length. Defaults to 1.5 million characters. */
|
|
40
|
+
maxLength?: number;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Input for a policy-checked binary image fetch. */
|
|
44
|
+
export interface WebImageFetchRequest {
|
|
45
|
+
/** Public HTTPS image URL. */
|
|
46
|
+
url: string;
|
|
47
|
+
/** Maximum accepted response size. Defaults to 5 MiB. */
|
|
48
|
+
maxBytes?: number;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Binary image fetched through the web-search runtime's URL policy. */
|
|
52
|
+
export interface WebImageFetchResult {
|
|
53
|
+
/** Final URL after redirects. */
|
|
54
|
+
url: string;
|
|
55
|
+
/** Image media type returned by the server. */
|
|
56
|
+
contentType: string;
|
|
57
|
+
/** Fetched image bytes. */
|
|
58
|
+
data: Uint8Array;
|
|
59
|
+
}
|
|
27
60
|
|
|
28
61
|
/** The slice of a got-scraping response this module reads. */
|
|
29
62
|
interface FetchedPage {
|
|
@@ -46,38 +79,26 @@ function truncate(text: string, max: number): { content: string; truncated: bool
|
|
|
46
79
|
return { content: text.slice(0, max), truncated: true };
|
|
47
80
|
}
|
|
48
81
|
|
|
49
|
-
/**
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
* correctable failure the design calls for on the fetch path). The request's
|
|
54
|
-
* `maxLength` narrows (never widens) the plugin's `fetchMaxLength` cap.
|
|
55
|
-
* `signal` cancels the in-flight fetch.
|
|
56
|
-
*/
|
|
57
|
-
export async function runWebFetch(
|
|
58
|
-
request: WebFetchRequest,
|
|
59
|
-
runtimeOrConfig: WebSearchRuntimeInput,
|
|
82
|
+
/** Fetch and cache one raw page before caller-specific output reduction. */
|
|
83
|
+
async function fetchPage(
|
|
84
|
+
requestUrl: string,
|
|
85
|
+
runtime: WebSearchRuntime,
|
|
60
86
|
signal?: AbortSignal,
|
|
61
|
-
): Promise<
|
|
62
|
-
const runtime = toWebSearchRuntime(runtimeOrConfig);
|
|
87
|
+
): Promise<FetchedPage> {
|
|
63
88
|
const { config } = runtime;
|
|
64
|
-
await assertFetchUrlAllowed(
|
|
65
|
-
const cap = Math.min(request.maxLength ?? config.fetchMaxLength, config.fetchMaxLength);
|
|
66
|
-
|
|
67
|
-
// The response is cached before the format reduction, so the same page read
|
|
68
|
-
// as text and as html shares one network round trip.
|
|
89
|
+
await assertFetchUrlAllowed(requestUrl, config.allowList);
|
|
69
90
|
const page = await executeRead(
|
|
70
91
|
runtime,
|
|
71
92
|
"page-fetch",
|
|
72
93
|
toCallSettings(webFetchExecuteDefaults, config.timeoutMs, [
|
|
73
94
|
"web-search",
|
|
74
95
|
"fetch",
|
|
75
|
-
|
|
96
|
+
requestUrl,
|
|
76
97
|
...config.allowList.patterns,
|
|
77
98
|
]),
|
|
78
99
|
async (executeSignal): Promise<FetchedPage> => {
|
|
79
100
|
const response = await gotScraping({
|
|
80
|
-
url:
|
|
101
|
+
url: requestUrl,
|
|
81
102
|
// Bun's node:http2 reports default-port origins with :443 while
|
|
82
103
|
// http2-wrapper compares them against URL-normalized origins.
|
|
83
104
|
http2: false,
|
|
@@ -96,7 +117,7 @@ export async function runWebFetch(
|
|
|
96
117
|
...(executeSignal ? { signal: executeSignal } : {}),
|
|
97
118
|
});
|
|
98
119
|
return {
|
|
99
|
-
url: response.url ??
|
|
120
|
+
url: response.url ?? requestUrl,
|
|
100
121
|
statusCode: response.statusCode,
|
|
101
122
|
contentType: response.headers["content-type"],
|
|
102
123
|
body: typeof response.body === "string" ? response.body : String(response.body ?? ""),
|
|
@@ -105,6 +126,26 @@ export async function runWebFetch(
|
|
|
105
126
|
signal,
|
|
106
127
|
);
|
|
107
128
|
await assertFetchUrlAllowed(page.url, config.allowList);
|
|
129
|
+
return page;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Fetch a single URL and return its content in the requested format.
|
|
134
|
+
*
|
|
135
|
+
* Throws when the URL is not permitted by the allow-list (the visible,
|
|
136
|
+
* correctable failure the design calls for on the fetch path). The request's
|
|
137
|
+
* `maxLength` narrows (never widens) the plugin's `fetchMaxLength` cap.
|
|
138
|
+
* `signal` cancels the in-flight fetch.
|
|
139
|
+
*/
|
|
140
|
+
export async function runWebFetch(
|
|
141
|
+
request: WebFetchRequest,
|
|
142
|
+
runtimeOrConfig: WebSearchRuntimeInput,
|
|
143
|
+
signal?: AbortSignal,
|
|
144
|
+
): Promise<WebFetchResult> {
|
|
145
|
+
const runtime = toWebSearchRuntime(runtimeOrConfig);
|
|
146
|
+
const { config } = runtime;
|
|
147
|
+
const cap = Math.min(request.maxLength ?? config.fetchMaxLength, config.fetchMaxLength);
|
|
148
|
+
const page = await fetchPage(request.url, runtime, signal);
|
|
108
149
|
|
|
109
150
|
const isHtml = !page.contentType || /html|xml/i.test(page.contentType);
|
|
110
151
|
const rawContent = request.format === "html" || !isHtml ? page.body : htmlToText(page.body);
|
|
@@ -128,3 +169,91 @@ export async function runWebFetch(
|
|
|
128
169
|
truncated,
|
|
129
170
|
};
|
|
130
171
|
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Fetch bounded raw markup for deterministic in-process asset discovery.
|
|
175
|
+
*
|
|
176
|
+
* This does not widen the model-facing `web_fetch` cap.
|
|
177
|
+
*/
|
|
178
|
+
export async function runWebAssetPageFetch(
|
|
179
|
+
request: WebAssetPageFetchRequest,
|
|
180
|
+
runtimeOrConfig: WebSearchRuntimeInput,
|
|
181
|
+
signal?: AbortSignal,
|
|
182
|
+
): Promise<WebFetchResult> {
|
|
183
|
+
const runtime = toWebSearchRuntime(runtimeOrConfig);
|
|
184
|
+
const cap = Math.min(
|
|
185
|
+
Math.max(1, request.maxLength ?? DEFAULT_ASSET_PAGE_MAX_LENGTH),
|
|
186
|
+
DEFAULT_ASSET_PAGE_MAX_LENGTH,
|
|
187
|
+
);
|
|
188
|
+
const page = await fetchPage(request.url, runtime, signal);
|
|
189
|
+
const { content, truncated } = truncate(page.body, cap);
|
|
190
|
+
const title = extractTitle(page.body);
|
|
191
|
+
return {
|
|
192
|
+
url: page.url,
|
|
193
|
+
status: page.statusCode,
|
|
194
|
+
...(page.contentType ? { contentType: page.contentType } : {}),
|
|
195
|
+
...(title ? { title } : {}),
|
|
196
|
+
content,
|
|
197
|
+
truncated,
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/** Fetch one public image while preserving URL policy, timeout, and redirect checks. */
|
|
202
|
+
export async function runWebImageFetch(
|
|
203
|
+
request: WebImageFetchRequest,
|
|
204
|
+
runtimeOrConfig: WebSearchRuntimeInput,
|
|
205
|
+
signal?: AbortSignal,
|
|
206
|
+
): Promise<WebImageFetchResult> {
|
|
207
|
+
const runtime = toWebSearchRuntime(runtimeOrConfig);
|
|
208
|
+
const { config } = runtime;
|
|
209
|
+
const maxBytes = Math.max(1, request.maxBytes ?? DEFAULT_IMAGE_MAX_BYTES);
|
|
210
|
+
await assertFetchUrlAllowed(request.url, config.allowList);
|
|
211
|
+
const page = await executeRead(
|
|
212
|
+
runtime,
|
|
213
|
+
"image-fetch",
|
|
214
|
+
{
|
|
215
|
+
default: {
|
|
216
|
+
cache: { enabled: false },
|
|
217
|
+
retry: { enabled: true, attempts: 2 },
|
|
218
|
+
timeout: config.timeoutMs,
|
|
219
|
+
},
|
|
220
|
+
},
|
|
221
|
+
async (executeSignal) => {
|
|
222
|
+
const response = await gotScraping({
|
|
223
|
+
url: request.url,
|
|
224
|
+
responseType: "buffer",
|
|
225
|
+
http2: false,
|
|
226
|
+
timeout: { request: config.timeoutMs },
|
|
227
|
+
throwHttpErrors: true,
|
|
228
|
+
followRedirect: true,
|
|
229
|
+
hooks: {
|
|
230
|
+
beforeRequest: [
|
|
231
|
+
async (options) => {
|
|
232
|
+
await assertFetchUrlAllowed(String(options.url), config.allowList);
|
|
233
|
+
},
|
|
234
|
+
],
|
|
235
|
+
},
|
|
236
|
+
...(executeSignal ? { signal: executeSignal } : {}),
|
|
237
|
+
});
|
|
238
|
+
const contentType = String(response.headers["content-type"] ?? "")
|
|
239
|
+
.split(";")[0]!
|
|
240
|
+
.trim()
|
|
241
|
+
.toLowerCase();
|
|
242
|
+
if (!contentType.startsWith("image/")) {
|
|
243
|
+
throw new TypeError(`Expected an image response, received ${contentType || "unknown"}`);
|
|
244
|
+
}
|
|
245
|
+
if (response.body.byteLength > maxBytes) {
|
|
246
|
+
throw new RangeError(`Image response exceeds ${maxBytes} bytes`);
|
|
247
|
+
}
|
|
248
|
+
return {
|
|
249
|
+
url: response.url ?? request.url,
|
|
250
|
+
contentType,
|
|
251
|
+
data: new Uint8Array(response.body),
|
|
252
|
+
};
|
|
253
|
+
},
|
|
254
|
+
signal,
|
|
255
|
+
false,
|
|
256
|
+
);
|
|
257
|
+
await assertFetchUrlAllowed(page.url, config.allowList);
|
|
258
|
+
return page;
|
|
259
|
+
}
|
package/src/plugin.ts
CHANGED
|
@@ -31,7 +31,14 @@ import {
|
|
|
31
31
|
WEB_SEARCH_CONFIG_SCHEMA,
|
|
32
32
|
type WebSearchPluginConfig,
|
|
33
33
|
} from "./config.ts";
|
|
34
|
-
import {
|
|
34
|
+
import {
|
|
35
|
+
runWebAssetPageFetch,
|
|
36
|
+
runWebFetch,
|
|
37
|
+
runWebImageFetch,
|
|
38
|
+
type WebAssetPageFetchRequest,
|
|
39
|
+
type WebImageFetchRequest,
|
|
40
|
+
type WebImageFetchResult,
|
|
41
|
+
} from "./fetch.ts";
|
|
35
42
|
import { createWebSearchRuntime, type WebSearchRuntime } from "./runtime.ts";
|
|
36
43
|
import {
|
|
37
44
|
webFetchRequestSchema,
|
|
@@ -230,6 +237,16 @@ export class WebSearchPlugin extends ToolRegistryPlugin<WebSearchPluginConfig> {
|
|
|
230
237
|
*/
|
|
231
238
|
fetch: (request: WebFetchRequest, signal?: AbortSignal): Promise<WebFetchResult> =>
|
|
232
239
|
this.fetch(request, signal),
|
|
240
|
+
/** Fetch expanded HTML for in-process asset discovery. */
|
|
241
|
+
fetchAssetPage: (
|
|
242
|
+
request: WebAssetPageFetchRequest,
|
|
243
|
+
signal?: AbortSignal,
|
|
244
|
+
): Promise<WebFetchResult> => runWebAssetPageFetch(request, this.runtime, signal),
|
|
245
|
+
/** Fetch one image for in-process visual research. */
|
|
246
|
+
fetchImage: (
|
|
247
|
+
request: WebImageFetchRequest,
|
|
248
|
+
signal?: AbortSignal,
|
|
249
|
+
): Promise<WebImageFetchResult> => runWebImageFetch(request, this.runtime, signal),
|
|
233
250
|
};
|
|
234
251
|
}
|
|
235
252
|
|
package/src/runtime.ts
CHANGED
|
@@ -37,6 +37,10 @@ export interface WebSearchRuntime {
|
|
|
37
37
|
execute: WebSearchExecutor;
|
|
38
38
|
/** Provider-family cooldown deadlines learned from workspace failures. */
|
|
39
39
|
familyCooldowns: Map<policy.ModelFamily, number>;
|
|
40
|
+
/** Provider families that completed a native search successfully. */
|
|
41
|
+
availableFamilies: Set<policy.ModelFamily>;
|
|
42
|
+
/** First native request currently establishing one family's availability. */
|
|
43
|
+
familyProbes: Map<policy.ModelFamily, Promise<void>>;
|
|
40
44
|
}
|
|
41
45
|
|
|
42
46
|
/** Runtime-aware operation input, retaining resolved-config compatibility. */
|
|
@@ -54,7 +58,13 @@ export function createWebSearchRuntime(
|
|
|
54
58
|
overrides?: WebSearchPluginConfig,
|
|
55
59
|
execute: WebSearchExecutor = directExecute,
|
|
56
60
|
): WebSearchRuntime {
|
|
57
|
-
return {
|
|
61
|
+
return {
|
|
62
|
+
config: resolveWebSearchConfig(overrides),
|
|
63
|
+
execute,
|
|
64
|
+
familyCooldowns: new Map(),
|
|
65
|
+
availableFamilies: new Set(),
|
|
66
|
+
familyProbes: new Map(),
|
|
67
|
+
};
|
|
58
68
|
}
|
|
59
69
|
|
|
60
70
|
/** Build an isolated runtime from config that has already been resolved. */
|
|
@@ -62,7 +72,13 @@ export function createResolvedWebSearchRuntime(
|
|
|
62
72
|
config: ResolvedWebSearchConfig,
|
|
63
73
|
execute: WebSearchExecutor = directExecute,
|
|
64
74
|
): WebSearchRuntime {
|
|
65
|
-
return {
|
|
75
|
+
return {
|
|
76
|
+
config,
|
|
77
|
+
execute,
|
|
78
|
+
familyCooldowns: new Map(),
|
|
79
|
+
availableFamilies: new Set(),
|
|
80
|
+
familyProbes: new Map(),
|
|
81
|
+
};
|
|
66
82
|
}
|
|
67
83
|
|
|
68
84
|
/** Normalize an explicit runtime or legacy resolved config for one operation. */
|
|
@@ -91,10 +107,11 @@ export async function executeRead<T>(
|
|
|
91
107
|
settings: WebSearchExecutionSettings,
|
|
92
108
|
fn: (signal?: AbortSignal) => Promise<T>,
|
|
93
109
|
signal?: AbortSignal,
|
|
110
|
+
logFailure = true,
|
|
94
111
|
): Promise<T> {
|
|
95
112
|
return pluginExecution.runPluginExecution({
|
|
96
113
|
plugin: "web-search",
|
|
97
|
-
logger,
|
|
114
|
+
logger: logFailure ? logger : { warn() {} },
|
|
98
115
|
operation,
|
|
99
116
|
settings,
|
|
100
117
|
execute: runtime.execute,
|
package/src/search.ts
CHANGED
|
@@ -231,25 +231,22 @@ async function postServing(
|
|
|
231
231
|
const responseBody = await response.text();
|
|
232
232
|
const model =
|
|
233
233
|
object.isRecord(body) && typeof body.model === "string" ? body.model : undefined;
|
|
234
|
-
|
|
235
|
-
model,
|
|
236
|
-
api: url.endsWith("/responses") ? "responses" : "chat",
|
|
237
|
-
url,
|
|
238
|
-
status: response.status,
|
|
239
|
-
responseBody,
|
|
240
|
-
});
|
|
241
|
-
if (
|
|
234
|
+
const crossRegionDisabled =
|
|
242
235
|
response.status === 400 &&
|
|
243
236
|
model &&
|
|
244
237
|
policy.modelFamily(model) === policy.ModelFamily.Gemini &&
|
|
245
|
-
GEMINI_CROSS_REGION_DISABLED.test(responseBody)
|
|
246
|
-
) {
|
|
238
|
+
GEMINI_CROSS_REGION_DISABLED.test(responseBody);
|
|
239
|
+
if (crossRegionDisabled) {
|
|
247
240
|
const expiresAt = Date.now() + GEMINI_CROSS_REGION_COOLDOWN_MS;
|
|
248
241
|
runtime.familyCooldowns.set(policy.ModelFamily.Gemini, expiresAt);
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
242
|
+
runtime.availableFamilies.delete(policy.ModelFamily.Gemini);
|
|
243
|
+
} else {
|
|
244
|
+
logger.error("serving-request-rejected", {
|
|
245
|
+
model,
|
|
246
|
+
api: url.endsWith("/responses") ? "responses" : "chat",
|
|
247
|
+
url,
|
|
248
|
+
status: response.status,
|
|
249
|
+
responseBody,
|
|
253
250
|
});
|
|
254
251
|
}
|
|
255
252
|
// Load-shedding and server faults are worth another attempt; a 4xx is
|
|
@@ -268,6 +265,7 @@ async function postServing(
|
|
|
268
265
|
: response.json();
|
|
269
266
|
},
|
|
270
267
|
signal,
|
|
268
|
+
false,
|
|
271
269
|
);
|
|
272
270
|
return object.isRecord(payload) ? payload : {};
|
|
273
271
|
}
|
|
@@ -456,29 +454,95 @@ export async function runWebSearch(
|
|
|
456
454
|
);
|
|
457
455
|
}
|
|
458
456
|
|
|
457
|
+
return runSelectedWebSearch(request, runtime, ctx, endpoint, signal, writeProgress);
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
/** Run one selected family after its first request establishes availability. */
|
|
461
|
+
async function runSelectedWebSearch(
|
|
462
|
+
request: WebSearchRequest,
|
|
463
|
+
runtime: WebSearchRuntime,
|
|
464
|
+
ctx: WebSearchContext,
|
|
465
|
+
endpoint: ServingEndpointSummary,
|
|
466
|
+
signal?: AbortSignal,
|
|
467
|
+
writeProgress?: ProgressWriter,
|
|
468
|
+
): Promise<WebSearchResult> {
|
|
469
|
+
const family = endpointFamily(endpoint);
|
|
470
|
+
if (!family || runtime.availableFamilies.has(family)) {
|
|
471
|
+
try {
|
|
472
|
+
return await runNativeWebSearch(request, runtime, ctx, endpoint, signal, writeProgress);
|
|
473
|
+
} catch (error) {
|
|
474
|
+
return runSearchFallback(request, runtime, ctx, endpoint, error, signal, writeProgress);
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
const activeProbe = runtime.familyProbes.get(family);
|
|
479
|
+
if (activeProbe) {
|
|
480
|
+
signal?.throwIfAborted();
|
|
481
|
+
await activeProbe;
|
|
482
|
+
signal?.throwIfAborted();
|
|
483
|
+
return runWebSearch(request, runtime, ctx, signal, writeProgress);
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
let releaseProbe!: () => void;
|
|
487
|
+
const probe = new Promise<void>((resolve) => {
|
|
488
|
+
releaseProbe = resolve;
|
|
489
|
+
});
|
|
490
|
+
runtime.familyProbes.set(family, probe);
|
|
491
|
+
|
|
492
|
+
if (runtime.availableFamilies.has(family) || activeFamilyCooldowns(runtime).has(family)) {
|
|
493
|
+
runtime.familyProbes.delete(family);
|
|
494
|
+
releaseProbe();
|
|
495
|
+
return runWebSearch(request, runtime, ctx, signal, writeProgress);
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
let failure: unknown;
|
|
459
499
|
try {
|
|
460
|
-
|
|
500
|
+
const result = await runNativeWebSearch(request, runtime, ctx, endpoint, signal, writeProgress);
|
|
501
|
+
runtime.availableFamilies.add(family);
|
|
502
|
+
return result;
|
|
461
503
|
} catch (error) {
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
)
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
504
|
+
failure = error;
|
|
505
|
+
} finally {
|
|
506
|
+
if (runtime.familyProbes.get(family) === probe) runtime.familyProbes.delete(family);
|
|
507
|
+
releaseProbe();
|
|
508
|
+
}
|
|
509
|
+
return runSearchFallback(request, runtime, ctx, endpoint, failure, signal, writeProgress);
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
/** Resolve a cached family failure to another model or the scrape fallback. */
|
|
513
|
+
async function runSearchFallback(
|
|
514
|
+
request: WebSearchRequest,
|
|
515
|
+
runtime: WebSearchRuntime,
|
|
516
|
+
ctx: WebSearchContext,
|
|
517
|
+
endpoint: ServingEndpointSummary,
|
|
518
|
+
error: unknown,
|
|
519
|
+
signal?: AbortSignal,
|
|
520
|
+
writeProgress?: ProgressWriter,
|
|
521
|
+
): Promise<WebSearchResult> {
|
|
522
|
+
if (
|
|
523
|
+
endpointFamily(endpoint) !== policy.ModelFamily.Gemini ||
|
|
524
|
+
!activeFamilyCooldowns(runtime).has(policy.ModelFamily.Gemini)
|
|
525
|
+
) {
|
|
526
|
+
throw error;
|
|
527
|
+
}
|
|
528
|
+
const fallback = await resolveWebSearchModel(ctx, runtime, request.model);
|
|
529
|
+
if (!fallback) {
|
|
530
|
+
if (runtime.config.scrapeFallback) {
|
|
531
|
+
logger.warn("web-search-model-fallback", {
|
|
532
|
+
failedModel: endpoint.name,
|
|
533
|
+
fallback: "scrape",
|
|
534
|
+
reason: "cross-region-disabled",
|
|
535
|
+
});
|
|
536
|
+
return runScrapeSearch(request, runtime, signal);
|
|
475
537
|
}
|
|
476
|
-
|
|
477
|
-
failedModel: endpoint.name,
|
|
478
|
-
fallbackModel: fallback.name,
|
|
479
|
-
});
|
|
480
|
-
return runNativeWebSearch(request, runtime, ctx, fallback, signal, writeProgress);
|
|
538
|
+
throw error;
|
|
481
539
|
}
|
|
540
|
+
logger.warn("web-search-model-fallback", {
|
|
541
|
+
failedModel: endpoint.name,
|
|
542
|
+
fallbackModel: fallback.name,
|
|
543
|
+
reason: "cross-region-disabled",
|
|
544
|
+
});
|
|
545
|
+
return runSelectedWebSearch(request, runtime, ctx, fallback, signal, writeProgress);
|
|
482
546
|
}
|
|
483
547
|
|
|
484
548
|
/** Execute one native provider request against an already-selected endpoint. */
|