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:
@@ -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,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user