fix(web-search): thread AbortSignal to fetch() in anthropic, exa, jina, zai, gemini providers

- anthropic: add signal to AnthropicSearchParams, callSearch(), and
  AnthropicProvider.search()
- exa: add signal to ExaSearchParams, callExaSearch(), and ExaProvider.search()
- jina: add signal to JinaSearchParams, callJinaSearch(), and JinaProvider.search()
- zai: add signal to ZaiSearchParams, callZaiTool(), and ZaiProvider.search()
- gemini: add signal to GeminiSearchParams, callGeminiSearch() and
  buildInit() so both fetchWithRetry calls (initial + auth-refresh retry)
  carry the signal

The five providers listed above never forwarded SearchParams.signal to the
underlying HTTP layer, so pressing Esc during a web_search call had no effect
and the session froze until the request resolved or Ctrl+C was pressed.

brave, kimi, perplexity, searxng, tavily, synthetic, codex, kagi, and parallel
already thread the signal correctly and are unchanged.

Fixes #1044
This commit is contained in:
roboomp
2026-05-15 01:19:48 +00:00
parent 05117a0258
commit 565a6cf971
5 changed files with 23 additions and 4 deletions
@@ -38,6 +38,7 @@ export interface AnthropicSearchParams {
max_tokens?: number;
/** Sampling temperature (0–1). Lower = more focused/factual. */
temperature?: number;
signal?: AbortSignal;
}
/**
@@ -86,6 +87,7 @@ async function callSearch(
systemPrompt?: string,
maxTokens?: number,
temperature?: number,
signal?: AbortSignal,
): Promise<AnthropicApiResponse> {
const url = buildAnthropicUrl(auth);
const headers = buildAnthropicSearchHeaders(auth);
@@ -116,6 +118,7 @@ async function callSearch(
method: "POST",
headers,
body: JSON.stringify(body),
signal,
});
if (!response.ok) {
@@ -253,6 +256,7 @@ export async function searchAnthropic(params: AnthropicSearchParams): Promise<Se
params.system_prompt,
params.max_tokens,
params.temperature,
params.signal,
);
const result = parseResponse(response);
@@ -281,6 +285,7 @@ export class AnthropicProvider extends SearchProvider {
num_results: params.numSearchResults ?? params.limit,
max_tokens: params.maxOutputTokens,
temperature: params.temperature,
signal: params.signal,
});
}
}
@@ -29,6 +29,7 @@ export interface ExaSearchParams {
exclude_domains?: string[];
start_published_date?: string;
end_published_date?: string;
signal?: AbortSignal;
}
interface ExaSearchResult {
@@ -179,6 +180,7 @@ async function callExaSearch(apiKey: string, params: ExaSearchParams): Promise<E
"x-api-key": apiKey,
},
body: JSON.stringify(body),
signal: params.signal,
});
if (!response.ok) {
@@ -259,6 +261,7 @@ export class ExaProvider extends SearchProvider {
return searchExa({
query: params.query,
num_results: params.numSearchResults ?? params.limit,
signal: params.signal,
});
}
}
@@ -39,6 +39,7 @@ export interface GeminiSearchParams extends GeminiToolParams {
max_output_tokens?: number;
/** Sampling temperature (0–1). Lower = more focused/factual. */
temperature?: number;
signal?: AbortSignal;
}
export function buildGeminiRequestTools(params: GeminiToolParams): Array<Record<string, Record<string, unknown>>> {
@@ -235,6 +236,7 @@ async function callGeminiSearch(
maxOutputTokens?: number,
temperature?: number,
toolParams: GeminiToolParams = {},
signal?: AbortSignal,
): Promise<{
answer: string;
sources: SearchSource[];
@@ -308,6 +310,7 @@ async function callGeminiSearch(
...headers,
},
body: JSON.stringify(requestBody),
signal,
});
const urlFor = (attempt: number) =>
`${endpoints[Math.min(attempt, endpoints.length - 1)]}/v1internal:streamGenerateContent?alt=sse`;
@@ -500,6 +503,7 @@ export async function searchGemini(params: GeminiSearchParams): Promise<SearchRe
code_execution: params.code_execution,
url_context: params.url_context,
},
params.signal,
);
let sources = result.sources;
@@ -539,6 +543,7 @@ export class GeminiProvider extends SearchProvider {
google_search: params.googleSearch,
code_execution: params.codeExecution,
url_context: params.urlContext,
signal: params.signal,
});
}
}
@@ -17,6 +17,7 @@ const JINA_SEARCH_URL = "https://s.jina.ai";
export interface JinaSearchParams {
query: string;
num_results?: number;
signal?: AbortSignal;
}
interface JinaSearchResult {
@@ -33,13 +34,14 @@ export function findApiKey(): string | null {
}
/** Call Jina Reader search API. */
async function callJinaSearch(apiKey: string, query: string): Promise<JinaSearchResponse> {
async function callJinaSearch(apiKey: string, query: string, signal?: AbortSignal): Promise<JinaSearchResponse> {
const requestUrl = `${JINA_SEARCH_URL}/${encodeURIComponent(query)}`;
const response = await fetch(requestUrl, {
headers: {
Accept: "application/json",
Authorization: `Bearer ${apiKey}`,
},
signal,
});
if (!response.ok) {
@@ -58,7 +60,7 @@ export async function searchJina(params: JinaSearchParams): Promise<SearchRespon
throw new Error("JINA_API_KEY not found. Set it in environment or .env file.");
}
const response = await callJinaSearch(apiKey, params.query);
const response = await callJinaSearch(apiKey, params.query, params.signal);
const sources: SearchSource[] = [];
for (const result of response) {
@@ -91,6 +93,7 @@ export class JinaProvider extends SearchProvider {
return searchJina({
query: params.query,
num_results: params.numSearchResults ?? params.limit,
signal: params.signal,
});
}
}
@@ -20,6 +20,7 @@ const DEFAULT_NUM_RESULTS = 10;
export interface ZaiSearchParams {
query: string;
num_results?: number;
signal?: AbortSignal;
}
interface ZaiSearchResult {
@@ -55,7 +56,7 @@ export async function findApiKey(): Promise<string | null> {
return findCredential(getEnvApiKey("zai"), "zai");
}
async function callZaiTool(apiKey: string, args: Record<string, unknown>): Promise<unknown> {
async function callZaiTool(apiKey: string, args: Record<string, unknown>, signal?: AbortSignal): Promise<unknown> {
const response = await fetch(ZAI_MCP_URL, {
method: "POST",
headers: {
@@ -72,6 +73,7 @@ async function callZaiTool(apiKey: string, args: Record<string, unknown>): Promi
arguments: args,
},
}),
signal,
});
if (!response.ok) {
@@ -157,7 +159,7 @@ async function callZaiSearch(apiKey: string, params: ZaiSearchParams): Promise<u
let lastError: unknown;
for (let i = 0; i < attempts.length; i++) {
try {
return await callZaiTool(apiKey, attempts[i]);
return await callZaiTool(apiKey, attempts[i], params.signal);
} catch (error) {
lastError = error;
const isLastAttempt = i === attempts.length - 1;
@@ -302,6 +304,7 @@ export class ZaiProvider extends SearchProvider {
return searchZai({
query: params.query,
num_results: params.numSearchResults ?? params.limit,
signal: params.signal,
});
}
}