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`
This commit is contained in:
Mason Daugherty
2026-03-12 10:55:36 -04:00
committed by GitHub
parent b676167707
commit 6b9b4c6546
5 changed files with 45 additions and 10 deletions

View File

@@ -7,7 +7,7 @@ from typing import TYPE_CHECKING, Any, Literal, TypeAlias, cast
import openai import openai
from langchain_core.messages import AIMessageChunk 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 langchain_openai.chat_models.base import BaseChatOpenAI
from pydantic import BaseModel, ConfigDict, Field, SecretStr, model_validator from pydantic import BaseModel, ConfigDict, Field, SecretStr, model_validator
from typing_extensions import Self 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: str = Field(default="grok-4", alias="model")
"""Model name to use.""" """Model name to use."""
xai_api_key: SecretStr | None = Field( xai_api_key: SecretStr | None = Field(
alias="api_key", alias="api_key",
default_factory=secret_from_env("XAI_API_KEY", default=None), 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. 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 search_parameters: dict[str, Any] | None = None
"""**Deprecated.** Use web search tools instead: """**Deprecated.** Use web search tools instead:

View File

@@ -17,6 +17,7 @@ def test_reasoning(output_version: Literal["", "v1"]) -> None:
"""Test reasoning features. """Test reasoning features.
!!! note !!! note
`grok-4` does not return `reasoning_content`, but may optionally return `grok-4` does not return `reasoning_content`, but may optionally return
encrypted reasoning content if `use_encrypted_content` is set to `True`. 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( chat_model = ChatXAI(
model="grok-3-mini", model="grok-3-mini",
reasoning_effort="low", reasoning_effort="low",
temperature=0,
output_version=output_version, output_version=output_version,
) )
else: else:
chat_model = ChatXAI( chat_model = ChatXAI(
model="grok-3-mini", model="grok-3-mini",
reasoning_effort="low", reasoning_effort="low",
temperature=0,
) )
input_message = "What is 3^3?" input_message = "What is 3^3?"
response = chat_model.invoke(input_message) response = chat_model.invoke(input_message)
@@ -95,7 +98,7 @@ def test_reasoning(output_version: Literal["", "v1"]) -> None:
def test_web_search() -> 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 # Test invoke
response = llm.invoke("Look up the current time in Boston, MA.") response = llm.invoke("Look up the current time in Boston, MA.")

View File

@@ -34,6 +34,7 @@ class TestXAIStandard(ChatModelIntegrationTests):
def chat_model_params(self) -> dict: def chat_model_params(self) -> dict:
return { return {
"model": MODEL_NAME, "model": MODEL_NAME,
"temperature": 0,
"rate_limiter": rate_limiter, "rate_limiter": rate_limiter,
} }

View File

@@ -12,6 +12,7 @@ from langchain_openai.chat_models.base import (
_convert_dict_to_message, _convert_dict_to_message,
_convert_message_to_dict, _convert_message_to_dict,
) )
from pydantic import SecretStr
from langchain_xai import ChatXAI 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] 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: def test_function_dict_to_message_function_message() -> None:
content = json.dumps({"result": "Example #1"}) content = json.dumps({"result": "Example #1"})
name = "test_function" name = "test_function"

View File

@@ -655,7 +655,7 @@ wheels = [
[[package]] [[package]]
name = "langchain-core" name = "langchain-core"
version = "1.2.13" version = "1.2.18"
source = { editable = "../../core" } source = { editable = "../../core" }
dependencies = [ dependencies = [
{ name = "jsonpatch" }, { name = "jsonpatch" },
@@ -715,7 +715,7 @@ typing = [
[[package]] [[package]]
name = "langchain-openai" name = "langchain-openai"
version = "1.1.10" version = "1.1.11"
source = { editable = "../openai" } source = { editable = "../openai" }
dependencies = [ dependencies = [
{ name = "langchain-core" }, { name = "langchain-core" },
@@ -726,7 +726,7 @@ dependencies = [
[package.metadata] [package.metadata]
requires-dist = [ requires-dist = [
{ name = "langchain-core", editable = "../../core" }, { 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" }, { name = "tiktoken", specifier = ">=0.7.0,<1.0.0" },
] ]
@@ -1228,7 +1228,7 @@ wheels = [
[[package]] [[package]]
name = "openai" name = "openai"
version = "2.21.0" version = "2.26.0"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
dependencies = [ dependencies = [
{ name = "anyio" }, { name = "anyio" },
@@ -1240,9 +1240,9 @@ dependencies = [
{ name = "tqdm" }, { name = "tqdm" },
{ name = "typing-extensions" }, { 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 = [ 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]] [[package]]