From 59c4550dbaad1e73877f0d5212956d672460cb53 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Twardziak?= Date: Tue, 2 Dec 2025 23:08:03 +0100 Subject: [PATCH 01/15] Adds native Live Search support to xAI provider Adds support for xAI's native `live_search` tool, enabling models to search the web for real-time information. This includes the ability to bind the `live_search` tool, configure search parameters, and override search parameters per request. It also allows combining `live_search` with custom tools. The changes introduce the `XAILiveSearchTool`, `XAIBuiltInTool`, and `XAISearchParameters` types to provide better type safety and control over the search functionality. --- libs/providers/langchain-xai/README.md | 80 ++++ .../langchain-xai/src/chat_models.ts | 394 +++++++++++++++++- libs/providers/langchain-xai/src/index.ts | 9 + .../src/tests/chat_models.int.test.ts | 123 +++++- .../src/tests/chat_models.test.ts | 190 ++++++++- 5 files changed, 774 insertions(+), 22 deletions(-) diff --git a/libs/providers/langchain-xai/README.md b/libs/providers/langchain-xai/README.md index ef4a518403ef..b992bd0fd323 100644 --- a/libs/providers/langchain-xai/README.md +++ b/libs/providers/langchain-xai/README.md @@ -31,6 +31,86 @@ const message = new HumanMessage("What color is the sky?"); const res = await model.invoke([message]); ``` +## Server Tool Calling (Live Search) + +xAI supports server-side tools that are executed by the API rather than requiring client-side execution. The `live_search` tool enables the model to search the web for real-time information. + +### Using the built-in live_search tool + +```typescript +import { ChatXAI } from "@langchain/xai"; + +const model = new ChatXAI({ + model: "grok-2-1212", +}); + +// Bind the live_search tool +const modelWithSearch = model.bindTools([{ type: "live_search" }]); + +// The model will search the web for real-time information +const result = await modelWithSearch.invoke( + "What happened in tech news today?" +); +console.log(result.content); +``` + +### Using searchParameters for more control + +```typescript +import { ChatXAI } from "@langchain/xai"; + +const model = new ChatXAI({ + model: "grok-2-1212", + searchParameters: { + mode: "auto", // "auto" | "on" | "off" + max_search_results: 5, + from_date: "2024-01-01", // ISO date string + return_citations: true, + }, +}); + +const result = await model.invoke("What are the latest AI developments?"); +``` + +### Override search parameters per request + +```typescript +const result = await model.invoke("Find recent news about SpaceX", { + searchParameters: { + mode: "on", + max_search_results: 10, + allowed_domains: ["spacex.com", "nasa.gov"], + }, +}); +``` + +### Combining live_search with custom tools + +```typescript +import { ChatXAI } from "@langchain/xai"; + +const model = new ChatXAI({ model: "grok-2-1212" }); + +const modelWithTools = model.bindTools([ + { type: "live_search" }, // Built-in server tool + { + // Custom function tool + type: "function", + function: { + name: "get_stock_price", + description: "Get the current stock price", + parameters: { + type: "object", + properties: { + symbol: { type: "string" }, + }, + required: ["symbol"], + }, + }, + }, +]); +``` + ## Development To develop the `@langchain/xai` package, you'll need to follow these instructions: diff --git a/libs/providers/langchain-xai/src/chat_models.ts b/libs/providers/langchain-xai/src/chat_models.ts index 27e201d3b879..26c1759b7b8c 100644 --- a/libs/providers/langchain-xai/src/chat_models.ts +++ b/libs/providers/langchain-xai/src/chat_models.ts @@ -10,24 +10,207 @@ import { } from "@langchain/core/language_models/chat_models"; import { ModelProfile } from "@langchain/core/language_models/profile"; import { Serialized } from "@langchain/core/load/serializable"; -import { AIMessageChunk, BaseMessage } from "@langchain/core/messages"; +import { + AIMessageChunk, + BaseMessage, + type UsageMetadata, +} from "@langchain/core/messages"; import { Runnable } from "@langchain/core/runnables"; import { getEnvironmentVariable } from "@langchain/core/utils/env"; import { InteropZodType } from "@langchain/core/utils/types"; import { type OpenAICoreRequestOptions, type OpenAIClient, - OpenAIToolChoice, ChatOpenAICompletions, } from "@langchain/openai"; import PROFILES from "./profiles.js"; -type ChatXAIToolType = BindToolsInput | OpenAIClient.ChatCompletionTool; +export type OpenAIToolChoice = + | OpenAIClient.ChatCompletionToolChoiceOption + | "any" + | string; + +/** + * xAI's built-in live_search tool type. + * Enables the model to search the web for real-time information. + */ +export interface XAILiveSearchTool { + /** + * The type of the tool. Must be "live_search" for xAI's built-in search. + */ + type: "live_search"; +} + +/** + * Union type for all xAI built-in server-side tools. + */ +export type XAIBuiltInTool = XAILiveSearchTool; + +/** + * Tool type that includes both standard tools and xAI built-in tools. + */ +type ChatXAIToolType = + | BindToolsInput + | OpenAIClient.ChatCompletionTool + | XAIBuiltInTool; + +/** + * Search parameters for xAI's Live Search API. + * Controls how the model searches for and retrieves real-time information. + * + * @note The Live Search API is being deprecated by xAI in favor of + * the agentic tool calling approach. Consider using `tools: [{ type: "live_search" }]` + * for future compatibility. + */ +export interface XAISearchParameters { + /** + * Controls when the model should perform a search. + * - "auto": Let the model decide when to search (default) + * - "on": Always search for every request + * - "off": Never search + */ + mode?: "auto" | "on" | "off"; + /** + * Maximum number of search results to return. + * @default 5 + */ + max_search_results?: number; + /** + * Filter search results to only include content from after this date. + * Format: ISO 8601 date string (e.g., "2024-01-01") + */ + from_date?: string; + /** + * Filter search results to only include content from before this date. + * Format: ISO 8601 date string (e.g., "2024-12-31") + */ + to_date?: string; + /** + * Whether to return citations/sources for the search results. + * @default true + */ + return_citations?: boolean; + /** + * Specific domains to include in the search. + * Example: ["wikipedia.org", "arxiv.org"] + */ + allowed_domains?: string[]; + /** + * Specific domains to exclude from the search. + * Example: ["reddit.com"] + */ + excluded_domains?: string[]; +} + +/** + * xAI-specific invocation parameters that extend the OpenAI completion params + * with xAI's search_parameters field. + */ +export type ChatXAICompletionsInvocationParams = Omit< + OpenAIClient.Chat.Completions.ChatCompletionCreateParams, + "messages" +> & { + /** + * Search parameters for xAI's Live Search API. + * When present, enables the model to search the web for real-time information. + */ + search_parameters?: { + mode: "auto" | "on" | "off"; + max_search_results?: number; + from_date?: string; + to_date?: string; + return_citations?: boolean; + allowed_domains?: string[]; + excluded_domains?: string[]; + }; +}; + +/** + * xAI-specific additional kwargs that may be present on AI messages. + * Includes xAI-specific fields like reasoning_content. + */ +export interface XAIAdditionalKwargs { + /** + * The reasoning content from xAI models that support chain-of-thought reasoning. + * This contains the model's internal reasoning process. + */ + reasoning_content?: string; + /** + * Tool calls made by the model. + */ + tool_calls?: OpenAIClient.ChatCompletionMessageToolCall[]; + /** + * Additional properties that may be present. + */ + [key: string]: unknown; +} + +/** + * xAI-specific response metadata that may include usage information. + */ +export interface XAIResponseMetadata { + /** + * Token usage information. + */ + usage?: UsageMetadata; + /** + * Additional metadata properties. + */ + [key: string]: unknown; +} + +/** + * Checks if a tool is an xAI built-in tool (like live_search). + * Built-in tools are executed server-side by the xAI API. + * + * @param tool - The tool to check + * @returns true if the tool is an xAI built-in tool + */ +export function isXAIBuiltInTool( + tool: ChatXAIToolType +): tool is XAIBuiltInTool { + return ( + typeof tool === "object" && + tool !== null && + "type" in tool && + (tool as XAIBuiltInTool).type === "live_search" + ); +} export interface ChatXAICallOptions extends BaseChatModelCallOptions { headers?: Record; + /** + * A list of tools the model may call. + * Can include standard function tools and xAI built-in tools like `{ type: "live_search" }`. + * + * @example + * ```typescript + * // Using built-in live_search tool + * const llm = new ChatXAI().bindTools([{ type: "live_search" }]); + * const result = await llm.invoke("What happened in tech news today?"); + * ``` + */ tools?: ChatXAIToolType[]; tool_choice?: OpenAIToolChoice | string | "auto" | "any"; + /** + * Search parameters for xAI's Live Search API. + * Enables the model to search the web for real-time information. + * + * @note This is an alternative to using `tools: [{ type: "live_search" }]`. + * The Live Search API parameters approach may be deprecated in favor of + * the tool-based approach. + * + * @example + * ```typescript + * const result = await llm.invoke("What's the latest news?", { + * searchParameters: { + * mode: "auto", + * max_search_results: 5, + * } + * }); + * ``` + */ + searchParameters?: XAISearchParameters; } export interface ChatXAIInput extends BaseChatModelParams { @@ -66,6 +249,23 @@ export interface ChatXAIInput extends BaseChatModelParams { * This limits ensures computational efficiency and resource management. */ maxTokens?: number; + /** + * Default search parameters for xAI's Live Search API. + * When set, these parameters will be applied to all requests unless + * overridden in the call options. + * + * @example + * ```typescript + * const llm = new ChatXAI({ + * model: "grok-beta", + * searchParameters: { + * mode: "auto", + * max_search_results: 5, + * } + * }); + * ``` + */ + searchParameters?: XAISearchParameters; } /** @@ -393,6 +593,55 @@ export interface ChatXAIInput extends BaseChatModelParams { * * *
+ * + *
+ * Server Tool Calling (Live Search) + * + * xAI supports server-side tools that are executed by the API rather than + * requiring client-side execution. The `live_search` tool enables the model + * to search the web for real-time information. + * + * ```typescript + * // Method 1: Using the built-in live_search tool + * const llm = new ChatXAI({ + * model: "grok-beta", + * temperature: 0, + * }); + * + * const llmWithSearch = llm.bindTools([{ type: "live_search" }]); + * const result = await llmWithSearch.invoke("What happened in tech news today?"); + * console.log(result.content); + * // The model will search the web and include real-time information in its response + * ``` + * + * ```typescript + * // Method 2: Using searchParameters for more control + * const llm = new ChatXAI({ + * model: "grok-beta", + * searchParameters: { + * mode: "auto", // "auto" | "on" | "off" + * max_search_results: 5, + * from_date: "2024-01-01", // ISO date string + * return_citations: true, + * } + * }); + * + * const result = await llm.invoke("What are the latest AI developments?"); + * ``` + * + * ```typescript + * // Method 3: Override search parameters per request + * const result = await llm.invoke("Find recent news about SpaceX", { + * searchParameters: { + * mode: "on", + * max_search_results: 10, + * allowed_domains: ["spacex.com", "nasa.gov"], + * } + * }); + * ``` + *
+ * + *
*/ export class ChatXAI extends ChatOpenAICompletions { static lc_name() { @@ -413,6 +662,11 @@ export class ChatXAI extends ChatOpenAICompletions { lc_namespace = ["langchain", "chat_models", "xai"]; + /** + * Default search parameters for the Live Search API. + */ + searchParameters?: XAISearchParameters; + constructor(fields?: Partial) { const apiKey = fields?.apiKey || getEnvironmentVariable("XAI_API_KEY"); if (!apiKey) { @@ -429,6 +683,8 @@ export class ChatXAI extends ChatOpenAICompletions { baseURL: "https://api.x.ai/v1", }, }); + + this.searchParameters = fields?.searchParameters; } toJSON(): Serialized { @@ -452,6 +708,86 @@ export class ChatXAI extends ChatOpenAICompletions { return params; } + /** + * Get the effective search parameters, merging defaults with call options. + * @param options Call options that may contain search parameters + * @returns Merged search parameters or undefined if none are configured + */ + protected _getEffectiveSearchParameters( + options?: this["ParsedCallOptions"] + ): XAISearchParameters | undefined { + const callSearchParams = options?.searchParameters; + if (!this.searchParameters && !callSearchParams) { + return undefined; + } + // Merge instance-level with call-level, call-level takes precedence + return { + ...this.searchParameters, + ...callSearchParams, + }; + } + + /** + * Check if any built-in tools (like live_search) are in the tools list. + * @param tools List of tools to check + * @returns true if any built-in tools are present + */ + protected _hasBuiltInTools(tools?: ChatXAIToolType[]): boolean { + return tools?.some(isXAIBuiltInTool) ?? false; + } + + /** @internal */ + override invocationParams( + options?: this["ParsedCallOptions"], + extra?: { streaming?: boolean } + ): ChatXAICompletionsInvocationParams { + const baseParams = super.invocationParams(options, extra); + + // Cast to xAI-specific params type + const params: ChatXAICompletionsInvocationParams = { ...baseParams }; + + // Get effective search parameters from instance and call options + const effectiveSearchParams = this._getEffectiveSearchParameters(options); + + // Check if live_search tool is being used + const hasLiveSearchTool = this._hasBuiltInTools( + options?.tools as ChatXAIToolType[] | undefined + ); + + // Add search_parameters if needed + if (effectiveSearchParams || hasLiveSearchTool) { + const searchParams = hasLiveSearchTool + ? { mode: "auto" as const, ...effectiveSearchParams } + : effectiveSearchParams; + + if (searchParams) { + params.search_parameters = { + mode: searchParams.mode ?? "auto", + ...(searchParams.max_search_results !== undefined && { + max_search_results: searchParams.max_search_results, + }), + ...(searchParams.from_date !== undefined && { + from_date: searchParams.from_date, + }), + ...(searchParams.to_date !== undefined && { + to_date: searchParams.to_date, + }), + ...(searchParams.return_citations !== undefined && { + return_citations: searchParams.return_citations, + }), + ...(searchParams.allowed_domains !== undefined && { + allowed_domains: searchParams.allowed_domains, + }), + ...(searchParams.excluded_domains !== undefined && { + excluded_domains: searchParams.excluded_domains, + }), + }; + } + } + + return params; + } + async completionWithRetry( request: OpenAIClient.Chat.ChatCompletionCreateParamsStreaming, options?: OpenAICoreRequestOptions @@ -492,9 +828,22 @@ export class ChatXAI extends ChatOpenAICompletions { return msg; }); + // Filter out xAI built-in tools from the standard tools array + // Built-in tools are handled via search_parameters (added in invocationParams) + let filteredTools: OpenAIClient.ChatCompletionTool[] | undefined; + if (request.tools) { + filteredTools = request.tools.filter( + (tool) => !isXAIBuiltInTool(tool) + ) as OpenAIClient.ChatCompletionTool[]; + if (filteredTools.length === 0) { + filteredTools = undefined; + } + } + const newRequest = { ...request, messages: newRequestMessages, + tools: filteredTools, }; if (newRequest.stream === true) { @@ -515,34 +864,43 @@ export class ChatXAI extends ChatOpenAICompletions { | "developer" | "assistant" | "tool" - ) { - const messageChunk: AIMessageChunk = - super._convertCompletionsDeltaToBaseMessageChunk( - delta, - rawResponse, - defaultRole - ); + ): AIMessageChunk { + const messageChunk = super._convertCompletionsDeltaToBaseMessageChunk( + delta, + rawResponse, + defaultRole + ) as AIMessageChunk; + + // Cast to xAI-specific types for proper typing + const responseMetadata = + messageChunk.response_metadata as XAIResponseMetadata; + // Make concatenating chunks work without merge warning if (!rawResponse.choices[0]?.finish_reason) { - delete messageChunk.response_metadata.usage; + delete responseMetadata.usage; delete messageChunk.usage_metadata; } else { - messageChunk.usage_metadata = messageChunk.response_metadata.usage; + messageChunk.usage_metadata = responseMetadata.usage; } return messageChunk; } protected override _convertCompletionsMessageToBaseMessage( - message: OpenAIClient.ChatCompletionMessage, + message: OpenAIClient.ChatCompletionMessage & { + reasoning_content?: string; + }, rawResponse: OpenAIClient.ChatCompletion - ) { + ): AIMessageChunk { const langChainMessage = super._convertCompletionsMessageToBaseMessage( message, rawResponse - ); - langChainMessage.additional_kwargs.reasoning_content = - // eslint-disable-next-line @typescript-eslint/no-explicit-any - (message as any).reasoning_content; + ) as AIMessageChunk; + + // Cast additional_kwargs to xAI-specific type and add reasoning_content + const additionalKwargs = + langChainMessage.additional_kwargs as XAIAdditionalKwargs; + additionalKwargs.reasoning_content = message.reasoning_content; + return langChainMessage; } diff --git a/libs/providers/langchain-xai/src/index.ts b/libs/providers/langchain-xai/src/index.ts index 38c7cea7f478..6a4c0b3ed399 100644 --- a/libs/providers/langchain-xai/src/index.ts +++ b/libs/providers/langchain-xai/src/index.ts @@ -1 +1,10 @@ export * from "./chat_models.js"; +export type { + XAILiveSearchTool, + XAIBuiltInTool, + XAISearchParameters, + ChatXAICompletionsInvocationParams, + XAIAdditionalKwargs, + XAIResponseMetadata, +} from "./chat_models.js"; +export { isXAIBuiltInTool } from "./chat_models.js"; diff --git a/libs/providers/langchain-xai/src/tests/chat_models.int.test.ts b/libs/providers/langchain-xai/src/tests/chat_models.int.test.ts index 699d34795248..3024119d7e7f 100644 --- a/libs/providers/langchain-xai/src/tests/chat_models.int.test.ts +++ b/libs/providers/langchain-xai/src/tests/chat_models.int.test.ts @@ -1,4 +1,4 @@ -import { test, expect } from "vitest"; +import { test, expect, describe } from "vitest"; import { z } from "zod/v3"; import { @@ -10,7 +10,7 @@ import { import { tool } from "@langchain/core/tools"; import { concat } from "@langchain/core/utils/stream"; -import { ChatXAI } from "../chat_models.js"; +import { ChatXAI, type XAILiveSearchTool } from "../chat_models.js"; test("invoke", async () => { const chat = new ChatXAI({ @@ -228,3 +228,122 @@ test("xAI can stream tool calls", async () => { expect(finalMessage.tool_calls?.[0].args).toHaveProperty("location"); expect(finalMessage.tool_calls?.[0].id).toBeDefined(); }); + +// Server Tool Calling (Live Search) Integration Tests +describe("Server Tool Calling (Live Search)", () => { + test("invoke with live_search built-in tool", async () => { + const chat = new ChatXAI({ + maxRetries: 0, + model: "grok-2-1212", + }); + + const liveSearchTool: XAILiveSearchTool = { type: "live_search" }; + const chatWithSearch = chat.bindTools([liveSearchTool]); + + // Ask about recent events to trigger search + const message = new HumanMessage( + "What are the latest developments in AI as of today?" + ); + const res = await chatWithSearch.invoke([message]); + + // The response should contain information (live search results are incorporated) + expect(res.content).toBeDefined(); + expect((res.content as string).length).toBeGreaterThan(50); + }); + + test("invoke with searchParameters in constructor", async () => { + const chat = new ChatXAI({ + maxRetries: 0, + model: "grok-2-1212", + searchParameters: { + mode: "auto", + max_search_results: 5, + }, + }); + + const message = new HumanMessage("What happened in the news today?"); + const res = await chat.invoke([message]); + + expect(res.content).toBeDefined(); + expect((res.content as string).length).toBeGreaterThan(50); + }); + + test("invoke with searchParameters in call options", async () => { + const chat = new ChatXAI({ + maxRetries: 0, + model: "grok-2-1212", + }); + + const message = new HumanMessage( + "What is the current status of SpaceX launches?" + ); + const res = await chat.invoke([message], { + searchParameters: { + mode: "on", + max_search_results: 3, + }, + }); + + expect(res.content).toBeDefined(); + expect((res.content as string).length).toBeGreaterThan(50); + }); + + test("stream with live_search tool", async () => { + const chat = new ChatXAI({ + maxRetries: 0, + model: "grok-2-1212", + }); + + const liveSearchTool: XAILiveSearchTool = { type: "live_search" }; + const chatWithSearch = chat.bindTools([liveSearchTool]); + + const message = new HumanMessage("What are the top tech news stories?"); + const stream = await chatWithSearch.stream([message]); + + let finalMessage: AIMessageChunk | undefined; + for await (const chunk of stream) { + finalMessage = !finalMessage ? chunk : concat(finalMessage, chunk); + } + + expect(finalMessage).toBeDefined(); + expect(finalMessage?.content).toBeDefined(); + }); + + test("combine live_search with function tools", async () => { + const chat = new ChatXAI({ + maxRetries: 0, + model: "grok-2-1212", + }); + + const liveSearchTool: XAILiveSearchTool = { type: "live_search" }; + const customTool = { + type: "function" as const, + function: { + name: "get_stock_price", + description: "Get the current stock price for a given symbol", + parameters: { + type: "object", + properties: { + symbol: { + type: "string", + description: "The stock symbol, e.g. AAPL", + }, + }, + required: ["symbol"], + }, + }, + }; + + const chatWithTools = chat.bindTools([liveSearchTool, customTool]); + + // Ask something that might trigger either tool + const message = new HumanMessage( + "What is Apple's current stock price and what are the latest news about the company?" + ); + const res = await chatWithTools.invoke([message]); + + expect(res.content).toBeDefined(); + // The response might have tool calls for the custom tool + // or might answer directly with live search data + }); +}); diff --git a/libs/providers/langchain-xai/src/tests/chat_models.test.ts b/libs/providers/langchain-xai/src/tests/chat_models.test.ts index 2acfd0d7b3bb..7e0bf8f293ed 100644 --- a/libs/providers/langchain-xai/src/tests/chat_models.test.ts +++ b/libs/providers/langchain-xai/src/tests/chat_models.test.ts @@ -1,5 +1,11 @@ -import { test, expect, beforeEach } from "vitest"; -import { ChatXAI } from "../chat_models.js"; +import { test, expect, beforeEach, describe } from "vitest"; +import { + ChatXAI, + isXAIBuiltInTool, + type XAILiveSearchTool, + type XAISearchParameters, + type ChatXAICompletionsInvocationParams, +} from "../chat_models.js"; beforeEach(() => { process.env.XAI_API_KEY = "foo"; @@ -22,3 +28,183 @@ test("Serialization with no params", () => { `{"lc":1,"type":"constructor","id":["langchain","chat_models","xai","ChatXAI"],"kwargs":{"model":"grok-beta"}}` ); }); + +describe("Server Tool Calling", () => { + describe("isXAIBuiltInTool", () => { + test("should identify live_search as a built-in tool", () => { + const liveSearchTool: XAILiveSearchTool = { type: "live_search" }; + expect(isXAIBuiltInTool(liveSearchTool)).toBe(true); + }); + + test("should not identify function tools as built-in", () => { + const functionTool = { + type: "function", + function: { + name: "get_weather", + description: "Get the weather", + parameters: { type: "object", properties: {} }, + }, + }; + expect(isXAIBuiltInTool(functionTool)).toBe(false); + }); + + test("should not identify invalid objects as built-in", () => { + expect(isXAIBuiltInTool(null as unknown as XAILiveSearchTool)).toBe( + false + ); + expect(isXAIBuiltInTool(undefined as unknown as XAILiveSearchTool)).toBe( + false + ); + expect(isXAIBuiltInTool({} as unknown as XAILiveSearchTool)).toBe(false); + expect( + isXAIBuiltInTool({ type: "other" } as unknown as XAILiveSearchTool) + ).toBe(false); + }); + }); + + describe("ChatXAI with searchParameters", () => { + test("should store searchParameters from constructor", () => { + const searchParams: XAISearchParameters = { + mode: "auto", + max_search_results: 5, + }; + const model = new ChatXAI({ + searchParameters: searchParams, + }); + expect(model.searchParameters).toEqual(searchParams); + }); + + test("should have undefined searchParameters by default", () => { + const model = new ChatXAI(); + expect(model.searchParameters).toBeUndefined(); + }); + + test("should merge search parameters correctly", () => { + const model = new ChatXAI({ + searchParameters: { + mode: "auto", + max_search_results: 5, + }, + }); + + // Access protected method via any cast for testing + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const effectiveParams = (model as any)._getEffectiveSearchParameters({ + searchParameters: { + max_search_results: 10, + from_date: "2024-01-01", + }, + }); + + expect(effectiveParams).toEqual({ + mode: "auto", + max_search_results: 10, + from_date: "2024-01-01", + }); + }); + }); + + describe("invocationParams with server tools", () => { + test("should add search_parameters when live_search tool is bound", () => { + const model = new ChatXAI(); + + const params: ChatXAICompletionsInvocationParams = model.invocationParams( + { + tools: [{ type: "live_search" }], + } as unknown as ChatXAI["ParsedCallOptions"] + ); + + expect(params.search_parameters).toBeDefined(); + expect(params.search_parameters?.mode).toBe("auto"); + }); + + test("should add search_parameters from call options", () => { + const model = new ChatXAI(); + + const params: ChatXAICompletionsInvocationParams = model.invocationParams( + { + searchParameters: { + mode: "on", + max_search_results: 10, + from_date: "2024-01-01", + }, + } as unknown as ChatXAI["ParsedCallOptions"] + ); + + expect(params.search_parameters).toEqual({ + mode: "on", + max_search_results: 10, + from_date: "2024-01-01", + }); + }); + + test("should merge instance and call option search parameters", () => { + const model = new ChatXAI({ + searchParameters: { + mode: "auto", + max_search_results: 5, + return_citations: true, + }, + }); + + const params: ChatXAICompletionsInvocationParams = model.invocationParams( + { + searchParameters: { + max_search_results: 10, + }, + } as unknown as ChatXAI["ParsedCallOptions"] + ); + + expect(params.search_parameters).toEqual({ + mode: "auto", + max_search_results: 10, + return_citations: true, + }); + }); + + test("should not add search_parameters when no search config is present", () => { + const model = new ChatXAI(); + + const params: ChatXAICompletionsInvocationParams = model.invocationParams( + {} as ChatXAI["ParsedCallOptions"] + ); + + expect(params.search_parameters).toBeUndefined(); + }); + }); + + describe("_hasBuiltInTools", () => { + test("should return true when live_search tool is present", () => { + const model = new ChatXAI(); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const result = (model as any)._hasBuiltInTools([ + { type: "live_search" }, + { + type: "function", + function: { name: "test", parameters: {} }, + }, + ]); + expect(result).toBe(true); + }); + + test("should return false when no built-in tools are present", () => { + const model = new ChatXAI(); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const result = (model as any)._hasBuiltInTools([ + { + type: "function", + function: { name: "test", parameters: {} }, + }, + ]); + expect(result).toBe(false); + }); + + test("should return false for undefined or empty tools", () => { + const model = new ChatXAI(); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect((model as any)._hasBuiltInTools(undefined)).toBe(false); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect((model as any)._hasBuiltInTools([])).toBe(false); + }); + }); +}); From ce5d98cb9d501aaea09c7054b3130b4a49ba5fb0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Twardziak?= Date: Wed, 3 Dec 2025 16:05:26 +0100 Subject: [PATCH 02/15] Remove redundant exports --- libs/providers/langchain-xai/src/index.ts | 9 --------- 1 file changed, 9 deletions(-) diff --git a/libs/providers/langchain-xai/src/index.ts b/libs/providers/langchain-xai/src/index.ts index 6a4c0b3ed399..38c7cea7f478 100644 --- a/libs/providers/langchain-xai/src/index.ts +++ b/libs/providers/langchain-xai/src/index.ts @@ -1,10 +1 @@ export * from "./chat_models.js"; -export type { - XAILiveSearchTool, - XAIBuiltInTool, - XAISearchParameters, - ChatXAICompletionsInvocationParams, - XAIAdditionalKwargs, - XAIResponseMetadata, -} from "./chat_models.js"; -export { isXAIBuiltInTool } from "./chat_models.js"; From e95f0ec399d6134b5c1bcb7af9f104f93379e585 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Twardziak?= Date: Thu, 11 Dec 2025 00:27:51 +0100 Subject: [PATCH 03/15] feat(xai): Enables native Live Search with flexible sources Introduces native Live Search capabilities, allowing users to configure various search sources such as web, news, X, and RSS feeds. Replaces domain-based filtering with a more flexible source-based approach, providing tailored results and aligning with xAI's API updates. --- libs/providers/langchain-xai/README.md | 68 ++++- .../langchain-xai/src/chat_models.ts | 187 ++++++-------- libs/providers/langchain-xai/src/index.ts | 1 + .../langchain-xai/src/live_search.ts | 238 ++++++++++++++++++ .../src/tests/chat_models.int.test.ts | 29 +++ .../src/tests/chat_models.test.ts | 74 ++++++ .../langchain-xai/src/tools/live_search.ts | 79 ++++++ .../src/tools/tests/live_search.test.ts | 155 ++++++++++++ 8 files changed, 720 insertions(+), 111 deletions(-) create mode 100644 libs/providers/langchain-xai/src/live_search.ts create mode 100644 libs/providers/langchain-xai/src/tools/live_search.ts create mode 100644 libs/providers/langchain-xai/src/tools/tests/live_search.test.ts diff --git a/libs/providers/langchain-xai/README.md b/libs/providers/langchain-xai/README.md index b992bd0fd323..8b9b4fce8aec 100644 --- a/libs/providers/langchain-xai/README.md +++ b/libs/providers/langchain-xai/README.md @@ -79,11 +79,77 @@ const result = await model.invoke("Find recent news about SpaceX", { searchParameters: { mode: "on", max_search_results: 10, - allowed_domains: ["spacex.com", "nasa.gov"], + sources: [ + { + type: "web", + allowed_websites: ["spacex.com", "nasa.gov"], + }, + ], + }, +}); +``` + +### Configuring data sources with `sources` + +You can configure which data sources Live Search should use via the `sources` field +in `searchParameters`. Each entry corresponds to one of the sources described in the +official xAI Live Search docs (`web`, `news`, `x`, `rss`). + +```typescript +const result = await model.invoke( + "What are the latest updates from xAI and related news?", + { + searchParameters: { + mode: "on", + sources: [ + { + type: "web", + // Only search on these websites + allowed_websites: ["x.ai"], + }, + { + type: "news", + // Exclude specific news websites + excluded_websites: ["bbc.co.uk"], + }, + { + type: "x", + // Focus on specific X handles + included_x_handles: ["xai"], + }, + ], + }, + } +); +``` + +You can also use RSS feeds as a data source: + +```typescript +const result = await model.invoke("Summarize the latest posts from this feed", { + searchParameters: { + mode: "on", + sources: [ + { + type: "rss", + links: ["https://example.com/feed.rss"], + }, + ], }, }); ``` +> Notes: +> +> - The previous `allowed_domains` / `excluded_domains` fields are not +> supported in this provider. Use `sources` with `allowed_websites` and +> `excluded_websites` instead. +> - In TypeScript, the `XAISearchParameters` and `sources` types use the same +> `snake_case` field names as the underlying JSON API (for example +> `allowed_websites`, `excluded_websites`, `included_x_handles`). There are no +> separate camelCase aliases (`allowedWebsites`, etc.), which keeps the +> provider aligned with the official xAI documentation. + ### Combining live_search with custom tools ```typescript diff --git a/libs/providers/langchain-xai/src/chat_models.ts b/libs/providers/langchain-xai/src/chat_models.ts index 26c1759b7b8c..5cee5ef38d84 100644 --- a/libs/providers/langchain-xai/src/chat_models.ts +++ b/libs/providers/langchain-xai/src/chat_models.ts @@ -8,6 +8,10 @@ import { LangSmithParams, type BaseChatModelParams, } from "@langchain/core/language_models/chat_models"; +import { + isLangChainTool, + convertToOpenAITool, +} from "@langchain/core/utils/function_calling"; import { ModelProfile } from "@langchain/core/language_models/profile"; import { Serialized } from "@langchain/core/load/serializable"; import { @@ -23,6 +27,13 @@ import { type OpenAIClient, ChatOpenAICompletions, } from "@langchain/openai"; +import { + buildSearchParametersPayload, + filterXAIBuiltInTools, + mergeSearchParams, + type XAISearchParameters, + type XAISearchParametersPayload, +} from "./live_search.js"; import PROFILES from "./profiles.js"; export type OpenAIToolChoice = @@ -34,7 +45,7 @@ export type OpenAIToolChoice = * xAI's built-in live_search tool type. * Enables the model to search the web for real-time information. */ -export interface XAILiveSearchTool { +export interface XAILiveSearchTool extends XAISearchParameters { /** * The type of the tool. Must be "live_search" for xAI's built-in search. */ @@ -46,6 +57,13 @@ export interface XAILiveSearchTool { */ export type XAIBuiltInTool = XAILiveSearchTool; +/** + * Set of all supported xAI built-in server-side tool types. + * This allows us to easily extend support for future built-in tools + * without changing the core detection logic. + */ +const XAI_BUILT_IN_TOOL_TYPES = new Set(["live_search"]); + /** * Tool type that includes both standard tools and xAI built-in tools. */ @@ -54,54 +72,6 @@ type ChatXAIToolType = | OpenAIClient.ChatCompletionTool | XAIBuiltInTool; -/** - * Search parameters for xAI's Live Search API. - * Controls how the model searches for and retrieves real-time information. - * - * @note The Live Search API is being deprecated by xAI in favor of - * the agentic tool calling approach. Consider using `tools: [{ type: "live_search" }]` - * for future compatibility. - */ -export interface XAISearchParameters { - /** - * Controls when the model should perform a search. - * - "auto": Let the model decide when to search (default) - * - "on": Always search for every request - * - "off": Never search - */ - mode?: "auto" | "on" | "off"; - /** - * Maximum number of search results to return. - * @default 5 - */ - max_search_results?: number; - /** - * Filter search results to only include content from after this date. - * Format: ISO 8601 date string (e.g., "2024-01-01") - */ - from_date?: string; - /** - * Filter search results to only include content from before this date. - * Format: ISO 8601 date string (e.g., "2024-12-31") - */ - to_date?: string; - /** - * Whether to return citations/sources for the search results. - * @default true - */ - return_citations?: boolean; - /** - * Specific domains to include in the search. - * Example: ["wikipedia.org", "arxiv.org"] - */ - allowed_domains?: string[]; - /** - * Specific domains to exclude from the search. - * Example: ["reddit.com"] - */ - excluded_domains?: string[]; -} - /** * xAI-specific invocation parameters that extend the OpenAI completion params * with xAI's search_parameters field. @@ -114,15 +84,7 @@ export type ChatXAICompletionsInvocationParams = Omit< * Search parameters for xAI's Live Search API. * When present, enables the model to search the web for real-time information. */ - search_parameters?: { - mode: "auto" | "on" | "off"; - max_search_results?: number; - from_date?: string; - to_date?: string; - return_citations?: boolean; - allowed_domains?: string[]; - excluded_domains?: string[]; - }; + search_parameters?: XAISearchParametersPayload; }; /** @@ -173,7 +135,8 @@ export function isXAIBuiltInTool( typeof tool === "object" && tool !== null && "type" in tool && - (tool as XAIBuiltInTool).type === "live_search" + typeof (tool as { type?: unknown }).type === "string" && + XAI_BUILT_IN_TOOL_TYPES.has((tool as { type: string }).type) ); } @@ -635,7 +598,9 @@ export interface ChatXAIInput extends BaseChatModelParams { * searchParameters: { * mode: "on", * max_search_results: 10, - * allowed_domains: ["spacex.com", "nasa.gov"], + * sources: [ + * { type: "web", allowed_websites: ["spacex.com", "nasa.gov"] }, + * ], * } * }); * ``` @@ -716,15 +681,7 @@ export class ChatXAI extends ChatOpenAICompletions { protected _getEffectiveSearchParameters( options?: this["ParsedCallOptions"] ): XAISearchParameters | undefined { - const callSearchParams = options?.searchParameters; - if (!this.searchParameters && !callSearchParams) { - return undefined; - } - // Merge instance-level with call-level, call-level takes precedence - return { - ...this.searchParameters, - ...callSearchParams, - }; + return mergeSearchParams(this.searchParameters, options?.searchParameters); } /** @@ -736,6 +693,42 @@ export class ChatXAI extends ChatOpenAICompletions { return tools?.some(isXAIBuiltInTool) ?? false; } + /** + * Formats tools to xAI/OpenAI format, preserving provider-specific definitions. + * + * @param tools The tools to format + * @returns The formatted tools + */ + formatStructuredToolToXAI( + tools: ChatXAIToolType[] + ): (OpenAIClient.ChatCompletionTool | XAIBuiltInTool)[] | undefined { + if (!tools || !tools.length) { + return undefined; + } + return tools.map((tool) => { + // 1. Check for provider definition first (from xaiLiveSearch factory) + if (isLangChainTool(tool) && tool.extras?.providerToolDefinition) { + return tool.extras.providerToolDefinition as XAIBuiltInTool; + } + // 2. Check for built-in tools (legacy { type: "live_search" }) + if (isXAIBuiltInTool(tool)) { + return tool; + } + // 3. Convert standard tools to OpenAI format + return convertToOpenAITool(tool) as OpenAIClient.ChatCompletionTool; + }); + } + + override bindTools( + tools: ChatXAIToolType[], + kwargs?: Partial + ): Runnable { + return this.withConfig({ + tools: this.formatStructuredToolToXAI(tools), + ...kwargs, + } as Partial); + } + /** @internal */ override invocationParams( options?: this["ParsedCallOptions"], @@ -746,43 +739,22 @@ export class ChatXAI extends ChatOpenAICompletions { // Cast to xAI-specific params type const params: ChatXAICompletionsInvocationParams = { ...baseParams }; - // Get effective search parameters from instance and call options - const effectiveSearchParams = this._getEffectiveSearchParameters(options); - // Check if live_search tool is being used - const hasLiveSearchTool = this._hasBuiltInTools( - options?.tools as ChatXAIToolType[] | undefined + // We also need to extract params from the tool definition if present + const liveSearchTool = options?.tools?.find(isXAIBuiltInTool) as + | XAILiveSearchTool + | undefined; + + const mergedSearchParams = mergeSearchParams( + this.searchParameters, + options?.searchParameters, + liveSearchTool ); // Add search_parameters if needed - if (effectiveSearchParams || hasLiveSearchTool) { - const searchParams = hasLiveSearchTool - ? { mode: "auto" as const, ...effectiveSearchParams } - : effectiveSearchParams; - - if (searchParams) { - params.search_parameters = { - mode: searchParams.mode ?? "auto", - ...(searchParams.max_search_results !== undefined && { - max_search_results: searchParams.max_search_results, - }), - ...(searchParams.from_date !== undefined && { - from_date: searchParams.from_date, - }), - ...(searchParams.to_date !== undefined && { - to_date: searchParams.to_date, - }), - ...(searchParams.return_citations !== undefined && { - return_citations: searchParams.return_citations, - }), - ...(searchParams.allowed_domains !== undefined && { - allowed_domains: searchParams.allowed_domains, - }), - ...(searchParams.excluded_domains !== undefined && { - excluded_domains: searchParams.excluded_domains, - }), - }; - } + if (mergedSearchParams) { + params.search_parameters = + buildSearchParametersPayload(mergedSearchParams); } return params; @@ -828,16 +800,11 @@ export class ChatXAI extends ChatOpenAICompletions { return msg; }); - // Filter out xAI built-in tools from the standard tools array - // Built-in tools are handled via search_parameters (added in invocationParams) let filteredTools: OpenAIClient.ChatCompletionTool[] | undefined; if (request.tools) { - filteredTools = request.tools.filter( - (tool) => !isXAIBuiltInTool(tool) - ) as OpenAIClient.ChatCompletionTool[]; - if (filteredTools.length === 0) { - filteredTools = undefined; - } + filteredTools = filterXAIBuiltInTools(request.tools) as + | OpenAIClient.ChatCompletionTool[] + | undefined; } const newRequest = { diff --git a/libs/providers/langchain-xai/src/index.ts b/libs/providers/langchain-xai/src/index.ts index 38c7cea7f478..1228c1604ce4 100644 --- a/libs/providers/langchain-xai/src/index.ts +++ b/libs/providers/langchain-xai/src/index.ts @@ -1 +1,2 @@ export * from "./chat_models.js"; +export * from "./tools/live_search.js"; diff --git a/libs/providers/langchain-xai/src/live_search.ts b/libs/providers/langchain-xai/src/live_search.ts new file mode 100644 index 000000000000..edeb21f7178c --- /dev/null +++ b/libs/providers/langchain-xai/src/live_search.ts @@ -0,0 +1,238 @@ +/** + * Web search source configuration for xAI Live Search. + * Corresponds to a `{"type": "web", ...}` entry in `search_parameters.sources`. + */ +export interface XAIWebSource { + type: "web"; + /** + * Optional ISO alpha-2 country code used to bias results + * towards a specific country/region. + */ + country?: string; + /** + * Websites that should be excluded from the search results. + * Maximum of 5 entries. + */ + excluded_websites?: string[]; + /** + * Websites that should be exclusively included in the search results. + * Maximum of 5 entries. + */ + allowed_websites?: string[]; + /** + * Whether to enable safe search filtering for this source. + */ + safe_search?: boolean; +} + +/** + * News search source configuration for xAI Live Search. + * Corresponds to a `{"type": "news", ...}` entry in `search_parameters.sources`. + */ +export interface XAINewsSource { + type: "news"; + /** + * Optional ISO alpha-2 country code used to bias results + * towards a specific country/region. + */ + country?: string; + /** + * Websites that should be excluded from the search results. + * Maximum of 5 entries. + */ + excluded_websites?: string[]; + /** + * Whether to enable safe search filtering for this source. + */ + safe_search?: boolean; +} + +/** + * X (formerly Twitter) search source configuration for xAI Live Search. + * Corresponds to a `{"type": "x", ...}` entry in `search_parameters.sources`. + */ +export interface XAIXSource { + type: "x"; + /** + * X handles that should be explicitly included in the search. + * Maximum of 10 entries. + */ + included_x_handles?: string[]; + /** + * X handles that should be excluded from the search. + * Maximum of 10 entries. + */ + excluded_x_handles?: string[]; + /** + * Minimum number of favorites a post must have to be included. + */ + post_favorite_count?: number; + /** + * Minimum number of views a post must have to be included. + */ + post_view_count?: number; +} + +/** + * RSS feed search source configuration for xAI Live Search. + * Corresponds to a `{"type": "rss", ...}` entry in `search_parameters.sources`. + */ +export interface XAIRssSource { + type: "rss"; + /** + * Links to RSS feeds to be used as a data source. + * The API currently expects a single URL. + */ + links: string[]; +} + +export type XAISearchSource = + | XAIWebSource + | XAINewsSource + | XAIXSource + | XAIRssSource; + +/** + * Search parameters for xAI's Live Search API. + * Controls how the model searches for and retrieves real-time information. + * + * @note The Live Search API is being deprecated by xAI in favor of + * the agentic tool calling approach. Consider using `tools: [{ type: "live_search" }]` + * for future compatibility. + */ +export interface XAISearchParameters { + /** + * Controls when the model should perform a search. + * - "auto": Let the model decide when to search (default) + * - "on": Always search for every request + * - "off": Never search + */ + mode?: "auto" | "on" | "off"; + /** + * Maximum number of search results to return. + * @default 20 + */ + max_search_results?: number; + /** + * Filter search results to only include content from after this date. + * Format: ISO 8601 date string (e.g., "2024-01-01") + */ + from_date?: string; + /** + * Filter search results to only include content from before this date. + * Format: ISO 8601 date string (e.g., "2024-12-31") + */ + to_date?: string; + /** + * Whether to return citations/sources for the search results. + * @default true + */ + return_citations?: boolean; + /** + * Specific web/news/X/RSS sources that can be used for the search. + * Each entry corresponds to a `{"type": "...", ...}` object in + * `search_parameters.sources` as documented in the xAI Live Search docs. + * + * If omitted, xAI will default to enabling `web`, `news` and `x` sources. + */ + sources?: XAISearchSource[]; +} + +/** + * Concrete payload shape sent as `search_parameters` to the xAI API. + */ +export interface XAISearchParametersPayload { + mode: "auto" | "on" | "off"; + max_search_results?: number; + from_date?: string; + to_date?: string; + return_citations?: boolean; + sources?: XAISearchSource[]; +} + +/** + * Merge search parameters from instance defaults, tool definition + * and per-call overrides. + * + * Precedence (lowest → highest): + * 1. tool-level configuration (e.g. from xaiLiveSearch) + * 2. instance-level defaults + * 3. per-call overrides passed via `searchParameters` + */ +export function mergeSearchParams( + instanceParams?: XAISearchParameters, + callParams?: XAISearchParameters, + toolParams?: XAISearchParameters +): XAISearchParameters | undefined { + if (!instanceParams && !callParams && !toolParams) { + return undefined; + } + + return { + ...(toolParams ?? {}), + ...(instanceParams ?? {}), + ...(callParams ?? {}), + }; +} + +/** + * Build the `search_parameters` payload to send to the xAI API + * from high-level `XAISearchParameters`. + */ +export function buildSearchParametersPayload( + params?: XAISearchParameters +): XAISearchParametersPayload | undefined { + if (!params) { + return undefined; + } + + const payload: XAISearchParametersPayload = { + mode: params.mode ?? "auto", + }; + + if (params.max_search_results !== undefined) { + payload.max_search_results = params.max_search_results; + } + if (params.from_date !== undefined) { + payload.from_date = params.from_date; + } + if (params.to_date !== undefined) { + payload.to_date = params.to_date; + } + if (params.return_citations !== undefined) { + payload.return_citations = params.return_citations; + } + if (params.sources && params.sources.length > 0) { + payload.sources = params.sources; + } + + return payload; +} + +/** + * Filter out xAI built-in tools (like `live_search`) from a tools array. + * Used before sending the request to the xAI API, since built-in tools + * are controlled via `search_parameters` instead. + */ +export function filterXAIBuiltInTools< + // eslint-disable-next-line @typescript-eslint/no-explicit-any + T extends { [key: string]: any } +>(tools?: T[]): T[] | undefined { + if (!tools) { + return undefined; + } + + const filtered = tools.filter((tool) => { + if (tool == null || typeof tool !== "object") { + return true; + } + + if (!("type" in tool)) { + return true; + } + + return (tool as { type?: unknown }).type !== "live_search"; + }); + + return filtered.length > 0 ? filtered : undefined; +} diff --git a/libs/providers/langchain-xai/src/tests/chat_models.int.test.ts b/libs/providers/langchain-xai/src/tests/chat_models.int.test.ts index 3024119d7e7f..ad08fe7c9355 100644 --- a/libs/providers/langchain-xai/src/tests/chat_models.int.test.ts +++ b/libs/providers/langchain-xai/src/tests/chat_models.int.test.ts @@ -288,6 +288,35 @@ describe("Server Tool Calling (Live Search)", () => { expect((res.content as string).length).toBeGreaterThan(50); }); + test("invoke with searchParameters sources in call options", async () => { + const chat = new ChatXAI({ + maxRetries: 0, + model: "grok-2-1212", + }); + + const message = new HumanMessage( + "What are the latest updates from xAI and related news?" + ); + const res = await chat.invoke([message], { + searchParameters: { + mode: "on", + sources: [ + { + type: "web", + allowed_websites: ["x.ai"], + }, + { + type: "news", + excluded_websites: ["bbc.co.uk"], + }, + ], + }, + }); + + expect(res.content).toBeDefined(); + expect((res.content as string).length).toBeGreaterThan(50); + }); + test("stream with live_search tool", async () => { const chat = new ChatXAI({ maxRetries: 0, diff --git a/libs/providers/langchain-xai/src/tests/chat_models.test.ts b/libs/providers/langchain-xai/src/tests/chat_models.test.ts index 7e0bf8f293ed..9a4615aedada 100644 --- a/libs/providers/langchain-xai/src/tests/chat_models.test.ts +++ b/libs/providers/langchain-xai/src/tests/chat_models.test.ts @@ -138,6 +138,80 @@ describe("Server Tool Calling", () => { }); }); + test("should include sources in search_parameters when provided", () => { + const model = new ChatXAI(); + + const params: ChatXAICompletionsInvocationParams = model.invocationParams( + { + searchParameters: { + mode: "on", + sources: [ + { + type: "web", + allowed_websites: ["x.ai"], + }, + { + type: "news", + excluded_websites: ["bbc.co.uk"], + }, + { + type: "x", + included_x_handles: ["xai"], + }, + { + type: "rss", + links: ["https://example.com/feed.rss"], + }, + ], + }, + } as unknown as ChatXAI["ParsedCallOptions"] + ); + + expect(params.search_parameters).toBeDefined(); + expect(params.search_parameters?.sources).toEqual([ + { + type: "web", + allowed_websites: ["x.ai"], + }, + { + type: "news", + excluded_websites: ["bbc.co.uk"], + }, + { + type: "x", + included_x_handles: ["xai"], + }, + { + type: "rss", + links: ["https://example.com/feed.rss"], + }, + ]); + }); + + test("should omit sources field when none are configured", () => { + const model = new ChatXAI(); + + const params: ChatXAICompletionsInvocationParams = model.invocationParams( + { + searchParameters: { + mode: "auto", + }, + } as unknown as ChatXAI["ParsedCallOptions"] + ); + + expect(params.search_parameters).toEqual({ + mode: "auto", + }); + expect( + Object.prototype.hasOwnProperty.call( + params.search_parameters as NonNullable< + ChatXAICompletionsInvocationParams["search_parameters"] + >, + "sources" + ) + ).toBe(false); + }); + test("should merge instance and call option search parameters", () => { const model = new ChatXAI({ searchParameters: { diff --git a/libs/providers/langchain-xai/src/tools/live_search.ts b/libs/providers/langchain-xai/src/tools/live_search.ts new file mode 100644 index 000000000000..2f96475f3ecd --- /dev/null +++ b/libs/providers/langchain-xai/src/tools/live_search.ts @@ -0,0 +1,79 @@ +import { tool } from "@langchain/core/tools"; +import type { DynamicStructuredTool, ToolRuntime } from "@langchain/core/tools"; +import type { XAISearchParameters } from "../live_search.js"; + +/** + * Options for the xAI live search tool. + * Controls how the model searches for and retrieves real-time information. + */ +export interface XAILiveSearchToolOptions extends XAISearchParameters { + /** + * Optional execute function that handles the search execution. + * Since xAI search is server-side, this is typically not used for execution + * but can be provided for compatibility. + */ + execute?: (args: unknown) => Promise | string; +} + +/** + * Creates an xAI built-in live search tool. + * Enables the model to search the web for real-time information. + * + * This tool is executed server-side by the xAI API. + * + * @example + * ```typescript + * import { ChatXAI, xaiLiveSearch } from "@langchain/xai"; + * + * const llm = new ChatXAI({ + * model: "grok-beta", + * }); + * + * const searchTool = xaiLiveSearch({ + * max_search_results: 5, + * from_date: "2024-01-01", + * return_citations: true + * }); + * + * const llmWithSearch = llm.bindTools([searchTool]); + * const result = await llmWithSearch.invoke("What happened in tech today?"); + * ``` + */ +export function xaiLiveSearch( + options: XAILiveSearchToolOptions = {} +): DynamicStructuredTool { + const { execute, ...searchParams } = options; + const name = "live_search"; + + const searchTool = tool( + (async () => { + // This is a server-side tool, so client-side execution is a no-op + // unless a custom executor is provided. + if (execute) { + return execute({}); + } + return "This tool is executed server-side by xAI."; + }) as ( + input: unknown, + runtime: ToolRuntime + ) => string | Promise, + { + name, + description: "Search the web for real-time information.", + schema: { + type: "object", + properties: {}, + }, + } + ); + + searchTool.extras = { + ...(searchTool.extras ?? {}), + providerToolDefinition: { + type: "live_search", + ...searchParams, + }, + }; + + return searchTool; +} diff --git a/libs/providers/langchain-xai/src/tools/tests/live_search.test.ts b/libs/providers/langchain-xai/src/tools/tests/live_search.test.ts new file mode 100644 index 000000000000..5a866e459722 --- /dev/null +++ b/libs/providers/langchain-xai/src/tools/tests/live_search.test.ts @@ -0,0 +1,155 @@ +import { test, expect, describe } from "vitest"; +import { xaiLiveSearch } from "../live_search.js"; +import { ChatXAI } from "../../chat_models.js"; + +describe("xaiLiveSearch tool", () => { + test("creates a tool with correct provider definition", async () => { + const tool = xaiLiveSearch({ + max_search_results: 10, + from_date: "2024-01-01", + return_citations: true, + }); + + expect(tool.name).toBe("live_search"); + expect(tool.extras).toBeDefined(); + expect(tool.extras?.providerToolDefinition).toEqual({ + type: "live_search", + max_search_results: 10, + from_date: "2024-01-01", + return_citations: true, + }); + }); + + test("creates a tool with default options", async () => { + const tool = xaiLiveSearch(); + expect(tool.name).toBe("live_search"); + expect(tool.extras?.providerToolDefinition).toEqual({ + type: "live_search", + }); + }); + + test("creates a tool with web and news sources using excluded_websites", async () => { + const tool = xaiLiveSearch({ + sources: [ + { + type: "web", + excluded_websites: ["wikipedia.org"], + }, + { + type: "news", + excluded_websites: ["bbc.co.uk"], + }, + ], + }); + + expect(tool.name).toBe("live_search"); + expect(tool.extras?.providerToolDefinition).toEqual({ + type: "live_search", + sources: [ + { + type: "web", + excluded_websites: ["wikipedia.org"], + }, + { + type: "news", + excluded_websites: ["bbc.co.uk"], + }, + ], + }); + }); +}); + +describe("ChatXAI with xaiLiveSearch tool", () => { + test("formatStructuredToolToXAI preserves provider definition", () => { + const model = new ChatXAI({ apiKey: "foo" }); + const searchTool = xaiLiveSearch({ + max_search_results: 8, + sources: [ + { + type: "web", + allowed_websites: ["example.com"], + }, + ], + }); + + // Access protected method for testing + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const formattedTools = (model as any).formatStructuredToolToXAI([ + searchTool, + ]); + + expect(formattedTools).toHaveLength(1); + expect(formattedTools[0]).toEqual({ + type: "live_search", + max_search_results: 8, + sources: [ + { + type: "web", + allowed_websites: ["example.com"], + }, + ], + }); + }); + + test("invocationParams extracts parameters from formatted tools", () => { + const model = new ChatXAI({ apiKey: "foo" }); + + // Simulate the tools being passed in options (as they would be after bindTools -> withConfig -> invoke) + const tools = [ + { + type: "live_search", + max_search_results: 8, + sources: [ + { + type: "web", + allowed_websites: ["example.com"], + }, + ], + }, + ]; + + // Access protected method for testing + const params = model.invocationParams({ + tools: tools as never, + } as never); + + expect(params.search_parameters).toEqual({ + mode: "auto", + max_search_results: 8, + sources: [ + { + type: "web", + allowed_websites: ["example.com"], + }, + ], + }); + }); + + test("explicit searchParameters override tool parameters", () => { + const model = new ChatXAI({ + apiKey: "foo", + searchParameters: { + mode: "on", + max_search_results: 5, // This should override the tool's 10 + }, + }); + + const tools = [ + { + type: "live_search", + max_search_results: 10, + from_date: "2024-01-01", + }, + ]; + + const params = model.invocationParams({ + tools: tools as never, + } as never); + + expect(params.search_parameters).toEqual({ + mode: "on", + max_search_results: 5, // Overridden by constructor/option + from_date: "2024-01-01", // Preserved from a tool + }); + }); +}); From 9f4c6eb16370db369289e06a8b43663d8ec7ef7a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Twardziak?= Date: Thu, 11 Dec 2025 00:34:31 +0100 Subject: [PATCH 04/15] refactor(xai): Moves XAILiveSearchTool definition Moves the XAILiveSearchTool interface definition to the live_search tool file. This improves code organization and maintainability by grouping related tool definitions together. --- libs/providers/langchain-xai/src/chat_models.ts | 12 +----------- .../langchain-xai/src/tests/chat_models.test.ts | 4 ++-- .../providers/langchain-xai/src/tools/live_search.ts | 11 +++++++++++ 3 files changed, 14 insertions(+), 13 deletions(-) diff --git a/libs/providers/langchain-xai/src/chat_models.ts b/libs/providers/langchain-xai/src/chat_models.ts index 5cee5ef38d84..fe8f73fea911 100644 --- a/libs/providers/langchain-xai/src/chat_models.ts +++ b/libs/providers/langchain-xai/src/chat_models.ts @@ -35,23 +35,13 @@ import { type XAISearchParametersPayload, } from "./live_search.js"; import PROFILES from "./profiles.js"; +import { XAILiveSearchTool } from "./tools/live_search.js"; export type OpenAIToolChoice = | OpenAIClient.ChatCompletionToolChoiceOption | "any" | string; -/** - * xAI's built-in live_search tool type. - * Enables the model to search the web for real-time information. - */ -export interface XAILiveSearchTool extends XAISearchParameters { - /** - * The type of the tool. Must be "live_search" for xAI's built-in search. - */ - type: "live_search"; -} - /** * Union type for all xAI built-in server-side tools. */ diff --git a/libs/providers/langchain-xai/src/tests/chat_models.test.ts b/libs/providers/langchain-xai/src/tests/chat_models.test.ts index 9a4615aedada..5578fdde4bc1 100644 --- a/libs/providers/langchain-xai/src/tests/chat_models.test.ts +++ b/libs/providers/langchain-xai/src/tests/chat_models.test.ts @@ -2,10 +2,10 @@ import { test, expect, beforeEach, describe } from "vitest"; import { ChatXAI, isXAIBuiltInTool, - type XAILiveSearchTool, - type XAISearchParameters, type ChatXAICompletionsInvocationParams, } from "../chat_models.js"; +import { XAISearchParameters } from "../live_search.js"; +import { XAILiveSearchTool } from "../tools/live_search.js"; beforeEach(() => { process.env.XAI_API_KEY = "foo"; diff --git a/libs/providers/langchain-xai/src/tools/live_search.ts b/libs/providers/langchain-xai/src/tools/live_search.ts index 2f96475f3ecd..df47107bed10 100644 --- a/libs/providers/langchain-xai/src/tools/live_search.ts +++ b/libs/providers/langchain-xai/src/tools/live_search.ts @@ -2,6 +2,17 @@ import { tool } from "@langchain/core/tools"; import type { DynamicStructuredTool, ToolRuntime } from "@langchain/core/tools"; import type { XAISearchParameters } from "../live_search.js"; +/** + * xAI's built-in live_search tool type. + * Enables the model to search the web for real-time information. + */ +export interface XAILiveSearchTool extends XAISearchParameters { + /** + * The type of the tool. Must be "live_search" for xAI's built-in search. + */ + type: "live_search"; +} + /** * Options for the xAI live search tool. * Controls how the model searches for and retrieves real-time information. From 9905ac766ba9e4f00eb9aaf37f4aa737387100c2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Twardziak?= Date: Thu, 11 Dec 2025 00:59:19 +0100 Subject: [PATCH 05/15] chore(xai): Adds usage example for `xaiLiveSearch` tool Adds an example demonstrating how to use the built-in `xaiLiveSearch` tool with optional parameters. This enhances clarity and provides a practical guide for users integrating the `live_search` functionality. --- libs/providers/langchain-xai/README.md | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/libs/providers/langchain-xai/README.md b/libs/providers/langchain-xai/README.md index 8b9b4fce8aec..5e7c6eae3a75 100644 --- a/libs/providers/langchain-xai/README.md +++ b/libs/providers/langchain-xai/README.md @@ -38,14 +38,20 @@ xAI supports server-side tools that are executed by the API rather than requirin ### Using the built-in live_search tool ```typescript -import { ChatXAI } from "@langchain/xai"; +import { ChatXAI, xaiLiveSearch } from "@langchain/xai"; const model = new ChatXAI({ model: "grok-2-1212", }); -// Bind the live_search tool -const modelWithSearch = model.bindTools([{ type: "live_search" }]); +// Create the built-in live_search tool with optional parameters +const searchTool = xaiLiveSearch({ + max_search_results: 5, + return_citations: true, +}); + +// Bind the live_search tool to the model +const modelWithSearch = model.bindTools([searchTool]); // The model will search the web for real-time information const result = await modelWithSearch.invoke( From 7d8abbc6fd0b6cc891cd8276988de8796ebf2672 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Twardziak?= Date: Thu, 11 Dec 2025 01:13:52 +0100 Subject: [PATCH 06/15] refactor(xai): Exports tools via a dedicated module Refactors the xAI provider to export the tools via a dedicated module. This change improves code organization and makes it easier to import and use the available tools. The `xaiLiveSearch` tool is now accessed through `tools.xaiLiveSearch` instead of direct import. --- libs/providers/langchain-xai/README.md | 4 ++-- libs/providers/langchain-xai/src/index.ts | 2 +- libs/providers/langchain-xai/src/tools/index.ts | 5 +++++ 3 files changed, 8 insertions(+), 3 deletions(-) create mode 100644 libs/providers/langchain-xai/src/tools/index.ts diff --git a/libs/providers/langchain-xai/README.md b/libs/providers/langchain-xai/README.md index 5e7c6eae3a75..0f0714ec4b68 100644 --- a/libs/providers/langchain-xai/README.md +++ b/libs/providers/langchain-xai/README.md @@ -38,14 +38,14 @@ xAI supports server-side tools that are executed by the API rather than requirin ### Using the built-in live_search tool ```typescript -import { ChatXAI, xaiLiveSearch } from "@langchain/xai"; +import { ChatXAI, tools } from "@langchain/xai"; const model = new ChatXAI({ model: "grok-2-1212", }); // Create the built-in live_search tool with optional parameters -const searchTool = xaiLiveSearch({ +const searchTool = tools.xaiLiveSearch({ max_search_results: 5, return_citations: true, }); diff --git a/libs/providers/langchain-xai/src/index.ts b/libs/providers/langchain-xai/src/index.ts index 1228c1604ce4..7fc19f5d2bdc 100644 --- a/libs/providers/langchain-xai/src/index.ts +++ b/libs/providers/langchain-xai/src/index.ts @@ -1,2 +1,2 @@ export * from "./chat_models.js"; -export * from "./tools/live_search.js"; +export { tools } from "./tools/index.js"; diff --git a/libs/providers/langchain-xai/src/tools/index.ts b/libs/providers/langchain-xai/src/tools/index.ts new file mode 100644 index 000000000000..a9828b7ae7fb --- /dev/null +++ b/libs/providers/langchain-xai/src/tools/index.ts @@ -0,0 +1,5 @@ +import { xaiLiveSearch } from "./live_search.js"; + +export const tools = { + xaiLiveSearch, +}; From de967b76a98b60bcf1cf2a5fb84e2afb026aadc6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Twardziak?= Date: Thu, 11 Dec 2025 12:59:15 +0100 Subject: [PATCH 07/15] refactor(xai): Simplifies xAI live search tool options Converts snake_case `xaiLiveSearch` parameters to camelCase for easier use and configuration. Simplifies tool configuration by accepting user-friendly options, and automatically translates them into the format expected by the xAI API. --- .../src/tests/chat_models.int.test.ts | 3 +- .../langchain-xai/src/tools/live_search.ts | 74 ++++++++++++++----- .../src/tools/tests/live_search.test.ts | 8 +- 3 files changed, 61 insertions(+), 24 deletions(-) diff --git a/libs/providers/langchain-xai/src/tests/chat_models.int.test.ts b/libs/providers/langchain-xai/src/tests/chat_models.int.test.ts index ad08fe7c9355..0de55c0cd1bc 100644 --- a/libs/providers/langchain-xai/src/tests/chat_models.int.test.ts +++ b/libs/providers/langchain-xai/src/tests/chat_models.int.test.ts @@ -10,7 +10,8 @@ import { import { tool } from "@langchain/core/tools"; import { concat } from "@langchain/core/utils/stream"; -import { ChatXAI, type XAILiveSearchTool } from "../chat_models.js"; +import { ChatXAI } from "../chat_models.js"; +import { type XAILiveSearchTool } from "../tools/live_search.js"; test("invoke", async () => { const chat = new ChatXAI({ diff --git a/libs/providers/langchain-xai/src/tools/live_search.ts b/libs/providers/langchain-xai/src/tools/live_search.ts index df47107bed10..069eb3734089 100644 --- a/libs/providers/langchain-xai/src/tools/live_search.ts +++ b/libs/providers/langchain-xai/src/tools/live_search.ts @@ -1,6 +1,6 @@ import { tool } from "@langchain/core/tools"; import type { DynamicStructuredTool, ToolRuntime } from "@langchain/core/tools"; -import type { XAISearchParameters } from "../live_search.js"; +import type { XAISearchParameters, XAISearchSource } from "../live_search.js"; /** * xAI's built-in live_search tool type. @@ -14,16 +14,43 @@ export interface XAILiveSearchTool extends XAISearchParameters { } /** - * Options for the xAI live search tool. - * Controls how the model searches for and retrieves real-time information. + * Options for the xAI live search tool (camelCase). + * These are converted to the snake_case `XAISearchParameters` used internally. */ -export interface XAILiveSearchToolOptions extends XAISearchParameters { +export interface XAILiveSearchToolOptions { /** - * Optional execute function that handles the search execution. - * Since xAI search is server-side, this is typically not used for execution - * but can be provided for compatibility. + * Controls when the model should perform a search. + * - "auto": Let the model decide when to search (default) + * - "on": Always search for every request + * - "off": Never search */ - execute?: (args: unknown) => Promise | string; + mode?: "auto" | "on" | "off"; + /** + * Maximum number of search results to return. + * @default 20 + */ + maxSearchResults?: number; + /** + * Filter search results to only include content from after this date. + * Format: ISO 8601 date string (e.g., "2024-01-01") + */ + fromDate?: string; + /** + * Filter search results to only include content from before this date. + * Format: ISO 8601 date string (e.g., "2024-12-31") + */ + toDate?: string; + /** + * Whether to return citations/sources for the search results. + * @default true + */ + returnCitations?: boolean; + /** + * Specific web/news/X/RSS sources that can be used for the search. + * These use the same structure as `XAISearchSource` (snake_case fields) + * since they are passed directly to the JSON API. + */ + sources?: XAISearchSource[]; } /** @@ -34,16 +61,16 @@ export interface XAILiveSearchToolOptions extends XAISearchParameters { * * @example * ```typescript - * import { ChatXAI, xaiLiveSearch } from "@langchain/xai"; + * import { ChatXAI, tools } from "@langchain/xai"; * * const llm = new ChatXAI({ * model: "grok-beta", * }); * - * const searchTool = xaiLiveSearch({ - * max_search_results: 5, - * from_date: "2024-01-01", - * return_citations: true + * const searchTool = tools.xaiLiveSearch({ + * maxSearchResults: 5, + * fromDate: "2024-01-01", + * returnCitations: true * }); * * const llmWithSearch = llm.bindTools([searchTool]); @@ -53,16 +80,25 @@ export interface XAILiveSearchToolOptions extends XAISearchParameters { export function xaiLiveSearch( options: XAILiveSearchToolOptions = {} ): DynamicStructuredTool { - const { execute, ...searchParams } = options; const name = "live_search"; + const searchParams: XAISearchParameters = { + ...(options.mode !== undefined && { mode: options.mode }), + ...(options.maxSearchResults !== undefined && { + max_search_results: options.maxSearchResults, + }), + ...(options.fromDate !== undefined && { from_date: options.fromDate }), + ...(options.toDate !== undefined && { to_date: options.toDate }), + ...(options.returnCitations !== undefined && { + return_citations: options.returnCitations, + }), + ...(options.sources !== undefined && { sources: options.sources }), + }; + const searchTool = tool( (async () => { - // This is a server-side tool, so client-side execution is a no-op - // unless a custom executor is provided. - if (execute) { - return execute({}); - } + // This is a server-side tool; the actual search is executed by xAI + // based on the `search_parameters` we send via the ChatXAI provider. return "This tool is executed server-side by xAI."; }) as ( input: unknown, diff --git a/libs/providers/langchain-xai/src/tools/tests/live_search.test.ts b/libs/providers/langchain-xai/src/tools/tests/live_search.test.ts index 5a866e459722..8060f05d5572 100644 --- a/libs/providers/langchain-xai/src/tools/tests/live_search.test.ts +++ b/libs/providers/langchain-xai/src/tools/tests/live_search.test.ts @@ -5,9 +5,9 @@ import { ChatXAI } from "../../chat_models.js"; describe("xaiLiveSearch tool", () => { test("creates a tool with correct provider definition", async () => { const tool = xaiLiveSearch({ - max_search_results: 10, - from_date: "2024-01-01", - return_citations: true, + maxSearchResults: 10, + fromDate: "2024-01-01", + returnCitations: true, }); expect(tool.name).toBe("live_search"); @@ -63,7 +63,7 @@ describe("ChatXAI with xaiLiveSearch tool", () => { test("formatStructuredToolToXAI preserves provider definition", () => { const model = new ChatXAI({ apiKey: "foo" }); const searchTool = xaiLiveSearch({ - max_search_results: 8, + maxSearchResults: 8, sources: [ { type: "web", From 81b85abf1531dd966a10bb4edf0050357976dd5b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Twardziak?= Date: Thu, 11 Dec 2025 13:40:15 +0100 Subject: [PATCH 08/15] refactor(xai): Refactors xAI live search tool Improves the xAI live search tool by adopting the `ServerTool` interface. This change aligns with the xAI API's server-side execution model and simplifies the tool definition. It also addresses upcoming deprecation of the Live Search API. Removes the previous `DynamicStructuredTool` implementation and adds support for specifying web, news, X, and RSS sources. --- .../langchain-xai/src/tools/live_search.ts | 168 ++++++++++++++---- .../src/tools/tests/live_search.test.ts | 20 +-- 2 files changed, 139 insertions(+), 49 deletions(-) diff --git a/libs/providers/langchain-xai/src/tools/live_search.ts b/libs/providers/langchain-xai/src/tools/live_search.ts index 069eb3734089..475eaa48ea3d 100644 --- a/libs/providers/langchain-xai/src/tools/live_search.ts +++ b/libs/providers/langchain-xai/src/tools/live_search.ts @@ -1,5 +1,4 @@ -import { tool } from "@langchain/core/tools"; -import type { DynamicStructuredTool, ToolRuntime } from "@langchain/core/tools"; +import { type ServerTool } from "@langchain/core/tools"; import type { XAISearchParameters, XAISearchSource } from "../live_search.js"; /** @@ -8,11 +7,68 @@ import type { XAISearchParameters, XAISearchSource } from "../live_search.js"; */ export interface XAILiveSearchTool extends XAISearchParameters { /** - * The type of the tool. Must be "live_search" for xAI's built-in search. + * The name of the tool. Must be "live_search" for xAI's built-in search. */ - type: "live_search"; + name: "live_search"; + /** + * The type of the tool. This uses a deprecated Live Search API shape: + * the advanced agentic search capabilities powering grok.com are generally + * available in the new agentic tool calling API, and the Live Search API + * will be deprecated by December 15, 2025. + */ + type: "live_search_deprecated_20251215"; +} + +/** + * Web search source configuration for the xAI live search tool (camelCase). + * This is converted to the snake_case `XAIWebSource` internally. + */ +export interface XAIWebSearchToolSource { + type: "web"; + country?: string; + excludedWebsites?: string[]; + allowedWebsites?: string[]; + safeSearch?: boolean; +} + +/** + * News search source configuration for the xAI live search tool (camelCase). + * This is converted to the snake_case `XAINewsSource` internally. + */ +export interface XAINewsSearchToolSource { + type: "news"; + country?: string; + excludedWebsites?: string[]; + safeSearch?: boolean; +} + +/** + * X (formerly Twitter) search source configuration for the xAI live search tool (camelCase). + * This is converted to the snake_case `XAIXSource` internally. + */ +export interface XAIXSearchToolSource { + type: "x"; + includedXHandles?: string[]; + excludedXHandles?: string[]; + postFavoriteCount?: number; + postViewCount?: number; } +/** + * RSS feed search source configuration for the xAI live search tool. + * The structure matches `XAIRssSource` (only `links`). + */ +export interface XAIRssSearchToolSource { + type: "rss"; + links: string[]; +} + +export type XAISearchToolSource = + | XAIWebSearchToolSource + | XAINewsSearchToolSource + | XAIXSearchToolSource + | XAIRssSearchToolSource; + /** * Options for the xAI live search tool (camelCase). * These are converted to the snake_case `XAISearchParameters` used internally. @@ -47,10 +103,67 @@ export interface XAILiveSearchToolOptions { returnCitations?: boolean; /** * Specific web/news/X/RSS sources that can be used for the search. - * These use the same structure as `XAISearchSource` (snake_case fields) - * since they are passed directly to the JSON API. + * These are converted to the snake_case `XAISearchSource` structures + * used by the underlying xAI Live Search API. */ - sources?: XAISearchSource[]; + sources?: XAISearchToolSource[]; +} + +function mapToolSourceToSearchSource( + source: XAISearchToolSource +): XAISearchSource { + switch (source.type) { + case "web": + return { + type: "web", + ...(source.country !== undefined && { country: source.country }), + ...(source.allowedWebsites !== undefined && { + allowed_websites: source.allowedWebsites, + }), + ...(source.excludedWebsites !== undefined && { + excluded_websites: source.excludedWebsites, + }), + ...(source.safeSearch !== undefined && { + safe_search: source.safeSearch, + }), + }; + case "news": + return { + type: "news", + ...(source.country !== undefined && { country: source.country }), + ...(source.excludedWebsites !== undefined && { + excluded_websites: source.excludedWebsites, + }), + ...(source.safeSearch !== undefined && { + safe_search: source.safeSearch, + }), + }; + case "x": + return { + type: "x", + ...(source.includedXHandles !== undefined && { + included_x_handles: source.includedXHandles, + }), + ...(source.excludedXHandles !== undefined && { + excluded_x_handles: source.excludedXHandles, + }), + ...(source.postFavoriteCount !== undefined && { + post_favorite_count: source.postFavoriteCount, + }), + ...(source.postViewCount !== undefined && { + post_view_count: source.postViewCount, + }), + }; + case "rss": + return { + type: "rss", + links: source.links, + }; + default: { + const _exhaustive: never = source; + return _exhaustive; + } + } } /** @@ -79,9 +192,7 @@ export interface XAILiveSearchToolOptions { */ export function xaiLiveSearch( options: XAILiveSearchToolOptions = {} -): DynamicStructuredTool { - const name = "live_search"; - +): XAILiveSearchTool & ServerTool { const searchParams: XAISearchParameters = { ...(options.mode !== undefined && { mode: options.mode }), ...(options.maxSearchResults !== undefined && { @@ -92,35 +203,14 @@ export function xaiLiveSearch( ...(options.returnCitations !== undefined && { return_citations: options.returnCitations, }), - ...(options.sources !== undefined && { sources: options.sources }), - }; - - const searchTool = tool( - (async () => { - // This is a server-side tool; the actual search is executed by xAI - // based on the `search_parameters` we send via the ChatXAI provider. - return "This tool is executed server-side by xAI."; - }) as ( - input: unknown, - runtime: ToolRuntime - ) => string | Promise, - { - name, - description: "Search the web for real-time information.", - schema: { - type: "object", - properties: {}, - }, - } - ); - - searchTool.extras = { - ...(searchTool.extras ?? {}), - providerToolDefinition: { - type: "live_search", - ...searchParams, - }, + ...(options.sources !== undefined && { + sources: options.sources.map(mapToolSourceToSearchSource), + }), }; - return searchTool; + return { + type: "live_search_deprecated_20251215", + name: "live_search", + ...searchParams, + } satisfies XAILiveSearchTool & ServerTool; } diff --git a/libs/providers/langchain-xai/src/tools/tests/live_search.test.ts b/libs/providers/langchain-xai/src/tools/tests/live_search.test.ts index 8060f05d5572..db4b20b08cbb 100644 --- a/libs/providers/langchain-xai/src/tools/tests/live_search.test.ts +++ b/libs/providers/langchain-xai/src/tools/tests/live_search.test.ts @@ -10,10 +10,9 @@ describe("xaiLiveSearch tool", () => { returnCitations: true, }); - expect(tool.name).toBe("live_search"); - expect(tool.extras).toBeDefined(); - expect(tool.extras?.providerToolDefinition).toEqual({ + expect(tool).toEqual({ type: "live_search", + name: "live_search", max_search_results: 10, from_date: "2024-01-01", return_citations: true, @@ -22,9 +21,9 @@ describe("xaiLiveSearch tool", () => { test("creates a tool with default options", async () => { const tool = xaiLiveSearch(); - expect(tool.name).toBe("live_search"); - expect(tool.extras?.providerToolDefinition).toEqual({ + expect(tool).toEqual({ type: "live_search", + name: "live_search", }); }); @@ -33,18 +32,18 @@ describe("xaiLiveSearch tool", () => { sources: [ { type: "web", - excluded_websites: ["wikipedia.org"], + excludedWebsites: ["wikipedia.org"], }, { type: "news", - excluded_websites: ["bbc.co.uk"], + excludedWebsites: ["bbc.co.uk"], }, ], }); - expect(tool.name).toBe("live_search"); - expect(tool.extras?.providerToolDefinition).toEqual({ + expect(tool).toEqual({ type: "live_search", + name: "live_search", sources: [ { type: "web", @@ -67,7 +66,7 @@ describe("ChatXAI with xaiLiveSearch tool", () => { sources: [ { type: "web", - allowed_websites: ["example.com"], + allowedWebsites: ["example.com"], }, ], }); @@ -82,6 +81,7 @@ describe("ChatXAI with xaiLiveSearch tool", () => { expect(formattedTools[0]).toEqual({ type: "live_search", max_search_results: 8, + name: "live_search", sources: [ { type: "web", From bcf66b673ec6338b449da149a3bc65f2ac7d3222 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Twardziak?= Date: Thu, 11 Dec 2025 13:49:14 +0100 Subject: [PATCH 09/15] refactor(xai): Simplifies XAI Live Search tool definition Refactors the XAI Live Search tool to directly define properties, removing the intermediate `searchParams` object. This simplifies the tool's structure and improves readability. Also, the tool definition is updated to reflect the change from a ServerTool to a regular tool. --- .../langchain-xai/src/tools/live_search.ts | 27 ++++++------------- .../src/tools/tests/live_search.test.ts | 16 +++++------ 2 files changed, 16 insertions(+), 27 deletions(-) diff --git a/libs/providers/langchain-xai/src/tools/live_search.ts b/libs/providers/langchain-xai/src/tools/live_search.ts index 475eaa48ea3d..43ab6d0e8556 100644 --- a/libs/providers/langchain-xai/src/tools/live_search.ts +++ b/libs/providers/langchain-xai/src/tools/live_search.ts @@ -1,4 +1,3 @@ -import { type ServerTool } from "@langchain/core/tools"; import type { XAISearchParameters, XAISearchSource } from "../live_search.js"; /** @@ -192,25 +191,15 @@ function mapToolSourceToSearchSource( */ export function xaiLiveSearch( options: XAILiveSearchToolOptions = {} -): XAILiveSearchTool & ServerTool { - const searchParams: XAISearchParameters = { - ...(options.mode !== undefined && { mode: options.mode }), - ...(options.maxSearchResults !== undefined && { - max_search_results: options.maxSearchResults, - }), - ...(options.fromDate !== undefined && { from_date: options.fromDate }), - ...(options.toDate !== undefined && { to_date: options.toDate }), - ...(options.returnCitations !== undefined && { - return_citations: options.returnCitations, - }), - ...(options.sources !== undefined && { - sources: options.sources.map(mapToolSourceToSearchSource), - }), - }; - +): XAILiveSearchTool { return { type: "live_search_deprecated_20251215", name: "live_search", - ...searchParams, - } satisfies XAILiveSearchTool & ServerTool; + mode: options?.mode, + max_search_results: options?.maxSearchResults, + from_date: options?.fromDate, + to_date: options?.toDate, + return_citations: options?.returnCitations, + sources: options?.sources?.map(mapToolSourceToSearchSource), + } satisfies XAILiveSearchTool; } diff --git a/libs/providers/langchain-xai/src/tools/tests/live_search.test.ts b/libs/providers/langchain-xai/src/tools/tests/live_search.test.ts index db4b20b08cbb..94a67f10107b 100644 --- a/libs/providers/langchain-xai/src/tools/tests/live_search.test.ts +++ b/libs/providers/langchain-xai/src/tools/tests/live_search.test.ts @@ -10,8 +10,8 @@ describe("xaiLiveSearch tool", () => { returnCitations: true, }); - expect(tool).toEqual({ - type: "live_search", + expect(tool).toMatchObject({ + type: "live_search_deprecated_20251215", name: "live_search", max_search_results: 10, from_date: "2024-01-01", @@ -21,8 +21,8 @@ describe("xaiLiveSearch tool", () => { test("creates a tool with default options", async () => { const tool = xaiLiveSearch(); - expect(tool).toEqual({ - type: "live_search", + expect(tool).toMatchObject({ + type: "live_search_deprecated_20251215", name: "live_search", }); }); @@ -41,8 +41,8 @@ describe("xaiLiveSearch tool", () => { ], }); - expect(tool).toEqual({ - type: "live_search", + expect(tool).toMatchObject({ + type: "live_search_deprecated_20251215", name: "live_search", sources: [ { @@ -78,8 +78,8 @@ describe("ChatXAI with xaiLiveSearch tool", () => { ]); expect(formattedTools).toHaveLength(1); - expect(formattedTools[0]).toEqual({ - type: "live_search", + expect(formattedTools[0]).toMatchObject({ + type: "live_search_deprecated_20251215", max_search_results: 8, name: "live_search", sources: [ From 1abee53b54d9e3a9122b1570896d6e06bb8bc887 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Twardziak?= Date: Thu, 11 Dec 2025 14:12:11 +0100 Subject: [PATCH 10/15] chore(xai): Deprecates live search tool type Updates the live search tool type to a deprecated version. This prepares the codebase for a future transition, ensuring a smoother upgrade path. The old tool type is replaced with "live_search_deprecated_20251215". --- .../langchain-xai/src/chat_models.ts | 4 +- .../src/tests/chat_models.int.test.ts | 15 ++++++-- .../src/tests/chat_models.test.ts | 34 ++++++++++------- .../src/tools/tests/live_search.test.ts | 37 +++++++++---------- 4 files changed, 54 insertions(+), 36 deletions(-) diff --git a/libs/providers/langchain-xai/src/chat_models.ts b/libs/providers/langchain-xai/src/chat_models.ts index fe8f73fea911..5c4ada0405dc 100644 --- a/libs/providers/langchain-xai/src/chat_models.ts +++ b/libs/providers/langchain-xai/src/chat_models.ts @@ -52,7 +52,9 @@ export type XAIBuiltInTool = XAILiveSearchTool; * This allows us to easily extend support for future built-in tools * without changing the core detection logic. */ -const XAI_BUILT_IN_TOOL_TYPES = new Set(["live_search"]); +const XAI_BUILT_IN_TOOL_TYPES = new Set([ + "live_search_deprecated_20251215", +]); /** * Tool type that includes both standard tools and xAI built-in tools. diff --git a/libs/providers/langchain-xai/src/tests/chat_models.int.test.ts b/libs/providers/langchain-xai/src/tests/chat_models.int.test.ts index 0de55c0cd1bc..699ef07cfc58 100644 --- a/libs/providers/langchain-xai/src/tests/chat_models.int.test.ts +++ b/libs/providers/langchain-xai/src/tests/chat_models.int.test.ts @@ -238,7 +238,10 @@ describe("Server Tool Calling (Live Search)", () => { model: "grok-2-1212", }); - const liveSearchTool: XAILiveSearchTool = { type: "live_search" }; + const liveSearchTool: XAILiveSearchTool = { + name: "live_search", + type: "live_search_deprecated_20251215", + }; const chatWithSearch = chat.bindTools([liveSearchTool]); // Ask about recent events to trigger search @@ -324,7 +327,10 @@ describe("Server Tool Calling (Live Search)", () => { model: "grok-2-1212", }); - const liveSearchTool: XAILiveSearchTool = { type: "live_search" }; + const liveSearchTool: XAILiveSearchTool = { + name: "live_search", + type: "live_search_deprecated_20251215", + }; const chatWithSearch = chat.bindTools([liveSearchTool]); const message = new HumanMessage("What are the top tech news stories?"); @@ -345,7 +351,10 @@ describe("Server Tool Calling (Live Search)", () => { model: "grok-2-1212", }); - const liveSearchTool: XAILiveSearchTool = { type: "live_search" }; + const liveSearchTool: XAILiveSearchTool = { + type: "live_search_deprecated_20251215", + name: "live_search", + }; const customTool = { type: "function" as const, function: { diff --git a/libs/providers/langchain-xai/src/tests/chat_models.test.ts b/libs/providers/langchain-xai/src/tests/chat_models.test.ts index 5578fdde4bc1..a8a1fdfbb086 100644 --- a/libs/providers/langchain-xai/src/tests/chat_models.test.ts +++ b/libs/providers/langchain-xai/src/tests/chat_models.test.ts @@ -32,7 +32,10 @@ test("Serialization with no params", () => { describe("Server Tool Calling", () => { describe("isXAIBuiltInTool", () => { test("should identify live_search as a built-in tool", () => { - const liveSearchTool: XAILiveSearchTool = { type: "live_search" }; + const liveSearchTool: XAILiveSearchTool = { + name: "live_search", + type: "live_search_deprecated_20251215", + }; expect(isXAIBuiltInTool(liveSearchTool)).toBe(true); }); @@ -88,8 +91,8 @@ describe("Server Tool Calling", () => { }); // Access protected method via any cast for testing - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const effectiveParams = (model as any)._getEffectiveSearchParameters({ + // eslint-disable-next-line dot-notation + const effectiveParams = model["_getEffectiveSearchParameters"]({ searchParameters: { max_search_results: 10, from_date: "2024-01-01", @@ -110,7 +113,9 @@ describe("Server Tool Calling", () => { const params: ChatXAICompletionsInvocationParams = model.invocationParams( { - tools: [{ type: "live_search" }], + tools: [ + { type: "live_search_deprecated_20251215", name: "live_search" }, + ] satisfies [XAILiveSearchTool], } as unknown as ChatXAI["ParsedCallOptions"] ); @@ -250,9 +255,12 @@ describe("Server Tool Calling", () => { describe("_hasBuiltInTools", () => { test("should return true when live_search tool is present", () => { const model = new ChatXAI(); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const result = (model as any)._hasBuiltInTools([ - { type: "live_search" }, + // eslint-disable-next-line dot-notation + const result = model["_hasBuiltInTools"]([ + { + type: "live_search_deprecated_20251215", + name: "live_search", + } satisfies XAILiveSearchTool, { type: "function", function: { name: "test", parameters: {} }, @@ -263,8 +271,8 @@ describe("Server Tool Calling", () => { test("should return false when no built-in tools are present", () => { const model = new ChatXAI(); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const result = (model as any)._hasBuiltInTools([ + // eslint-disable-next-line dot-notation + const result = model["_hasBuiltInTools"]([ { type: "function", function: { name: "test", parameters: {} }, @@ -275,10 +283,10 @@ describe("Server Tool Calling", () => { test("should return false for undefined or empty tools", () => { const model = new ChatXAI(); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - expect((model as any)._hasBuiltInTools(undefined)).toBe(false); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - expect((model as any)._hasBuiltInTools([])).toBe(false); + // eslint-disable-next-line dot-notation + expect(model["_hasBuiltInTools"](undefined)).toBe(false); + // eslint-disable-next-line dot-notation + expect(model["_hasBuiltInTools"]([])).toBe(false); }); }); }); diff --git a/libs/providers/langchain-xai/src/tools/tests/live_search.test.ts b/libs/providers/langchain-xai/src/tools/tests/live_search.test.ts index 94a67f10107b..1e3779d26a6b 100644 --- a/libs/providers/langchain-xai/src/tools/tests/live_search.test.ts +++ b/libs/providers/langchain-xai/src/tools/tests/live_search.test.ts @@ -1,5 +1,5 @@ import { test, expect, describe } from "vitest"; -import { xaiLiveSearch } from "../live_search.js"; +import { xaiLiveSearch, XAILiveSearchTool } from "../live_search.js"; import { ChatXAI } from "../../chat_models.js"; describe("xaiLiveSearch tool", () => { @@ -16,7 +16,7 @@ describe("xaiLiveSearch tool", () => { max_search_results: 10, from_date: "2024-01-01", return_citations: true, - }); + } satisfies XAILiveSearchTool); }); test("creates a tool with default options", async () => { @@ -24,7 +24,7 @@ describe("xaiLiveSearch tool", () => { expect(tool).toMatchObject({ type: "live_search_deprecated_20251215", name: "live_search", - }); + } satisfies XAILiveSearchTool); }); test("creates a tool with web and news sources using excluded_websites", async () => { @@ -54,7 +54,7 @@ describe("xaiLiveSearch tool", () => { excluded_websites: ["bbc.co.uk"], }, ], - }); + } satisfies XAILiveSearchTool); }); }); @@ -72,32 +72,30 @@ describe("ChatXAI with xaiLiveSearch tool", () => { }); // Access protected method for testing - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const formattedTools = (model as any).formatStructuredToolToXAI([ - searchTool, - ]); + const formattedTools = model.formatStructuredToolToXAI([searchTool]); expect(formattedTools).toHaveLength(1); - expect(formattedTools[0]).toMatchObject({ + expect(formattedTools![0]).toMatchObject({ type: "live_search_deprecated_20251215", - max_search_results: 8, name: "live_search", + max_search_results: 8, sources: [ { type: "web", allowed_websites: ["example.com"], }, ], - }); + } satisfies XAILiveSearchTool); }); test("invocationParams extracts parameters from formatted tools", () => { const model = new ChatXAI({ apiKey: "foo" }); // Simulate the tools being passed in options (as they would be after bindTools -> withConfig -> invoke) - const tools = [ + const tools: [XAILiveSearchTool] = [ { - type: "live_search", + type: "live_search_deprecated_20251215", + name: "live_search", max_search_results: 8, sources: [ { @@ -110,8 +108,8 @@ describe("ChatXAI with xaiLiveSearch tool", () => { // Access protected method for testing const params = model.invocationParams({ - tools: tools as never, - } as never); + tools: tools, + }); expect(params.search_parameters).toEqual({ mode: "auto", @@ -134,17 +132,18 @@ describe("ChatXAI with xaiLiveSearch tool", () => { }, }); - const tools = [ + const tools: [XAILiveSearchTool] = [ { - type: "live_search", + type: "live_search_deprecated_20251215", + name: "live_search", max_search_results: 10, from_date: "2024-01-01", }, ]; const params = model.invocationParams({ - tools: tools as never, - } as never); + tools: tools, + }); expect(params.search_parameters).toEqual({ mode: "on", From a26a2d74566798b861ee08afbbc28f0eea0ad7d5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Twardziak?= Date: Thu, 11 Dec 2025 14:18:51 +0100 Subject: [PATCH 11/15] chore(xai): Updates live search example in README Refactors the README to use the `tools.xaiLiveSearch` constructor for creating the live search tool, providing a more accurate representation of its usage. Corrects casing of options from camel case to camel case. --- libs/providers/langchain-xai/README.md | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/libs/providers/langchain-xai/README.md b/libs/providers/langchain-xai/README.md index 0f0714ec4b68..250c77e8386e 100644 --- a/libs/providers/langchain-xai/README.md +++ b/libs/providers/langchain-xai/README.md @@ -46,8 +46,8 @@ const model = new ChatXAI({ // Create the built-in live_search tool with optional parameters const searchTool = tools.xaiLiveSearch({ - max_search_results: 5, - return_citations: true, + maxSearchResults: 5, + returnCitations: true, }); // Bind the live_search tool to the model @@ -159,12 +159,15 @@ const result = await model.invoke("Summarize the latest posts from this feed", { ### Combining live_search with custom tools ```typescript -import { ChatXAI } from "@langchain/xai"; +import { ChatXAI, tools } from "@langchain/xai"; const model = new ChatXAI({ model: "grok-2-1212" }); const modelWithTools = model.bindTools([ - { type: "live_search" }, // Built-in server tool + tools.xaiLiveSearch({ + maxSearchResults: 5, + returnCitations: true, + }), // Built-in server tool { // Custom function tool type: "function", From 387aa23059b5f584d6ce00abd011e9380ed901c0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Twardziak?= Date: Thu, 11 Dec 2025 14:19:59 +0100 Subject: [PATCH 12/15] chore(xai): Simplifies XAI Live Search tool usage Updates the XAI Live Search tool to use default parameters, simplifying its invocation and configuration. --- libs/providers/langchain-xai/README.md | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/libs/providers/langchain-xai/README.md b/libs/providers/langchain-xai/README.md index 250c77e8386e..99e9f0f8a2a2 100644 --- a/libs/providers/langchain-xai/README.md +++ b/libs/providers/langchain-xai/README.md @@ -164,10 +164,7 @@ import { ChatXAI, tools } from "@langchain/xai"; const model = new ChatXAI({ model: "grok-2-1212" }); const modelWithTools = model.bindTools([ - tools.xaiLiveSearch({ - maxSearchResults: 5, - returnCitations: true, - }), // Built-in server tool + tools.xaiLiveSearch(), // Built-in server tool { // Custom function tool type: "function", From 8044cb4b6cb625d821b908a8e700bfe2c2a0d42a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Twardziak?= Date: Thu, 11 Dec 2025 14:24:56 +0100 Subject: [PATCH 13/15] chore(xai): Clarifies xAI Live Search tool options casing Clarifies that the xAI Live Search tool options in TypeScript use camelCase field names, which are then mapped to snake_case field names in the underlying JSON API. This ensures users understand the expected casing when using the tool options in TypeScript. --- libs/providers/langchain-xai/README.md | 14 ++++++-------- .../langchain-xai/src/tools/live_search.ts | 4 +++- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/libs/providers/langchain-xai/README.md b/libs/providers/langchain-xai/README.md index 99e9f0f8a2a2..6e7811252297 100644 --- a/libs/providers/langchain-xai/README.md +++ b/libs/providers/langchain-xai/README.md @@ -147,14 +147,12 @@ const result = await model.invoke("Summarize the latest posts from this feed", { > Notes: > -> - The previous `allowed_domains` / `excluded_domains` fields are not -> supported in this provider. Use `sources` with `allowed_websites` and -> `excluded_websites` instead. -> - In TypeScript, the `XAISearchParameters` and `sources` types use the same -> `snake_case` field names as the underlying JSON API (for example -> `allowed_websites`, `excluded_websites`, `included_x_handles`). There are no -> separate camelCase aliases (`allowedWebsites`, etc.), which keeps the -> provider aligned with the official xAI documentation. +> - The `xaiLiveSearch` tool options use **camelCase** field names in TypeScript +> (for example `maxSearchResults`, `fromDate`, `returnCitations`, +> `allowedWebsites`, `excludedWebsites`, `includedXHandles`). These are +> automatically mapped to the underlying JSON API's `search_parameters` +> object, which uses `snake_case` field names as documented in the official +> xAI Live Search docs. ### Combining live_search with custom tools diff --git a/libs/providers/langchain-xai/src/tools/live_search.ts b/libs/providers/langchain-xai/src/tools/live_search.ts index 43ab6d0e8556..9f73758706f7 100644 --- a/libs/providers/langchain-xai/src/tools/live_search.ts +++ b/libs/providers/langchain-xai/src/tools/live_search.ts @@ -70,7 +70,9 @@ export type XAISearchToolSource = /** * Options for the xAI live search tool (camelCase). - * These are converted to the snake_case `XAISearchParameters` used internally. + * All fields are camel-cased for the TypeScript API and are mapped to the + * corresponding snake_case fields in the underlying `XAISearchParameters` + * object that is sent to xAI's deprecated Live Search API. */ export interface XAILiveSearchToolOptions { /** From 9a914951a9e3306f15f731dabdb2fe153ec954e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Twardziak?= Date: Thu, 11 Dec 2025 16:53:30 +0100 Subject: [PATCH 14/15] refactor(xai): Standardizes XAI live search tool type Updates the XAI provider to use constants for the live search tool type instead of hardcoding the string. This improves consistency and maintainability. Also introduces tests for live search utilities. --- .../langchain-xai/src/chat_models.ts | 14 +- .../langchain-xai/src/live_search.ts | 12 +- .../src/tests/chat_models.int.test.ts | 18 +- .../src/tests/chat_models.test.ts | 19 +- .../src/tests/live_search.test.ts | 205 ++++++++++++++++++ .../langchain-xai/src/tools/live_search.ts | 14 +- .../src/tools/tests/live_search.test.ts | 31 +-- 7 files changed, 274 insertions(+), 39 deletions(-) create mode 100644 libs/providers/langchain-xai/src/tests/live_search.test.ts diff --git a/libs/providers/langchain-xai/src/chat_models.ts b/libs/providers/langchain-xai/src/chat_models.ts index 5c4ada0405dc..d82ece69da5c 100644 --- a/libs/providers/langchain-xai/src/chat_models.ts +++ b/libs/providers/langchain-xai/src/chat_models.ts @@ -35,7 +35,10 @@ import { type XAISearchParametersPayload, } from "./live_search.js"; import PROFILES from "./profiles.js"; -import { XAILiveSearchTool } from "./tools/live_search.js"; +import { + XAI_LIVE_SEARCH_TOOL_TYPE, + XAILiveSearchTool, +} from "./tools/live_search.js"; export type OpenAIToolChoice = | OpenAIClient.ChatCompletionToolChoiceOption @@ -53,7 +56,7 @@ export type XAIBuiltInTool = XAILiveSearchTool; * without changing the core detection logic. */ const XAI_BUILT_IN_TOOL_TYPES = new Set([ - "live_search_deprecated_20251215", + XAI_LIVE_SEARCH_TOOL_TYPE, ]); /** @@ -794,9 +797,10 @@ export class ChatXAI extends ChatOpenAICompletions { let filteredTools: OpenAIClient.ChatCompletionTool[] | undefined; if (request.tools) { - filteredTools = filterXAIBuiltInTools(request.tools) as - | OpenAIClient.ChatCompletionTool[] - | undefined; + filteredTools = filterXAIBuiltInTools({ + tools: request.tools, + excludedTypes: [XAI_LIVE_SEARCH_TOOL_TYPE], + }) as OpenAIClient.ChatCompletionTool[] | undefined; } const newRequest = { diff --git a/libs/providers/langchain-xai/src/live_search.ts b/libs/providers/langchain-xai/src/live_search.ts index edeb21f7178c..19fab0c61d5e 100644 --- a/libs/providers/langchain-xai/src/live_search.ts +++ b/libs/providers/langchain-xai/src/live_search.ts @@ -217,12 +217,12 @@ export function buildSearchParametersPayload( export function filterXAIBuiltInTools< // eslint-disable-next-line @typescript-eslint/no-explicit-any T extends { [key: string]: any } ->(tools?: T[]): T[] | undefined { - if (!tools) { +>(payload?: { tools?: T[]; excludedTypes?: string[] }): T[] | undefined { + if (!payload?.tools) { return undefined; } - const filtered = tools.filter((tool) => { + const filtered = payload.tools.filter((tool) => { if (tool == null || typeof tool !== "object") { return true; } @@ -231,7 +231,11 @@ export function filterXAIBuiltInTools< return true; } - return (tool as { type?: unknown }).type !== "live_search"; + if (!payload?.excludedTypes?.length) { + return true; + } + + return !payload.excludedTypes.includes(tool.type); }); return filtered.length > 0 ? filtered : undefined; diff --git a/libs/providers/langchain-xai/src/tests/chat_models.int.test.ts b/libs/providers/langchain-xai/src/tests/chat_models.int.test.ts index 699ef07cfc58..96c384f8839a 100644 --- a/libs/providers/langchain-xai/src/tests/chat_models.int.test.ts +++ b/libs/providers/langchain-xai/src/tests/chat_models.int.test.ts @@ -11,7 +11,11 @@ import { tool } from "@langchain/core/tools"; import { concat } from "@langchain/core/utils/stream"; import { ChatXAI } from "../chat_models.js"; -import { type XAILiveSearchTool } from "../tools/live_search.js"; +import { + XAI_LIVE_SEARCH_TOOL_NAME, + XAI_LIVE_SEARCH_TOOL_TYPE, + type XAILiveSearchTool, +} from "../tools/live_search.js"; test("invoke", async () => { const chat = new ChatXAI({ @@ -239,8 +243,8 @@ describe("Server Tool Calling (Live Search)", () => { }); const liveSearchTool: XAILiveSearchTool = { - name: "live_search", - type: "live_search_deprecated_20251215", + name: XAI_LIVE_SEARCH_TOOL_NAME, + type: XAI_LIVE_SEARCH_TOOL_TYPE, }; const chatWithSearch = chat.bindTools([liveSearchTool]); @@ -328,8 +332,8 @@ describe("Server Tool Calling (Live Search)", () => { }); const liveSearchTool: XAILiveSearchTool = { - name: "live_search", - type: "live_search_deprecated_20251215", + name: XAI_LIVE_SEARCH_TOOL_NAME, + type: XAI_LIVE_SEARCH_TOOL_TYPE, }; const chatWithSearch = chat.bindTools([liveSearchTool]); @@ -352,8 +356,8 @@ describe("Server Tool Calling (Live Search)", () => { }); const liveSearchTool: XAILiveSearchTool = { - type: "live_search_deprecated_20251215", - name: "live_search", + type: XAI_LIVE_SEARCH_TOOL_TYPE, + name: XAI_LIVE_SEARCH_TOOL_NAME, }; const customTool = { type: "function" as const, diff --git a/libs/providers/langchain-xai/src/tests/chat_models.test.ts b/libs/providers/langchain-xai/src/tests/chat_models.test.ts index a8a1fdfbb086..d03556fdaa24 100644 --- a/libs/providers/langchain-xai/src/tests/chat_models.test.ts +++ b/libs/providers/langchain-xai/src/tests/chat_models.test.ts @@ -5,7 +5,11 @@ import { type ChatXAICompletionsInvocationParams, } from "../chat_models.js"; import { XAISearchParameters } from "../live_search.js"; -import { XAILiveSearchTool } from "../tools/live_search.js"; +import { + XAI_LIVE_SEARCH_TOOL_NAME, + XAI_LIVE_SEARCH_TOOL_TYPE, + XAILiveSearchTool, +} from "../tools/live_search.js"; beforeEach(() => { process.env.XAI_API_KEY = "foo"; @@ -33,8 +37,8 @@ describe("Server Tool Calling", () => { describe("isXAIBuiltInTool", () => { test("should identify live_search as a built-in tool", () => { const liveSearchTool: XAILiveSearchTool = { - name: "live_search", - type: "live_search_deprecated_20251215", + name: XAI_LIVE_SEARCH_TOOL_NAME, + type: XAI_LIVE_SEARCH_TOOL_TYPE, }; expect(isXAIBuiltInTool(liveSearchTool)).toBe(true); }); @@ -114,7 +118,10 @@ describe("Server Tool Calling", () => { const params: ChatXAICompletionsInvocationParams = model.invocationParams( { tools: [ - { type: "live_search_deprecated_20251215", name: "live_search" }, + { + type: XAI_LIVE_SEARCH_TOOL_TYPE, + name: XAI_LIVE_SEARCH_TOOL_NAME, + }, ] satisfies [XAILiveSearchTool], } as unknown as ChatXAI["ParsedCallOptions"] ); @@ -258,8 +265,8 @@ describe("Server Tool Calling", () => { // eslint-disable-next-line dot-notation const result = model["_hasBuiltInTools"]([ { - type: "live_search_deprecated_20251215", - name: "live_search", + type: XAI_LIVE_SEARCH_TOOL_TYPE, + name: XAI_LIVE_SEARCH_TOOL_NAME, } satisfies XAILiveSearchTool, { type: "function", diff --git a/libs/providers/langchain-xai/src/tests/live_search.test.ts b/libs/providers/langchain-xai/src/tests/live_search.test.ts new file mode 100644 index 000000000000..bfd398304db3 --- /dev/null +++ b/libs/providers/langchain-xai/src/tests/live_search.test.ts @@ -0,0 +1,205 @@ +import { describe, expect, test } from "vitest"; + +import { + buildSearchParametersPayload, + filterXAIBuiltInTools, + mergeSearchParams, + type XAISearchParameters, + type XAISearchSource, +} from "../live_search.js"; +import { XAI_LIVE_SEARCH_TOOL_TYPE } from "../tools/live_search.js"; + +describe("mergeSearchParams", () => { + test("returns undefined when no params are provided", () => { + expect(mergeSearchParams()).toBeUndefined(); + }); + + test("returns instance params when only instance params are provided", () => { + const instance: XAISearchParameters = { + mode: "auto", + max_search_results: 5, + }; + + const result = mergeSearchParams(instance, undefined, undefined); + + expect(result).toEqual(instance); + }); + + test("call-level params override instance-level params", () => { + const instance: XAISearchParameters = { + mode: "auto", + max_search_results: 5, + return_citations: true, + }; + const call: XAISearchParameters = { + mode: "on", + max_search_results: 10, + }; + + const result = mergeSearchParams(instance, call, undefined); + + expect(result).toEqual({ + mode: "on", + max_search_results: 10, + return_citations: true, + }); + }); + + test("instance params override tool params", () => { + const instance: XAISearchParameters = { + max_search_results: 5, + }; + const tool: XAISearchParameters = { + max_search_results: 10, + from_date: "2024-01-01", + }; + + const result = mergeSearchParams(instance, undefined, tool); + + expect(result).toEqual({ + max_search_results: 5, + from_date: "2024-01-01", + }); + }); + + test("applies precedence: tool < instance < call", () => { + const instance: XAISearchParameters = { + mode: "auto", + max_search_results: 5, + return_citations: true, + }; + const call: XAISearchParameters = { + mode: "on", + max_search_results: 10, + }; + const tool: XAISearchParameters = { + mode: "off", + from_date: "2024-01-01", + to_date: "2024-01-31", + }; + + const result = mergeSearchParams(instance, call, tool); + + expect(result).toEqual({ + // from tool + from_date: "2024-01-01", + to_date: "2024-01-31", + // overridden by instance / call + mode: "on", + max_search_results: 10, + return_citations: true, + }); + }); +}); + +describe("buildSearchParametersPayload", () => { + test("returns undefined when params are undefined", () => { + expect(buildSearchParametersPayload(undefined)).toBeUndefined(); + }); + + test("builds payload with basic fields", () => { + const params: XAISearchParameters = { + mode: "on", + max_search_results: 7, + from_date: "2024-01-01", + to_date: "2024-01-31", + return_citations: false, + }; + + const payload = buildSearchParametersPayload(params); + + expect(payload).toEqual({ + mode: "on", + max_search_results: 7, + from_date: "2024-01-01", + to_date: "2024-01-31", + return_citations: false, + }); + }); + + test("includes sources only when non-empty", () => { + const sources: XAISearchSource[] = [ + { + type: "web", + allowed_websites: ["x.ai"], + }, + { + type: "news", + excluded_websites: ["example.com"], + }, + ]; + + const withSources = buildSearchParametersPayload({ + mode: "auto", + sources, + }); + + expect(withSources).toEqual({ + mode: "auto", + sources, + }); + + const withoutSources = buildSearchParametersPayload({ + mode: "auto", + sources: [], + }); + expect(withoutSources).toEqual({ + mode: "auto", + }); + }); +}); + +describe("filterXAIBuiltInTools", () => { + test("returns undefined when no tools are provided", () => { + expect(filterXAIBuiltInTools()).toBeUndefined(); + expect(filterXAIBuiltInTools({})).toBeUndefined(); + expect(filterXAIBuiltInTools({ tools: [] })).toBeUndefined(); + }); + + test("returns tools unchanged when no excludedTypes are provided", () => { + const tools = [{ type: "foo" }, { type: XAI_LIVE_SEARCH_TOOL_TYPE }]; + + const result = filterXAIBuiltInTools({ tools }); + + expect(result).toEqual(tools); + }); + + test("filters out tools whose type is in excludedTypes", () => { + const liveSearchTool = { type: XAI_LIVE_SEARCH_TOOL_TYPE, name: "live" }; + const otherTool = { type: "some_other_tool", name: "other" }; + + const result = filterXAIBuiltInTools({ + tools: [liveSearchTool, otherTool], + excludedTypes: [XAI_LIVE_SEARCH_TOOL_TYPE], + }); + + expect(result).toEqual([otherTool]); + }); + + test("keeps tools without a type property", () => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const tools: any[] = [ + { id: 1 }, // no type + { type: XAI_LIVE_SEARCH_TOOL_TYPE }, + ]; + + const result = filterXAIBuiltInTools({ + tools, + excludedTypes: [XAI_LIVE_SEARCH_TOOL_TYPE], + }); + + // The tool without `type` should be kept, the built-in one should be filtered out + expect(result).toEqual([{ id: 1 }]); + }); + + test("returns undefined when all tools are filtered out", () => { + const tools = [{ type: XAI_LIVE_SEARCH_TOOL_TYPE }]; + + const result = filterXAIBuiltInTools({ + tools, + excludedTypes: [XAI_LIVE_SEARCH_TOOL_TYPE], + }); + + expect(result).toBeUndefined(); + }); +}); diff --git a/libs/providers/langchain-xai/src/tools/live_search.ts b/libs/providers/langchain-xai/src/tools/live_search.ts index 9f73758706f7..3354f3f3a0e4 100644 --- a/libs/providers/langchain-xai/src/tools/live_search.ts +++ b/libs/providers/langchain-xai/src/tools/live_search.ts @@ -1,5 +1,11 @@ import type { XAISearchParameters, XAISearchSource } from "../live_search.js"; +/** + * xAI's deprecated live_search tool type. + */ +export const XAI_LIVE_SEARCH_TOOL_TYPE = "live_search_deprecated_20251215"; +export const XAI_LIVE_SEARCH_TOOL_NAME = "live_search"; + /** * xAI's built-in live_search tool type. * Enables the model to search the web for real-time information. @@ -8,14 +14,14 @@ export interface XAILiveSearchTool extends XAISearchParameters { /** * The name of the tool. Must be "live_search" for xAI's built-in search. */ - name: "live_search"; + name: typeof XAI_LIVE_SEARCH_TOOL_NAME; /** * The type of the tool. This uses a deprecated Live Search API shape: * the advanced agentic search capabilities powering grok.com are generally * available in the new agentic tool calling API, and the Live Search API * will be deprecated by December 15, 2025. */ - type: "live_search_deprecated_20251215"; + type: typeof XAI_LIVE_SEARCH_TOOL_TYPE; } /** @@ -195,8 +201,8 @@ export function xaiLiveSearch( options: XAILiveSearchToolOptions = {} ): XAILiveSearchTool { return { - type: "live_search_deprecated_20251215", - name: "live_search", + type: XAI_LIVE_SEARCH_TOOL_TYPE, + name: XAI_LIVE_SEARCH_TOOL_NAME, mode: options?.mode, max_search_results: options?.maxSearchResults, from_date: options?.fromDate, diff --git a/libs/providers/langchain-xai/src/tools/tests/live_search.test.ts b/libs/providers/langchain-xai/src/tools/tests/live_search.test.ts index 1e3779d26a6b..3024f4b7ce2d 100644 --- a/libs/providers/langchain-xai/src/tools/tests/live_search.test.ts +++ b/libs/providers/langchain-xai/src/tools/tests/live_search.test.ts @@ -1,5 +1,10 @@ import { test, expect, describe } from "vitest"; -import { xaiLiveSearch, XAILiveSearchTool } from "../live_search.js"; +import { + XAI_LIVE_SEARCH_TOOL_NAME, + XAI_LIVE_SEARCH_TOOL_TYPE, + xaiLiveSearch, + XAILiveSearchTool, +} from "../live_search.js"; import { ChatXAI } from "../../chat_models.js"; describe("xaiLiveSearch tool", () => { @@ -11,8 +16,8 @@ describe("xaiLiveSearch tool", () => { }); expect(tool).toMatchObject({ - type: "live_search_deprecated_20251215", - name: "live_search", + type: XAI_LIVE_SEARCH_TOOL_TYPE, + name: XAI_LIVE_SEARCH_TOOL_NAME, max_search_results: 10, from_date: "2024-01-01", return_citations: true, @@ -22,8 +27,8 @@ describe("xaiLiveSearch tool", () => { test("creates a tool with default options", async () => { const tool = xaiLiveSearch(); expect(tool).toMatchObject({ - type: "live_search_deprecated_20251215", - name: "live_search", + type: XAI_LIVE_SEARCH_TOOL_TYPE, + name: XAI_LIVE_SEARCH_TOOL_NAME, } satisfies XAILiveSearchTool); }); @@ -42,8 +47,8 @@ describe("xaiLiveSearch tool", () => { }); expect(tool).toMatchObject({ - type: "live_search_deprecated_20251215", - name: "live_search", + type: XAI_LIVE_SEARCH_TOOL_TYPE, + name: XAI_LIVE_SEARCH_TOOL_NAME, sources: [ { type: "web", @@ -76,8 +81,8 @@ describe("ChatXAI with xaiLiveSearch tool", () => { expect(formattedTools).toHaveLength(1); expect(formattedTools![0]).toMatchObject({ - type: "live_search_deprecated_20251215", - name: "live_search", + type: XAI_LIVE_SEARCH_TOOL_TYPE, + name: XAI_LIVE_SEARCH_TOOL_NAME, max_search_results: 8, sources: [ { @@ -94,8 +99,8 @@ describe("ChatXAI with xaiLiveSearch tool", () => { // Simulate the tools being passed in options (as they would be after bindTools -> withConfig -> invoke) const tools: [XAILiveSearchTool] = [ { - type: "live_search_deprecated_20251215", - name: "live_search", + type: XAI_LIVE_SEARCH_TOOL_TYPE, + name: XAI_LIVE_SEARCH_TOOL_NAME, max_search_results: 8, sources: [ { @@ -134,8 +139,8 @@ describe("ChatXAI with xaiLiveSearch tool", () => { const tools: [XAILiveSearchTool] = [ { - type: "live_search_deprecated_20251215", - name: "live_search", + type: XAI_LIVE_SEARCH_TOOL_TYPE, + name: XAI_LIVE_SEARCH_TOOL_NAME, max_search_results: 10, from_date: "2024-01-01", }, From a5dc3979c3e5a0e004a9cf080eaa8f74aee6387b Mon Sep 17 00:00:00 2001 From: Christian Bromann Date: Thu, 11 Dec 2025 22:38:26 -0800 Subject: [PATCH 15/15] Update version to minor and add Live Search support This change updates the versioning of the '@langchain/xai' package from patch to minor and adds native Live Search support. --- .changeset/ninety-paws-cough.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/ninety-paws-cough.md diff --git a/.changeset/ninety-paws-cough.md b/.changeset/ninety-paws-cough.md new file mode 100644 index 000000000000..5df928a33f31 --- /dev/null +++ b/.changeset/ninety-paws-cough.md @@ -0,0 +1,5 @@ +--- +"@langchain/xai": minor +--- + +Adds native Live Search support to xAI provider