add cap to xai sources

This commit is contained in:
zekdevs
2026-06-26 10:11:31 -06:00
parent 3106f76d11
commit 2023b45d2c
3 changed files with 68 additions and 12 deletions
+5 -4
View File
@@ -39,10 +39,10 @@
| --- | --- | --- | --- |
| `query` | `string` | Yes | Search query, passed to providers unchanged. |
| `recency` | `"day" \| "week" \| "month" \| "year"` | No | Time filter. Only providers that implement it use it; code maps it for Brave, Perplexity, Tavily, SearXNG, Kagi, TinyFish, and Firecrawl. xAI ignores it because the documented Agent Tools `web_search` API does not expose date controls. |
| `limit` | `number` | No | Max results to return. Usually becomes the provider request's result-count parameter when `num_search_results` is absent. xAI does not send it upstream; it caps returned sources/citations after parsing unless `num_search_results` is provided. |
| `limit` | `number` | No | Max results to return. Usually becomes the provider request's result-count parameter when `num_search_results` is absent. For xAI, it is not sent upstream; it is a local post-parse cap on returned sources/citations, defaulting to `10` when omitted/invalid/zero and capped at `30`. |
| `max_tokens` | `number` | No | Passed through as provider token caps (`maxOutputTokens`, `max_tokens`, or xAI `max_output_tokens`) only by Anthropic, Gemini, xAI, and Perplexity API-key mode. Ignored by the other providers. |
| `temperature` | `number` | No | Passed through only by Anthropic, Gemini, xAI, and Perplexity API-key mode. Ignored by the other providers. |
| `num_search_results` | `number` | No | Requested search breadth or local result cap. Most providers send it upstream. xAI does not send it upstream; it caps returned sources/citations after parsing and takes precedence over `limit`. |
| `num_search_results` | `number` | No | Requested search breadth or local result cap. Most providers send it upstream. xAI does not send it upstream; it is a local cap on returned sources/citations after parsing, takes precedence over `limit`, defaults to `10` when omitted/invalid/zero, and is capped at `30`. |
## Outputs
The tool returns a single text content block plus structured `details`.
@@ -128,7 +128,7 @@ Streaming: none. `WebSearchTool.execute()` forwards its `AbortSignal` into `exec
- **xAI** — `packages/coding-agent/src/web/search/providers/xai.ts`
- Availability: `XAI_API_KEY` or `agent.db` credential for `xai`.
- Querying: POST `https://api.x.ai/v1/responses` with model `grok-4.3` and `tools: [{ type: "web_search" }]` using the `/v1/responses` Agent Tools API.
- `max_tokens` and `temperature` pass through. `recency` is ignored because the documented Agent Tools `web_search` API does not expose date controls. `limit` and `num_search_results` are not sent upstream; because xAI citations may include every encountered URL, the adapter locally caps returned `sources` and `citations` after parsing (`numSearchResults ?? limit`). Missing cap returns all sources/citations.
- `max_tokens` and `temperature` pass through. `recency` is ignored because the documented Agent Tools `web_search` API does not expose date controls. `limit` and `num_search_results` are not sent upstream; because xAI citations may include every encountered URL, the adapter locally caps returned `sources` and `citations` after parsing. The local cap uses `num_search_results` before `limit`, defaults to `10` when omitted/invalid/zero, and is capped at `30`.
- Output may include `answer`, `sources`, `citations`, `usage`, `model`, `requestId`, `authMode: "api_key"`.
- **Z.AI** — `packages/coding-agent/src/web/search/providers/zai.ts`
- Availability: env or `agent.db` credential for `zai`.
@@ -226,6 +226,7 @@ Streaming: none. `WebSearchTool.execute()` forwards its `AbortSignal` into `exec
- Parallel result count: default `10`, max `40`; per-result excerpt cap `10_000` chars (`packages/coding-agent/src/web/search/providers/parallel.ts`, `packages/coding-agent/src/web/parallel.ts`).
- Kagi result count: default `10`, max `40` (`packages/coding-agent/src/web/search/providers/kagi.ts`).
- SearXNG result count: default `10`, max `20` (`packages/coding-agent/src/web/search/providers/searxng.ts`).
- xAI local sources/citations cap: `num_search_results` before `limit`, omitted/invalid/zero => default `10`, max `30`; neither value is sent upstream (`packages/coding-agent/src/web/search/providers/xai.ts`).
- Perplexity API-key mode defaults: `max_tokens = 8192`, `temperature = 0.2`, `num_search_results = 20` (`packages/coding-agent/src/web/search/providers/perplexity.ts`).
- Anthropic defaults: model `claude-haiku-4-5`, `DEFAULT_MAX_TOKENS = 4096` when the provider omits `max_tokens` (`packages/coding-agent/src/web/search/providers/anthropic.ts`).
- Gemini retries: up to `3` retries per endpoint, base delay `1000` ms, rate-limit delay budget `5 * 60 * 1000` ms (`packages/coding-agent/src/web/search/providers/gemini.ts`).
@@ -246,7 +247,7 @@ Streaming: none. `WebSearchTool.execute()` forwards its `AbortSignal` into `exec
## Notes
- The model-facing schema does not expose `provider`, but internal callers can force one through `SearchQueryParams`.
- `resolveProviderChain()` lazily imports provider modules and caches singleton instances. Just asking for labels via `getSearchProviderLabel()` does not trigger those imports.
- Most providers treat `limit` and `num_search_results` as the same number because adapters pass `params.numSearchResults ?? params.limit`. Perplexity preserves both concepts; xAI applies that same precedence locally after parsing to cap returned sources/citations without sending a result-count control upstream.
- Most providers treat `limit` and `num_search_results` as the same number because adapters pass `params.numSearchResults ?? params.limit`. Perplexity preserves both concepts; xAI applies that same precedence locally after parsing to cap returned sources/citations without sending a result-count control upstream (`10` default, `30` max).
- `recency` is implemented by Brave, Perplexity, Tavily, SearXNG, Kagi, TinyFish, and Firecrawl; xAI ignores it because the documented Agent Tools `web_search` API does not expose date controls. The model-facing prompt does not name specific providers.
- `packages/coding-agent/src/config/settings-schema.ts` uses the shared `SEARCH_PROVIDER_PREFERENCES` / `SEARCH_PROVIDER_OPTIONS` metadata, so the settings selector and setup wizard expose `auto` plus every provider in the auto chain.
- DuckDuckGo is intentionally last in the auto chain because it is always available without credentials.
@@ -1,12 +1,15 @@
import { type ApiKey, type AuthStorage, withAuth } from "@oh-my-pi/pi-ai";
import type { SearchCitation, SearchResponse, SearchSource, SearchUsage } from "../../../web/search/types";
import { SearchProviderError } from "../../../web/search/types";
import { clampNumResults } from "../utils";
import type { SearchParams } from "./base";
import { SearchProvider } from "./base";
import { classifyProviderHttpError, withHardTimeout } from "./utils";
const XAI_RESPONSES_URL = "https://api.x.ai/v1/responses";
const XAI_WEB_SEARCH_MODEL = "grok-4.3";
const DEFAULT_NUM_RESULTS = 10;
const MAX_NUM_RESULTS = 30;
interface XAIUrlCitationAnnotation {
type?: string;
@@ -181,17 +184,15 @@ function parseUsage(usage: XAIResponsesUsage | null | undefined): SearchUsage |
function applyResultCap(
sources: SearchSource[],
citations: SearchCitation[],
requestedCap: number | undefined,
resultCap: number,
): { sources: SearchSource[]; citations: SearchCitation[] } {
if (requestedCap === undefined) return { sources, citations };
return {
sources: sources.slice(0, requestedCap),
citations: citations.slice(0, requestedCap),
sources: sources.slice(0, resultCap),
citations: citations.slice(0, resultCap),
};
}
function parseResponse(response: XAIResponsesResponse, requestedCap?: number): SearchResponse {
function parseResponse(response: XAIResponsesResponse, resultCap: number): SearchResponse {
const sources: SearchSource[] = [];
const citations: SearchCitation[] = [];
const seenUrls = new Set<string>();
@@ -206,7 +207,7 @@ function parseResponse(response: XAIResponsesResponse, requestedCap?: number): S
for (const url of response.citations ?? []) {
addCitationSource(sources, citations, seenUrls, url);
}
const limited = applyResultCap(sources, citations, requestedCap);
const limited = applyResultCap(sources, citations, resultCap);
return {
provider: "xai",
@@ -226,11 +227,12 @@ export async function searchXAI(params: SearchParams): Promise<SearchResponse> {
sessionId: params.sessionId,
});
const resultCap = clampNumResults(params.numSearchResults ?? params.limit, DEFAULT_NUM_RESULTS, MAX_NUM_RESULTS);
const response = await withAuth(keyOrResolver, (key: string) => callXAIResponses(key, params), {
signal: params.signal,
missingKeyMessage: 'xAI credentials not found. Set XAI_API_KEY or configure an API key for provider "xai".',
});
return parseResponse(response, params.numSearchResults ?? params.limit);
return parseResponse(response, resultCap);
}
/** Search provider for xAI web search. */
@@ -58,6 +58,10 @@ function captureFetch(responseBody: Record<string, unknown> | string, status = 2
};
}
function citationUrls(prefix: string, count: number): string[] {
return Array.from({ length: count }, (_, index) => `https://example.com/${prefix}-${index + 1}`);
}
describe("xAI web search provider", () => {
afterEach(() => {
vi.restoreAllMocks();
@@ -257,6 +261,55 @@ describe("xAI web search provider", () => {
});
});
it("defaults xAI local cap to 10 sources and citations when no count is requested", async () => {
const urls = citationUrls("default-cap", 12);
const capture = captureFetch({
id: "resp_default_cap",
model: "grok-4.3",
output_text: "Default capped xAI answer",
citations: urls,
});
const response = await searchXAI(makeParams(capture.fetchMock));
const expectedUrls = urls.slice(0, 10);
expect(response.sources).toHaveLength(10);
expect(response.citations).toHaveLength(10);
expect(response.sources.map(source => source.url)).toEqual(expectedUrls);
expect(response.citations?.map(citation => citation.url)).toEqual(expectedUrls);
expect(capture.capturedRequest).not.toBeNull();
const body = capture.capturedRequest?.body;
expect(body?.tools).toEqual([{ type: "web_search" }]);
expect(body).not.toHaveProperty("search_parameters");
expect(Object.keys(body ?? {}).sort()).toEqual(["input", "model", "tools"]);
});
it("clamps oversized xAI local cap requests to 30 sources and citations", async () => {
const urls = citationUrls("max-cap", 35);
const capture = captureFetch({
id: "resp_max_cap",
model: "grok-4.3",
output_text: "Max capped xAI answer",
citations: urls,
});
const response = await searchXAI({
...makeParams(capture.fetchMock),
numSearchResults: 99,
});
const expectedUrls = urls.slice(0, 30);
expect(response.sources).toHaveLength(30);
expect(response.citations).toHaveLength(30);
expect(response.sources.map(source => source.url)).toEqual(expectedUrls);
expect(response.citations?.map(citation => citation.url)).toEqual(expectedUrls);
expect(capture.capturedRequest).not.toBeNull();
const body = capture.capturedRequest?.body;
expect(body?.tools).toEqual([{ type: "web_search" }]);
expect(body).not.toHaveProperty("search_parameters");
expect(Object.keys(body ?? {}).sort()).toEqual(["input", "model", "tools"]);
});
it("caps parsed sources and citations locally without changing Agent Tools request shape", async () => {
const capture = captureFetch({
id: "resp_local_cap",