add cap to xai sources
This commit is contained in:
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user