@dbx-tools/appkit-web-search 0.9.69 → 0.9.71
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 +30 -33
- package/index.ts +5 -5
- package/lib/index.d.ts +5 -5
- package/lib/index.js +4 -4
- package/lib/src/config.d.ts +10 -15
- package/lib/src/config.js +8 -15
- package/lib/src/fetch.js +4 -1
- package/lib/src/plugin.d.ts +3 -0
- package/lib/src/plugin.js +16 -6
- package/lib/src/provider.d.ts +18 -20
- package/lib/src/provider.js +38 -29
- package/lib/src/runtime.d.ts +3 -0
- package/lib/src/runtime.js +3 -3
- package/lib/src/schema.d.ts +6 -0
- package/lib/src/schema.js +8 -7
- package/lib/src/search.d.ts +10 -2
- package/lib/src/search.js +216 -66
- package/lib/src/tool.d.ts +0 -1
- package/lib/src/tool.js +4 -4
- package/package.json +7 -7
- package/src/config.ts +13 -23
- package/src/fetch.ts +3 -0
- package/src/plugin.ts +35 -5
- package/src/provider.ts +44 -36
- package/src/runtime.ts +5 -2
- package/src/schema.ts +11 -6
- package/src/search.ts +252 -68
- package/src/tool.ts +3 -3
package/src/search.ts
CHANGED
|
@@ -26,14 +26,14 @@ import {
|
|
|
26
26
|
ConnectionError,
|
|
27
27
|
ExecutionError,
|
|
28
28
|
getExecutionContext,
|
|
29
|
-
ValidationError,
|
|
30
29
|
} from "@databricks/appkit";
|
|
31
|
-
import {
|
|
30
|
+
import type { AgentToolExecutionContext } from "@dbx-tools/appkit/tool-provider";
|
|
31
|
+
import { invoke, policy, resolve, modelCatalog } from "@dbx-tools/model";
|
|
32
32
|
import { log, object, stringUtils } from "@dbx-tools/shared-core";
|
|
33
|
-
import { openaiChat } from "@dbx-tools/shared-model";
|
|
34
|
-
import { MODEL_ENV,
|
|
33
|
+
import { openaiChat, type ServingEndpointSummary } from "@dbx-tools/shared-model";
|
|
34
|
+
import { MODEL_ENV, type ResolvedWebSearchConfig } from "./config.ts";
|
|
35
35
|
import { toCallSettings, webSearchExecuteDefaults } from "./defaults.ts";
|
|
36
|
-
import {
|
|
36
|
+
import { supportsWebSearch, webSearchProviderForFamily, webSearchToolSpec } from "./provider.ts";
|
|
37
37
|
import {
|
|
38
38
|
executeRead,
|
|
39
39
|
toWebSearchRuntime,
|
|
@@ -44,8 +44,9 @@ import type { WebSearchCitation, WebSearchRequest, WebSearchResult } from "./sch
|
|
|
44
44
|
import { runScrapeSearch } from "./scrape.ts";
|
|
45
45
|
|
|
46
46
|
type WorkspaceClientLike = modelCatalog.WorkspaceClientLike & invoke.AuthenticatingClientLike;
|
|
47
|
+
type ProgressWriter = NonNullable<AgentToolExecutionContext["writeProgress"]>;
|
|
47
48
|
const logger = log.logger("web-search/search");
|
|
48
|
-
const {
|
|
49
|
+
const { lookupModels } = resolve;
|
|
49
50
|
const { listServingEndpoints } = modelCatalog;
|
|
50
51
|
|
|
51
52
|
/**
|
|
@@ -55,6 +56,8 @@ const { listServingEndpoints } = modelCatalog;
|
|
|
55
56
|
* into a full traversal of the response.
|
|
56
57
|
*/
|
|
57
58
|
const MAX_GROUNDING_WALK_DEPTH = 6;
|
|
59
|
+
const GEMINI_CROSS_REGION_COOLDOWN_MS = 24 * 60 * 60 * 1000;
|
|
60
|
+
const GEMINI_CROSS_REGION_DISABLED = /cross-region processing is disabled/i;
|
|
58
61
|
|
|
59
62
|
/** Lowest HTTP status treated as a server-side (retryable) serving failure. */
|
|
60
63
|
const SERVER_ERROR_STATUS = 500;
|
|
@@ -70,6 +73,12 @@ export interface WebSearchContext {
|
|
|
70
73
|
cacheIdentity?: string;
|
|
71
74
|
}
|
|
72
75
|
|
|
76
|
+
/** Model-selection fields used to rank one web-search endpoint. */
|
|
77
|
+
export type WebSearchModelSelectionOptions = Pick<
|
|
78
|
+
ResolvedWebSearchConfig,
|
|
79
|
+
"model" | "modelFallbacks" | "fuzzy" | "fuzzyThreshold"
|
|
80
|
+
>;
|
|
81
|
+
|
|
73
82
|
/**
|
|
74
83
|
* Resolve the OBO workspace client + host from the active AppKit execution
|
|
75
84
|
* context. Inside `agent.stream`'s `asUser(req)` scope this hits the serving
|
|
@@ -87,77 +96,96 @@ export async function resolveWebSearchContext(): Promise<WebSearchContext> {
|
|
|
87
96
|
* Resolve a web-search-capable model against the LIVE workspace catalogue - so
|
|
88
97
|
* we never return an endpoint id that isn't actually deployed (the "endpoint
|
|
89
98
|
* does not exist" failure a hardcoded fallback id would cause). Reuses
|
|
90
|
-
* `@dbx-tools/model`'s existing catalogue +
|
|
99
|
+
* `@dbx-tools/model`'s existing catalogue + ranker rather than a custom
|
|
91
100
|
* lookup: {@link listServingEndpoints} lists the endpoints (cached), and we
|
|
92
|
-
* restrict the candidate set
|
|
93
|
-
* {@link
|
|
101
|
+
* restrict the candidate set through model-owned capability metadata before
|
|
102
|
+
* {@link lookupModels} ranks within it.
|
|
94
103
|
*
|
|
95
|
-
*
|
|
96
|
-
*
|
|
97
|
-
*
|
|
98
|
-
* plugin's `model`, or `WEB_SEARCH_MODEL` - throws when it resolves to an
|
|
99
|
-
* unsupported / absent endpoint, so a bad pin surfaces rather than silently
|
|
100
|
-
* degrading. A pin inherited from the shared `DATABRICKS_SERVING_ENDPOINT_NAME`
|
|
101
|
-
* binding is only a preference: that endpoint is the app's serving endpoint,
|
|
102
|
-
* not necessarily a web-search-capable one, so an unusable value falls through
|
|
103
|
-
* to the fallback order.
|
|
104
|
+
* The highest-ranked Gemini model wins. Without Gemini, a GPT request matching
|
|
105
|
+
* the calling/configured model wins, followed by the highest-ranked GPT model
|
|
106
|
+
* and then any additional configured searches.
|
|
104
107
|
*/
|
|
105
108
|
async function resolveWebSearchModel(
|
|
106
109
|
ctx: WebSearchContext,
|
|
107
|
-
|
|
110
|
+
runtime: WebSearchRuntime,
|
|
108
111
|
requested: string | undefined,
|
|
109
|
-
): Promise<
|
|
112
|
+
): Promise<ServingEndpointSummary | null> {
|
|
113
|
+
const { config } = runtime;
|
|
110
114
|
const endpoints = await listServingEndpoints(ctx.client, ctx.host, {
|
|
111
115
|
...(ctx.cacheIdentity ? { cacheIdentity: ctx.cacheIdentity } : {}),
|
|
112
116
|
});
|
|
117
|
+
return selectWebSearchEndpoint(endpoints, requested, config, activeFamilyCooldowns(runtime));
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** Select one deployed web-search endpoint using family and capability policy. */
|
|
121
|
+
export function selectWebSearchEndpoint(
|
|
122
|
+
endpoints: readonly ServingEndpointSummary[],
|
|
123
|
+
requested: string | undefined,
|
|
124
|
+
config: WebSearchModelSelectionOptions,
|
|
125
|
+
suppressedFamilies: ReadonlySet<policy.ModelFamily> = new Set(),
|
|
126
|
+
): ServingEndpointSummary | null {
|
|
113
127
|
// Only deployed, web-search-capable endpoints are candidates.
|
|
114
|
-
const capable = endpoints.filter((
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
128
|
+
const capable = endpoints.filter((endpoint) => {
|
|
129
|
+
const family = endpointFamily(endpoint);
|
|
130
|
+
return supportsWebSearch(endpoint) && (!family || !suppressedFamilies.has(family));
|
|
131
|
+
});
|
|
132
|
+
const gemini = lookupModels(capable, {
|
|
133
|
+
search: policy.ModelFamily.Gemini,
|
|
134
|
+
limit: 1,
|
|
135
|
+
threshold: config.fuzzyThreshold,
|
|
136
|
+
})[0]?.endpoint;
|
|
137
|
+
if (gemini) return gemini;
|
|
138
|
+
const gpt = lookupModels(capable, {
|
|
139
|
+
search: policy.ModelFamily.Gpt,
|
|
140
|
+
limit: 50,
|
|
141
|
+
threshold: config.fuzzyThreshold,
|
|
142
|
+
}).map(({ endpoint }) => endpoint);
|
|
118
143
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
const
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
"model",
|
|
139
|
-
pinned,
|
|
140
|
-
`a deployed web-search-capable endpoint (deployed: ${deployed})`,
|
|
141
|
-
);
|
|
142
|
-
} else {
|
|
143
|
-
throw ConfigurationError.resourceNotFound(
|
|
144
|
-
"Web-search-capable serving endpoint",
|
|
145
|
-
`Deployed web-search-capable endpoints: ${deployed}. Set model or ${MODEL_ENV} to one of them.`,
|
|
146
|
-
);
|
|
144
|
+
const preferred = requested ?? config.model;
|
|
145
|
+
if (preferred && policy.modelFamily(preferred) === policy.ModelFamily.Gpt) {
|
|
146
|
+
const matched = config.fuzzy
|
|
147
|
+
? lookupModels(gpt, {
|
|
148
|
+
search: preferred,
|
|
149
|
+
limit: 1,
|
|
150
|
+
threshold: config.fuzzyThreshold,
|
|
151
|
+
})[0]?.endpoint
|
|
152
|
+
: gpt.find((endpoint) => endpoint.name === preferred);
|
|
153
|
+
if (matched) return matched;
|
|
154
|
+
}
|
|
155
|
+
if (gpt[0]) return gpt[0];
|
|
156
|
+
|
|
157
|
+
for (const search of config.modelFallbacks) {
|
|
158
|
+
if (
|
|
159
|
+
search.toLowerCase() === policy.ModelFamily.Gemini ||
|
|
160
|
+
search.toLowerCase() === policy.ModelFamily.Gpt
|
|
161
|
+
) {
|
|
162
|
+
continue;
|
|
147
163
|
}
|
|
164
|
+
const matched = lookupModels(capable, {
|
|
165
|
+
search,
|
|
166
|
+
limit: 1,
|
|
167
|
+
threshold: config.fuzzyThreshold,
|
|
168
|
+
})[0]?.endpoint;
|
|
169
|
+
if (matched) return matched;
|
|
148
170
|
}
|
|
149
171
|
|
|
150
|
-
|
|
172
|
+
return null;
|
|
173
|
+
}
|
|
151
174
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
175
|
+
/** Return one endpoint's model-owned family identity. */
|
|
176
|
+
function endpointFamily(endpoint: ServingEndpointSummary): policy.ModelFamily | undefined {
|
|
177
|
+
return policy.modelFamily(endpoint.modelServiceName ?? endpoint.name);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/** Drop expired family cooldowns and return the currently suppressed set. */
|
|
181
|
+
function activeFamilyCooldowns(
|
|
182
|
+
runtime: WebSearchRuntime,
|
|
183
|
+
now = Date.now(),
|
|
184
|
+
): ReadonlySet<policy.ModelFamily> {
|
|
185
|
+
for (const [family, expiresAt] of runtime.familyCooldowns) {
|
|
186
|
+
if (expiresAt <= now) runtime.familyCooldowns.delete(family);
|
|
187
|
+
}
|
|
188
|
+
return new Set(runtime.familyCooldowns.keys());
|
|
161
189
|
}
|
|
162
190
|
|
|
163
191
|
/**
|
|
@@ -181,6 +209,7 @@ async function postServing(
|
|
|
181
209
|
runtime: WebSearchRuntime,
|
|
182
210
|
cacheKey: readonly (string | number)[],
|
|
183
211
|
signal?: AbortSignal,
|
|
212
|
+
writeProgress?: ProgressWriter,
|
|
184
213
|
): Promise<Record<string, unknown>> {
|
|
185
214
|
const { config } = runtime;
|
|
186
215
|
const payload = await executeRead(
|
|
@@ -199,6 +228,30 @@ async function postServing(
|
|
|
199
228
|
...(executeSignal ? { signal: executeSignal } : {}),
|
|
200
229
|
});
|
|
201
230
|
if (!response.ok) {
|
|
231
|
+
const responseBody = await response.text();
|
|
232
|
+
const model =
|
|
233
|
+
object.isRecord(body) && typeof body.model === "string" ? body.model : undefined;
|
|
234
|
+
logger.error("serving-request-rejected", {
|
|
235
|
+
model,
|
|
236
|
+
api: url.endsWith("/responses") ? "responses" : "chat",
|
|
237
|
+
url,
|
|
238
|
+
status: response.status,
|
|
239
|
+
responseBody,
|
|
240
|
+
});
|
|
241
|
+
if (
|
|
242
|
+
response.status === 400 &&
|
|
243
|
+
model &&
|
|
244
|
+
policy.modelFamily(model) === policy.ModelFamily.Gemini &&
|
|
245
|
+
GEMINI_CROSS_REGION_DISABLED.test(responseBody)
|
|
246
|
+
) {
|
|
247
|
+
const expiresAt = Date.now() + GEMINI_CROSS_REGION_COOLDOWN_MS;
|
|
248
|
+
runtime.familyCooldowns.set(policy.ModelFamily.Gemini, expiresAt);
|
|
249
|
+
logger.warn("provider-family-suppressed", {
|
|
250
|
+
family: policy.ModelFamily.Gemini,
|
|
251
|
+
reason: "cross-region-disabled",
|
|
252
|
+
expiresAt: new Date(expiresAt).toISOString(),
|
|
253
|
+
});
|
|
254
|
+
}
|
|
202
255
|
// Load-shedding and server faults are worth another attempt; a 4xx is
|
|
203
256
|
// this request's own problem, so it must not be retried.
|
|
204
257
|
const retryable =
|
|
@@ -208,13 +261,97 @@ async function postServing(
|
|
|
208
261
|
? new ConnectionError(message, { context: { status: response.status } })
|
|
209
262
|
: new ExecutionError(message, { context: { status: response.status } });
|
|
210
263
|
}
|
|
211
|
-
return
|
|
264
|
+
return writeProgress &&
|
|
265
|
+
response.body &&
|
|
266
|
+
response.headers.get("content-type")?.includes("text/event-stream")
|
|
267
|
+
? readStreamingResponse(response, writeProgress)
|
|
268
|
+
: response.json();
|
|
212
269
|
},
|
|
213
270
|
signal,
|
|
214
271
|
);
|
|
215
272
|
return object.isRecord(payload) ? payload : {};
|
|
216
273
|
}
|
|
217
274
|
|
|
275
|
+
/** Consume an OpenAI Responses SSE stream and retain its completed response. */
|
|
276
|
+
async function readStreamingResponse(
|
|
277
|
+
response: Response,
|
|
278
|
+
writeProgress: ProgressWriter,
|
|
279
|
+
): Promise<Record<string, unknown>> {
|
|
280
|
+
const reader = response.body!.getReader();
|
|
281
|
+
const decoder = new TextDecoder();
|
|
282
|
+
let buffer = "";
|
|
283
|
+
let completed: Record<string, unknown> | undefined;
|
|
284
|
+
const consume = async (frame: string) => {
|
|
285
|
+
const data = frame
|
|
286
|
+
.split("\n")
|
|
287
|
+
.filter((line) => line.startsWith("data:"))
|
|
288
|
+
.map((line) => line.slice(5).trimStart())
|
|
289
|
+
.join("\n");
|
|
290
|
+
if (!data || data === "[DONE]") return;
|
|
291
|
+
const event = JSON.parse(data) as unknown;
|
|
292
|
+
if (!object.isRecord(event) || typeof event.type !== "string") return;
|
|
293
|
+
await emitWebSearchProgress(event, writeProgress);
|
|
294
|
+
if (event.type === "response.completed" && object.isRecord(event.response)) {
|
|
295
|
+
completed = event.response;
|
|
296
|
+
}
|
|
297
|
+
if (event.type === "response.failed") {
|
|
298
|
+
throw new ExecutionError("web-search: streaming response failed", {
|
|
299
|
+
context: { response: event.response },
|
|
300
|
+
});
|
|
301
|
+
}
|
|
302
|
+
};
|
|
303
|
+
|
|
304
|
+
for (;;) {
|
|
305
|
+
const { done, value } = await reader.read();
|
|
306
|
+
buffer += decoder.decode(value, { stream: !done });
|
|
307
|
+
buffer = buffer.replaceAll("\r\n", "\n");
|
|
308
|
+
let boundary = buffer.indexOf("\n\n");
|
|
309
|
+
while (boundary >= 0) {
|
|
310
|
+
await consume(buffer.slice(0, boundary));
|
|
311
|
+
buffer = buffer.slice(boundary + 2);
|
|
312
|
+
boundary = buffer.indexOf("\n\n");
|
|
313
|
+
}
|
|
314
|
+
if (done) break;
|
|
315
|
+
}
|
|
316
|
+
if (buffer.trim()) await consume(buffer);
|
|
317
|
+
if (!completed) throw new ExecutionError("web-search: streaming response did not complete");
|
|
318
|
+
return completed;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/** Publish native web-search lifecycle events as common tool progress. */
|
|
322
|
+
async function emitWebSearchProgress(
|
|
323
|
+
event: Record<string, unknown>,
|
|
324
|
+
writeProgress: ProgressWriter,
|
|
325
|
+
): Promise<void> {
|
|
326
|
+
if (event.type !== "response.output_item.done" || !object.isRecord(event.item)) return;
|
|
327
|
+
const action = object.isRecord(event.item.action) ? event.item.action : undefined;
|
|
328
|
+
if (!action || action.type !== "search") return;
|
|
329
|
+
const query = typeof action.query === "string" ? action.query : undefined;
|
|
330
|
+
const queries = Array.isArray(action.queries)
|
|
331
|
+
? action.queries.filter((value): value is string => typeof value === "string")
|
|
332
|
+
: [];
|
|
333
|
+
const groupId =
|
|
334
|
+
typeof event.item.id === "string"
|
|
335
|
+
? event.item.id
|
|
336
|
+
: `search-${String(event.sequence_number ?? query ?? "unknown")}`;
|
|
337
|
+
const resultCount = queries.length > 0 ? queries.length : query ? 1 : 0;
|
|
338
|
+
await writeProgress({
|
|
339
|
+
type: "tool_status",
|
|
340
|
+
status: "search",
|
|
341
|
+
message: query ?? "the web",
|
|
342
|
+
groupId,
|
|
343
|
+
});
|
|
344
|
+
await writeProgress({
|
|
345
|
+
type: "tool_status",
|
|
346
|
+
status: "result",
|
|
347
|
+
message: "Result",
|
|
348
|
+
groupId,
|
|
349
|
+
...(resultCount > 0
|
|
350
|
+
? { detail: `${resultCount} ${resultCount === 1 ? "result" : "results"}` }
|
|
351
|
+
: {}),
|
|
352
|
+
});
|
|
353
|
+
}
|
|
354
|
+
|
|
218
355
|
/* --------------------------- response extraction --------------------------- */
|
|
219
356
|
|
|
220
357
|
/**
|
|
@@ -299,12 +436,13 @@ export async function runWebSearch(
|
|
|
299
436
|
runtimeOrConfig: WebSearchRuntimeInput,
|
|
300
437
|
ctx: WebSearchContext,
|
|
301
438
|
signal?: AbortSignal,
|
|
439
|
+
writeProgress?: ProgressWriter,
|
|
302
440
|
): Promise<WebSearchResult> {
|
|
303
441
|
const runtime = toWebSearchRuntime(runtimeOrConfig);
|
|
304
442
|
const { config } = runtime;
|
|
305
|
-
const
|
|
443
|
+
const endpoint = await resolveWebSearchModel(ctx, runtime, request.model);
|
|
306
444
|
|
|
307
|
-
if (
|
|
445
|
+
if (endpoint === null) {
|
|
308
446
|
// No native web-search model deployed in this workspace.
|
|
309
447
|
if (config.scrapeFallback) {
|
|
310
448
|
logger.info("no-native-model:scrape-fallback", { query: request.query });
|
|
@@ -318,7 +456,51 @@ export async function runWebSearch(
|
|
|
318
456
|
);
|
|
319
457
|
}
|
|
320
458
|
|
|
321
|
-
|
|
459
|
+
try {
|
|
460
|
+
return await runNativeWebSearch(request, runtime, ctx, endpoint, signal, writeProgress);
|
|
461
|
+
} catch (error) {
|
|
462
|
+
if (
|
|
463
|
+
endpointFamily(endpoint) !== policy.ModelFamily.Gemini ||
|
|
464
|
+
!activeFamilyCooldowns(runtime).has(policy.ModelFamily.Gemini)
|
|
465
|
+
) {
|
|
466
|
+
throw error;
|
|
467
|
+
}
|
|
468
|
+
const fallback = await resolveWebSearchModel(ctx, runtime, request.model);
|
|
469
|
+
if (!fallback) {
|
|
470
|
+
if (config.scrapeFallback) {
|
|
471
|
+
logger.info("gemini-unavailable:scrape-fallback", { query: request.query });
|
|
472
|
+
return runScrapeSearch(request, runtime, signal);
|
|
473
|
+
}
|
|
474
|
+
throw error;
|
|
475
|
+
}
|
|
476
|
+
logger.info("gemini-unavailable:model-fallback", {
|
|
477
|
+
failedModel: endpoint.name,
|
|
478
|
+
fallbackModel: fallback.name,
|
|
479
|
+
});
|
|
480
|
+
return runNativeWebSearch(request, runtime, ctx, fallback, signal, writeProgress);
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
/** Execute one native provider request against an already-selected endpoint. */
|
|
485
|
+
async function runNativeWebSearch(
|
|
486
|
+
request: WebSearchRequest,
|
|
487
|
+
runtime: WebSearchRuntime,
|
|
488
|
+
ctx: WebSearchContext,
|
|
489
|
+
endpoint: ServingEndpointSummary,
|
|
490
|
+
signal?: AbortSignal,
|
|
491
|
+
writeProgress?: ProgressWriter,
|
|
492
|
+
): Promise<WebSearchResult> {
|
|
493
|
+
const { config } = runtime;
|
|
494
|
+
const modelId = endpoint.name;
|
|
495
|
+
const provider = webSearchProviderForFamily(
|
|
496
|
+
policy.modelFamily(endpoint.modelServiceName ?? endpoint.name),
|
|
497
|
+
);
|
|
498
|
+
if (!provider) {
|
|
499
|
+
throw ConfigurationError.resourceNotFound(
|
|
500
|
+
"Native web-search provider",
|
|
501
|
+
`Model ${modelId} has no supported provider family.`,
|
|
502
|
+
);
|
|
503
|
+
}
|
|
322
504
|
const spec = webSearchToolSpec(provider, config.webSearchTools);
|
|
323
505
|
|
|
324
506
|
const url =
|
|
@@ -326,14 +508,15 @@ export async function runWebSearch(
|
|
|
326
508
|
const body =
|
|
327
509
|
spec.api === "responses"
|
|
328
510
|
? {
|
|
511
|
+
...spec.request,
|
|
329
512
|
model: modelId,
|
|
330
513
|
input: [{ role: "user", content: request.query }],
|
|
331
|
-
|
|
514
|
+
...(writeProgress ? { stream: true } : {}),
|
|
332
515
|
}
|
|
333
516
|
: {
|
|
517
|
+
...spec.request,
|
|
334
518
|
model: modelId,
|
|
335
519
|
messages: [{ role: "user", content: request.query }],
|
|
336
|
-
tools: [spec.tool],
|
|
337
520
|
};
|
|
338
521
|
|
|
339
522
|
const payload = await postServing(
|
|
@@ -343,6 +526,7 @@ export async function runWebSearch(
|
|
|
343
526
|
runtime,
|
|
344
527
|
["web-search", "serving", spec.api, modelId, request.query],
|
|
345
528
|
signal,
|
|
529
|
+
writeProgress,
|
|
346
530
|
);
|
|
347
531
|
|
|
348
532
|
const { answer, citations } =
|
package/src/tool.ts
CHANGED
|
@@ -37,7 +37,7 @@ import { createWebSearchRuntime, type WebSearchRuntime } from "./runtime.ts";
|
|
|
37
37
|
import {
|
|
38
38
|
webFetchRequestSchema,
|
|
39
39
|
webFetchResultSchema,
|
|
40
|
-
|
|
40
|
+
webSearchToolRequestSchema,
|
|
41
41
|
webSearchResultSchema,
|
|
42
42
|
WEB_FETCH_TOOL_DESCRIPTION,
|
|
43
43
|
WEB_SEARCH_TOOL_DESCRIPTION,
|
|
@@ -108,13 +108,13 @@ export function webSearchTool(opts: WebSearchToolOptions = {}) {
|
|
|
108
108
|
return createTool({
|
|
109
109
|
id: opts.id ?? "web_search",
|
|
110
110
|
description: WEB_SEARCH_TOOL_DESCRIPTION,
|
|
111
|
-
inputSchema:
|
|
111
|
+
inputSchema: webSearchToolRequestSchema,
|
|
112
112
|
outputSchema: webSearchResultSchema,
|
|
113
113
|
// A search's result URLs aren't known before the call, so a pattern gate
|
|
114
114
|
// is treated as "always gate".
|
|
115
115
|
...(gate.mode === "none" ? {} : { requireApproval: () => true }),
|
|
116
116
|
execute: async (input) => {
|
|
117
|
-
const request = parseToolInput(
|
|
117
|
+
const request = parseToolInput(webSearchToolRequestSchema, input);
|
|
118
118
|
return runWebSearch(request, runtime, await resolveWebSearchContext());
|
|
119
119
|
},
|
|
120
120
|
});
|