From 6b9b4c6546332346b787f337b92879fc9b6b530c Mon Sep 17 00:00:00 2001 From: Mason Daugherty Date: Thu, 12 Mar 2026 10:55:36 -0400 Subject: [PATCH] feat(xai): support `base_url` alias and `XAI_API_BASE` env var (#35790) Add `base_url` alias and `XAI_API_BASE` env variable support to `ChatXAI.xai_api_base`, aligning the xAI integration with the pattern used across other partner packages (OpenAI, Groq, Fireworks, etc.). Previously the base URL was a plain string field with no alias or env-var lookup, making it inconsistent with the rest of the ecosystem and harder to configure in deployment environments. ## Changes - Add `alias="base_url"` and `default_factory=from_env("XAI_API_BASE", default="https://api.x.ai/v1/")` to `ChatXAI.xai_api_base`, matching the convention in `langchain_openai`, `langchain_groq`, and `langchain_fireworks` --- .../partners/xai/langchain_xai/chat_models.py | 15 ++++++++++--- .../integration_tests/test_chat_models.py | 5 ++++- .../test_chat_models_standard.py | 1 + .../xai/tests/unit_tests/test_chat_models.py | 22 +++++++++++++++++++ libs/partners/xai/uv.lock | 12 +++++----- 5 files changed, 45 insertions(+), 10 deletions(-) diff --git a/libs/partners/xai/langchain_xai/chat_models.py b/libs/partners/xai/langchain_xai/chat_models.py index ca05da76c1d..591d8372ab6 100644 --- a/libs/partners/xai/langchain_xai/chat_models.py +++ b/libs/partners/xai/langchain_xai/chat_models.py @@ -7,7 +7,7 @@ from typing import TYPE_CHECKING, Any, Literal, TypeAlias, cast import openai from langchain_core.messages import AIMessageChunk -from langchain_core.utils import secret_from_env +from langchain_core.utils import from_env, secret_from_env from langchain_openai.chat_models.base import BaseChatOpenAI from pydantic import BaseModel, ConfigDict, Field, SecretStr, model_validator from typing_extensions import Self @@ -397,6 +397,7 @@ class ChatXAI(BaseChatOpenAI): # type: ignore[override] model_name: str = Field(default="grok-4", alias="model") """Model name to use.""" + xai_api_key: SecretStr | None = Field( alias="api_key", default_factory=secret_from_env("XAI_API_KEY", default=None), @@ -405,8 +406,16 @@ class ChatXAI(BaseChatOpenAI): # type: ignore[override] Automatically read from env variable `XAI_API_KEY` if not provided. """ - xai_api_base: str = Field(default="https://api.x.ai/v1/") - """Base URL path for API requests.""" + + xai_api_base: str = Field( + alias="base_url", + default_factory=from_env("XAI_API_BASE", default="https://api.x.ai/v1/"), + ) + """Base URL path for API requests. + + Automatically read from env variable `XAI_API_BASE` if not provided. + """ + search_parameters: dict[str, Any] | None = None """**Deprecated.** Use web search tools instead: diff --git a/libs/partners/xai/tests/integration_tests/test_chat_models.py b/libs/partners/xai/tests/integration_tests/test_chat_models.py index 64c06c6d6d7..10d7f53ef7a 100644 --- a/libs/partners/xai/tests/integration_tests/test_chat_models.py +++ b/libs/partners/xai/tests/integration_tests/test_chat_models.py @@ -17,6 +17,7 @@ def test_reasoning(output_version: Literal["", "v1"]) -> None: """Test reasoning features. !!! note + `grok-4` does not return `reasoning_content`, but may optionally return encrypted reasoning content if `use_encrypted_content` is set to `True`. """ @@ -25,12 +26,14 @@ def test_reasoning(output_version: Literal["", "v1"]) -> None: chat_model = ChatXAI( model="grok-3-mini", reasoning_effort="low", + temperature=0, output_version=output_version, ) else: chat_model = ChatXAI( model="grok-3-mini", reasoning_effort="low", + temperature=0, ) input_message = "What is 3^3?" response = chat_model.invoke(input_message) @@ -95,7 +98,7 @@ def test_reasoning(output_version: Literal["", "v1"]) -> None: def test_web_search() -> None: - llm = ChatXAI(model=MODEL_NAME).bind_tools([{"type": "web_search"}]) + llm = ChatXAI(model=MODEL_NAME, temperature=0).bind_tools([{"type": "web_search"}]) # Test invoke response = llm.invoke("Look up the current time in Boston, MA.") diff --git a/libs/partners/xai/tests/integration_tests/test_chat_models_standard.py b/libs/partners/xai/tests/integration_tests/test_chat_models_standard.py index 35c95b40d2e..fd62bdc1eb4 100644 --- a/libs/partners/xai/tests/integration_tests/test_chat_models_standard.py +++ b/libs/partners/xai/tests/integration_tests/test_chat_models_standard.py @@ -34,6 +34,7 @@ class TestXAIStandard(ChatModelIntegrationTests): def chat_model_params(self) -> dict: return { "model": MODEL_NAME, + "temperature": 0, "rate_limiter": rate_limiter, } diff --git a/libs/partners/xai/tests/unit_tests/test_chat_models.py b/libs/partners/xai/tests/unit_tests/test_chat_models.py index 11c62d7072d..331c16f0bca 100644 --- a/libs/partners/xai/tests/unit_tests/test_chat_models.py +++ b/libs/partners/xai/tests/unit_tests/test_chat_models.py @@ -12,6 +12,7 @@ from langchain_openai.chat_models.base import ( _convert_dict_to_message, _convert_message_to_dict, ) +from pydantic import SecretStr from langchain_xai import ChatXAI @@ -65,6 +66,27 @@ def test_chat_xai_extra_kwargs() -> None: ChatXAI(model=MODEL_NAME, foo=3, model_kwargs={"foo": 2}) # type: ignore[call-arg] +def test_chat_xai_base_url_alias() -> None: + llm = ChatXAI( + model=MODEL_NAME, + api_key=SecretStr("test-api-key"), + base_url="http://example.test/v1", + ) + assert llm.xai_api_base == "http://example.test/v1" + assert llm.model_kwargs == {} + + +def test_chat_xai_api_base_from_env(monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.setenv("XAI_API_BASE", "http://env.example.test/v1") + + llm = ChatXAI( + model=MODEL_NAME, + api_key=SecretStr("test-api-key"), + ) + + assert llm.xai_api_base == "http://env.example.test/v1" + + def test_function_dict_to_message_function_message() -> None: content = json.dumps({"result": "Example #1"}) name = "test_function" diff --git a/libs/partners/xai/uv.lock b/libs/partners/xai/uv.lock index a823c6b7613..fe4a4aabbb6 100644 --- a/libs/partners/xai/uv.lock +++ b/libs/partners/xai/uv.lock @@ -655,7 +655,7 @@ wheels = [ [[package]] name = "langchain-core" -version = "1.2.13" +version = "1.2.18" source = { editable = "../../core" } dependencies = [ { name = "jsonpatch" }, @@ -715,7 +715,7 @@ typing = [ [[package]] name = "langchain-openai" -version = "1.1.10" +version = "1.1.11" source = { editable = "../openai" } dependencies = [ { name = "langchain-core" }, @@ -726,7 +726,7 @@ dependencies = [ [package.metadata] requires-dist = [ { name = "langchain-core", editable = "../../core" }, - { name = "openai", specifier = ">=2.20.0,<3.0.0" }, + { name = "openai", specifier = ">=2.26.0,<3.0.0" }, { name = "tiktoken", specifier = ">=0.7.0,<1.0.0" }, ] @@ -1228,7 +1228,7 @@ wheels = [ [[package]] name = "openai" -version = "2.21.0" +version = "2.26.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio" }, @@ -1240,9 +1240,9 @@ dependencies = [ { name = "tqdm" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/92/e5/3d197a0947a166649f566706d7a4c8f7fe38f1fa7b24c9bcffe4c7591d44/openai-2.21.0.tar.gz", hash = "sha256:81b48ce4b8bbb2cc3af02047ceb19561f7b1dc0d4e52d1de7f02abfd15aa59b7", size = 644374, upload-time = "2026-02-14T00:12:01.577Z" } +sdist = { url = "https://files.pythonhosted.org/packages/d7/91/2a06c4e9597c338cac1e5e5a8dd6f29e1836fc229c4c523529dca387fda8/openai-2.26.0.tar.gz", hash = "sha256:b41f37c140ae0034a6e92b0c509376d907f3a66109935fba2c1b471a7c05a8fb", size = 666702, upload-time = "2026-03-05T23:17:35.874Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/cc/56/0a89092a453bb2c676d66abee44f863e742b2110d4dbb1dbcca3f7e5fc33/openai-2.21.0-py3-none-any.whl", hash = "sha256:0bc1c775e5b1536c294eded39ee08f8407656537ccc71b1004104fe1602e267c", size = 1103065, upload-time = "2026-02-14T00:11:59.603Z" }, + { url = "https://files.pythonhosted.org/packages/c6/2e/3f73e8ca53718952222cacd0cf7eecc9db439d020f0c1fe7ae717e4e199a/openai-2.26.0-py3-none-any.whl", hash = "sha256:6151bf8f83802f036117f06cc8a57b3a4da60da9926826cc96747888b57f394f", size = 1136409, upload-time = "2026-03-05T23:17:34.072Z" }, ] [[package]]