feat(model-profiles): distribute data across packages (#34024)

This commit is contained in:
ccurme authored and GitHub committed 2025-11-21 15:47:05 -05:00
1 parent ee3373afc2
commit 33e5d01f7c
74 files changed
+3278 -15714

No files matched your search

@@ -53,6 +53,10 @@ if TYPE_CHECKING:
ParrotFakeChatModel,
)
from langchain_core.language_models.llms import LLM, BaseLLM
from langchain_core.language_models.model_profile import (
ModelProfile,
ModelProfileRegistry,
)
__all__ = (
"LLM",
@@ -68,6 +72,8 @@ __all__ = (
"LanguageModelInput",
"LanguageModelLike",
"LanguageModelOutput",
"ModelProfile",
"ModelProfileRegistry",
"ParrotFakeChatModel",
"SimpleChatModel",
"get_tokenizer",
@@ -90,6 +96,8 @@ _dynamic_imports = {
"GenericFakeChatModel": "fake_chat_models",
"ParrotFakeChatModel": "fake_chat_models",
"LLM": "llms",
"ModelProfile": "model_profile",
"ModelProfileRegistry": "model_profile",
"BaseLLM": "llms",
"is_openai_data_block": "_utils",
}
@@ -15,7 +15,6 @@ from typing import TYPE_CHECKING, Any, Literal, cast
from pydantic import BaseModel, ConfigDict, Field
from typing_extensions import override
from langchain_core._api.beta_decorator import beta
from langchain_core.caches import BaseCache
from langchain_core.callbacks import (
AsyncCallbackManager,
@@ -34,6 +33,7 @@ from langchain_core.language_models.base import (
LangSmithParams,
LanguageModelInput,
)
from langchain_core.language_models.model_profile import ModelProfile
from langchain_core.load import dumpd, dumps
from langchain_core.messages import (
AIMessage,
@@ -76,8 +76,6 @@ from langchain_core.utils.utils import LC_ID_PREFIX, from_env
if TYPE_CHECKING:
import uuid
from langchain_model_profiles import ModelProfile # type: ignore[import-untyped]
from langchain_core.output_parsers.base import OutputParserLike
from langchain_core.runnables import Runnable, RunnableConfig
from langchain_core.tools import BaseTool
@@ -339,6 +337,21 @@ class BaseChatModel(BaseLanguageModel[AIMessage], ABC):
"""
profile: ModelProfile | None = Field(default=None, exclude=True)
"""Profile detailing model capabilities.
!!! warning "Beta feature"
This is a beta feature. The format of model profiles is subject to change.
If not specified, automatically loaded from the provider package on initialization
if data is available.
Example profile data includes context window sizes, supported modalities, or support
for tool calling, structured output, and other features.
!!! version-added "Added in `langchain-core` 1.1"
"""
model_config = ConfigDict(
arbitrary_types_allowed=True,
)
@@ -1688,40 +1701,6 @@ class BaseChatModel(BaseLanguageModel[AIMessage], ABC):
return RunnableMap(raw=llm) | parser_with_fallback
return llm | output_parser
@property
@beta()
def profile(self) -> ModelProfile:
"""Return profiling information for the model.
This property relies on the `langchain-model-profiles` package to retrieve chat
model capabilities, such as context window sizes and supported features.
Raises:
ImportError: If `langchain-model-profiles` is not installed.
Returns:
A `ModelProfile` object containing profiling information for the model.
"""
try:
from langchain_model_profiles import get_model_profile # noqa: PLC0415
except ImportError as err:
informative_error_message = (
"To access model profiling information, please install the "
"`langchain-model-profiles` package: "
"`pip install langchain-model-profiles`."
)
raise ImportError(informative_error_message) from err
provider_id = self._llm_type
model_name = (
# Model name is not standardized across integrations. New integrations
# should prefer `model`.
getattr(self, "model", None)
or getattr(self, "model_name", None)
or getattr(self, "model_id", "")
)
return get_model_profile(provider_id, model_name) or {}
class SimpleChatModel(BaseChatModel):
"""Simplified implementation for a chat model to inherit from.
@@ -0,0 +1,84 @@
"""Model profile types and utilities."""
from typing_extensions import TypedDict
class ModelProfile(TypedDict, total=False):
"""Model profile.
!!! warning "Beta feature"
This is a beta feature. The format of model profiles is subject to change.
Provides information about chat model capabilities, such as context window sizes
and supported features.
"""
# --- Input constraints ---
max_input_tokens: int
"""Maximum context window (tokens)"""
image_inputs: bool
"""Whether image inputs are supported."""
# TODO: add more detail about formats?
image_url_inputs: bool
"""Whether [image URL inputs](https://docs.langchain.com/oss/python/langchain/models#multimodal)
are supported."""
pdf_inputs: bool
"""Whether [PDF inputs](https://docs.langchain.com/oss/python/langchain/models#multimodal)
are supported."""
# TODO: add more detail about formats? e.g. bytes or base64
audio_inputs: bool
"""Whether [audio inputs](https://docs.langchain.com/oss/python/langchain/models#multimodal)
are supported."""
# TODO: add more detail about formats? e.g. bytes or base64
video_inputs: bool
"""Whether [video inputs](https://docs.langchain.com/oss/python/langchain/models#multimodal)
are supported."""
# TODO: add more detail about formats? e.g. bytes or base64
image_tool_message: bool
"""Whether images can be included in tool messages."""
pdf_tool_message: bool
"""Whether PDFs can be included in tool messages."""
# --- Output constraints ---
max_output_tokens: int
"""Maximum output tokens"""
reasoning_output: bool
"""Whether the model supports [reasoning / chain-of-thought](https://docs.langchain.com/oss/python/langchain/models#reasoning)"""
image_outputs: bool
"""Whether [image outputs](https://docs.langchain.com/oss/python/langchain/models#multimodal)
are supported."""
audio_outputs: bool
"""Whether [audio outputs](https://docs.langchain.com/oss/python/langchain/models#multimodal)
are supported."""
video_outputs: bool
"""Whether [video outputs](https://docs.langchain.com/oss/python/langchain/models#multimodal)
are supported."""
# --- Tool calling ---
tool_calling: bool
"""Whether the model supports [tool calling](https://docs.langchain.com/oss/python/langchain/models#tool-calling)"""
tool_choice: bool
"""Whether the model supports [tool choice](https://docs.langchain.com/oss/python/langchain/models#forcing-tool-calls)"""
# --- Structured output ---
structured_output: bool
"""Whether the model supports a native [structured output](https://docs.langchain.com/oss/python/langchain/models#structured-outputs)
feature"""
ModelProfileRegistry = dict[str, ModelProfile]
"""Registry mapping model identifiers or names to their ModelProfile."""
-3
View File
@@ -36,7 +36,6 @@ typing = [
"mypy>=1.18.1,<1.19.0",
"types-pyyaml>=6.0.12.2,<7.0.0.0",
"types-requests>=2.28.11.5,<3.0.0.0",
"langchain-model-profiles",
"langchain-text-splitters",
]
dev = [
@@ -58,7 +57,6 @@ test = [
"blockbuster>=1.5.18,<1.6.0",
"numpy>=1.26.4; python_version<'3.13'",
"numpy>=2.1.0; python_version>='3.13'",
"langchain-model-profiles",
"langchain-tests",
"pytest-benchmark",
"pytest-codspeed",
@@ -66,7 +64,6 @@ test = [
test_integration = []
[tool.uv.sources]
langchain-model-profiles = { path = "../model-profiles" }
langchain-tests = { path = "../standard-tests" }
langchain-text-splitters = { path = "../text-splitters" }
@@ -1222,19 +1222,12 @@ def test_get_ls_params() -> None:
def test_model_profiles() -> None:
model = GenericFakeChatModel(messages=iter([]))
profile = model.profile
assert profile == {}
assert model.profile is None
class MyModel(GenericFakeChatModel):
model: str = "gpt-5"
@property
def _llm_type(self) -> str:
return "openai-chat"
model = MyModel(messages=iter([]))
profile = model.profile
assert profile
model_with_profile = GenericFakeChatModel(
messages=iter([]), profile={"max_input_tokens": 100}
)
assert model_with_profile.profile == {"max_input_tokens": 100}
class MockResponse:
@@ -18,6 +18,8 @@ EXPECTED_ALL = [
"FakeStreamingListLLM",
"FakeListLLM",
"ParrotFakeChatModel",
"ModelProfile",
"ModelProfileRegistry",
"is_openai_data_block",
]
-44
View File
@@ -985,7 +985,6 @@ test = [
{ name = "blockbuster" },
{ name = "freezegun" },
{ name = "grandalf" },
{ name = "langchain-model-profiles" },
{ name = "langchain-tests" },
{ name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" },
{ name = "numpy", version = "2.3.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" },
@@ -1001,7 +1000,6 @@ test = [
{ name = "syrupy" },
]
typing = [
{ name = "langchain-model-profiles" },
{ name = "langchain-text-splitters" },
{ name = "mypy" },
{ name = "types-pyyaml" },
@@ -1031,7 +1029,6 @@ test = [
{ name = "blockbuster", specifier = ">=1.5.18,<1.6.0" },
{ name = "freezegun", specifier = ">=1.2.2,<2.0.0" },
{ name = "grandalf", specifier = ">=0.8.0,<1.0.0" },
{ name = "langchain-model-profiles", directory = "../model-profiles" },
{ name = "langchain-tests", directory = "../standard-tests" },
{ name = "numpy", marker = "python_full_version < '3.13'", specifier = ">=1.26.4" },
{ name = "numpy", marker = "python_full_version >= '3.13'", specifier = ">=2.1.0" },
@@ -1048,53 +1045,12 @@ test = [
]
test-integration = []
typing = [
{ name = "langchain-model-profiles", directory = "../model-profiles" },
{ name = "langchain-text-splitters", directory = "../text-splitters" },
{ name = "mypy", specifier = ">=1.18.1,<1.19.0" },
{ name = "types-pyyaml", specifier = ">=6.0.12.2,<7.0.0.0" },
{ name = "types-requests", specifier = ">=2.28.11.5,<3.0.0.0" },
]
[[package]]
name = "langchain-model-profiles"
version = "0.0.4"
source = { directory = "../model-profiles" }
dependencies = [
{ name = "tomli", marker = "python_full_version < '3.11'" },
{ name = "typing-extensions" },
]
[package.metadata]
requires-dist = [
{ name = "tomli", marker = "python_full_version < '3.11'", specifier = ">=2.0.0,<3.0.0" },
{ name = "typing-extensions", specifier = ">=4.7.0,<5.0.0" },
]
[package.metadata.requires-dev]
dev = [{ name = "httpx", specifier = ">=0.23.0,<1" }]
lint = [
{ name = "langchain", editable = "../langchain_v1" },
{ name = "ruff", specifier = ">=0.12.2,<0.13.0" },
]
test = [
{ name = "langchain", extras = ["openai"], editable = "../langchain_v1" },
{ name = "langchain-core", editable = "." },
{ name = "pytest", specifier = ">=8.0.0,<9.0.0" },
{ name = "pytest-asyncio", specifier = ">=0.23.2,<2.0.0" },
{ name = "pytest-cov", specifier = ">=4.0.0,<8.0.0" },
{ name = "pytest-mock" },
{ name = "pytest-socket", specifier = ">=0.6.0,<1.0.0" },
{ name = "pytest-watcher", specifier = ">=0.2.6,<1.0.0" },
{ name = "pytest-xdist", specifier = ">=3.6.1,<4.0.0" },
{ name = "syrupy", specifier = ">=4.0.2,<5.0.0" },
{ name = "toml", specifier = ">=0.10.2,<1.0.0" },
]
test-integration = [{ name = "langchain-core", editable = "." }]
typing = [
{ name = "mypy", specifier = ">=1.18.1,<1.19.0" },
{ name = "types-toml", specifier = ">=0.10.8.20240310,<1.0.0.0" },
]
[[package]]
name = "langchain-tests"
version = "1.0.1"
+10 -13
View File
@@ -64,7 +64,7 @@ if TYPE_CHECKING:
STRUCTURED_OUTPUT_ERROR_TEMPLATE = "Error: {error}\n Please fix your mistakes."
FALLBACK_MODELS_WITH_STRUCTURED_OUTPUT = [
# if langchain-model-profiles is not installed, these models are assumed to support
# if model profile data are not available, these models are assumed to support
# structured output
"grok",
"gpt-5",
@@ -381,18 +381,15 @@ def _supports_provider_strategy(model: str | BaseChatModel, tools: list | None =
or getattr(model, "model", None)
or getattr(model, "model_id", "")
)
try:
model_profile = model.profile
except ImportError:
pass
else:
if (
model_profile.get("structured_output")
# We make an exception for Gemini models, which currently do not support
# simultaneous tool use with structured output
and not (tools and isinstance(model_name, str) and "gemini" in model_name.lower())
):
return True
model_profile = model.profile
if (
model_profile is not None
and model_profile.get("structured_output")
# We make an exception for Gemini models, which currently do not support
# simultaneous tool use with structured output
and not (tools and isinstance(model_name, str) and "gemini" in model_name.lower())
):
return True
return (
any(part in model_name.lower() for part in FALLBACK_MODELS_WITH_STRUCTURED_OUTPUT)
@@ -159,9 +159,11 @@ class SummarizationMiddleware(AgentMiddleware):
requires_profile = True
if requires_profile and self._get_profile_limits() is None:
msg = (
"Model profile information is required to use fractional token limits. "
'pip install "langchain[model-profiles]" or use absolute token counts '
"instead."
"Model profile information is required to use fractional token limits, "
"and is unavailable for the specified model. Please use absolute token "
"counts instead, or pass "
'`\n\nChatModel(..., profile={"max_input_tokens": ...})`.\n\n'
"with a desired integer value of the model's maximum input tokens."
)
raise ValueError(msg)
@@ -308,7 +310,7 @@ class SummarizationMiddleware(AgentMiddleware):
"""Retrieve max input token limit from the model profile."""
try:
profile = self.model.profile
except (AttributeError, ImportError):
except AttributeError:
return None
if not isinstance(profile, Mapping):
-4
View File
@@ -18,7 +18,6 @@ dependencies = [
]
[project.optional-dependencies]
model-profiles = ["langchain-model-profiles"]
community = ["langchain-community"]
anthropic = ["langchain-anthropic"]
openai = ["langchain-openai"]
@@ -57,7 +56,6 @@ test = [
"pytest-mock",
"syrupy>=4.0.2,<5.0.0",
"toml>=0.10.2,<1.0.0",
"langchain-model-profiles",
"langchain-tests",
"langchain-openai",
]
@@ -76,7 +74,6 @@ test_integration = [
"cassio>=0.1.0,<1.0.0",
"langchainhub>=0.1.16,<1.0.0",
"langchain-core",
"langchain-model-profiles",
"langchain-text-splitters",
]
@@ -85,7 +82,6 @@ prerelease = "allow"
[tool.uv.sources]
langchain-core = { path = "../core", editable = true }
langchain-model-profiles = { path = "../model-profiles", editable = true }
langchain-tests = { path = "../standard-tests", editable = true }
langchain-text-splitters = { path = "../text-splitters", editable = true }
langchain-openai = { path = "../partners/openai", editable = true }
@@ -2,6 +2,7 @@ from typing import TYPE_CHECKING
from unittest.mock import patch
import pytest
from langchain_core.language_models import ModelProfile
from langchain_core.language_models.chat_models import BaseChatModel
from langchain_core.messages import AIMessage, AnyMessage, HumanMessage, RemoveMessage, ToolMessage
from langchain_core.outputs import ChatGeneration, ChatResult
@@ -11,9 +12,6 @@ from langchain.agents.middleware.summarization import SummarizationMiddleware
from ...model import FakeToolCallingModel
if TYPE_CHECKING:
from langchain_model_profiles import ModelProfile
class MockChatModel(BaseChatModel):
"""Mock chat model for testing."""
@@ -35,14 +33,12 @@ class ProfileChatModel(BaseChatModel):
def _generate(self, messages, **kwargs): # type: ignore[no-untyped-def]
return ChatResult(generations=[ChatGeneration(message=AIMessage(content="Summary"))])
profile: ModelProfile | None = {"max_input_tokens": 1000}
@property
def _llm_type(self) -> str:
return "mock"
@property
def profile(self) -> "ModelProfile":
return {"max_input_tokens": 1000}
def test_summarization_middleware_initialization() -> None:
"""Test SummarizationMiddleware initialization."""
+1 -51
View File
@@ -1967,9 +1967,6 @@ huggingface = [
mistralai = [
{ name = "langchain-mistralai" },
]
model-profiles = [
{ name = "langchain-model-profiles" },
]
ollama = [
{ name = "langchain-ollama" },
]
@@ -1991,7 +1988,6 @@ lint = [
{ name = "ruff" },
]
test = [
{ name = "langchain-model-profiles" },
{ name = "langchain-openai" },
{ name = "langchain-tests" },
{ name = "pytest" },
@@ -2007,7 +2003,6 @@ test = [
test-integration = [
{ name = "cassio" },
{ name = "langchain-core" },
{ name = "langchain-model-profiles" },
{ name = "langchain-text-splitters" },
{ name = "langchainhub" },
{ name = "python-dotenv" },
@@ -2033,7 +2028,6 @@ requires-dist = [
{ name = "langchain-groq", marker = "extra == 'groq'" },
{ name = "langchain-huggingface", marker = "extra == 'huggingface'" },
{ name = "langchain-mistralai", marker = "extra == 'mistralai'" },
{ name = "langchain-model-profiles", marker = "extra == 'model-profiles'", editable = "../model-profiles" },
{ name = "langchain-ollama", marker = "extra == 'ollama'" },
{ name = "langchain-openai", marker = "extra == 'openai'", editable = "../partners/openai" },
{ name = "langchain-perplexity", marker = "extra == 'perplexity'" },
@@ -2042,12 +2036,11 @@ requires-dist = [
{ name = "langgraph", specifier = ">=1.0.2,<1.1.0" },
{ name = "pydantic", specifier = ">=2.7.4,<3.0.0" },
]
provides-extras = ["model-profiles", "community", "anthropic", "openai", "azure-ai", "google-vertexai", "google-genai", "fireworks", "ollama", "together", "mistralai", "huggingface", "groq", "aws", "deepseek", "xai", "perplexity"]
provides-extras = ["community", "anthropic", "openai", "azure-ai", "google-vertexai", "google-genai", "fireworks", "ollama", "together", "mistralai", "huggingface", "groq", "aws", "deepseek", "xai", "perplexity"]
[package.metadata.requires-dev]
lint = [{ name = "ruff", specifier = ">=0.12.2,<0.13.0" }]
test = [
{ name = "langchain-model-profiles", editable = "../model-profiles" },
{ name = "langchain-openai", editable = "../partners/openai" },
{ name = "langchain-tests", editable = "../standard-tests" },
{ name = "pytest", specifier = ">=8.0.0,<9.0.0" },
@@ -2063,7 +2056,6 @@ test = [
test-integration = [
{ name = "cassio", specifier = ">=0.1.0,<1.0.0" },
{ name = "langchain-core", editable = "../core" },
{ name = "langchain-model-profiles", editable = "../model-profiles" },
{ name = "langchain-text-splitters", editable = "../text-splitters" },
{ name = "langchainhub", specifier = ">=0.1.16,<1.0.0" },
{ name = "python-dotenv", specifier = ">=1.0.0,<2.0.0" },
@@ -2208,7 +2200,6 @@ test = [
{ name = "blockbuster", specifier = ">=1.5.18,<1.6.0" },
{ name = "freezegun", specifier = ">=1.2.2,<2.0.0" },
{ name = "grandalf", specifier = ">=0.8.0,<1.0.0" },
{ name = "langchain-model-profiles", directory = "../model-profiles" },
{ name = "langchain-tests", directory = "../standard-tests" },
{ name = "numpy", marker = "python_full_version < '3.13'", specifier = ">=1.26.4" },
{ name = "numpy", marker = "python_full_version >= '3.13'", specifier = ">=2.1.0" },
@@ -2225,7 +2216,6 @@ test = [
]
test-integration = []
typing = [
{ name = "langchain-model-profiles", directory = "../model-profiles" },
{ name = "langchain-text-splitters", directory = "../text-splitters" },
{ name = "mypy", specifier = ">=1.18.1,<1.19.0" },
{ name = "types-pyyaml", specifier = ">=6.0.12.2,<7.0.0.0" },
@@ -2340,46 +2330,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/e6/5e/d1ed75e38ce431c49b609a524fc178ecbc38cc0a3ba44b5c3b935d82d824/langchain_mistralai-1.0.1-py3-none-any.whl", hash = "sha256:1777de4ceef3d3d5b2d08ab7cf6ce2d27188469eeae4d09756fd3b08ea3751a6", size = 17843, upload-time = "2025-10-24T13:55:39.358Z" },
]
[[package]]
name = "langchain-model-profiles"
version = "0.0.4"
source = { editable = "../model-profiles" }
dependencies = [
{ name = "tomli", marker = "python_full_version < '3.11'" },
{ name = "typing-extensions" },
]
[package.metadata]
requires-dist = [
{ name = "tomli", marker = "python_full_version < '3.11'", specifier = ">=2.0.0,<3.0.0" },
{ name = "typing-extensions", specifier = ">=4.7.0,<5.0.0" },
]
[package.metadata.requires-dev]
dev = [{ name = "httpx", specifier = ">=0.23.0,<1" }]
lint = [
{ name = "langchain", editable = "." },
{ name = "ruff", specifier = ">=0.12.2,<0.13.0" },
]
test = [
{ name = "langchain", extras = ["openai"], editable = "." },
{ name = "langchain-core", editable = "../core" },
{ name = "pytest", specifier = ">=8.0.0,<9.0.0" },
{ name = "pytest-asyncio", specifier = ">=0.23.2,<2.0.0" },
{ name = "pytest-cov", specifier = ">=4.0.0,<8.0.0" },
{ name = "pytest-mock" },
{ name = "pytest-socket", specifier = ">=0.6.0,<1.0.0" },
{ name = "pytest-watcher", specifier = ">=0.2.6,<1.0.0" },
{ name = "pytest-xdist", specifier = ">=3.6.1,<4.0.0" },
{ name = "syrupy", specifier = ">=4.0.2,<5.0.0" },
{ name = "toml", specifier = ">=0.10.2,<1.0.0" },
]
test-integration = [{ name = "langchain-core", editable = "../core" }]
typing = [
{ name = "mypy", specifier = ">=1.18.1,<1.19.0" },
{ name = "types-toml", specifier = ">=0.10.8.20240310,<1.0.0.0" },
]
[[package]]
name = "langchain-ollama"
version = "1.0.0"
+13 -32
View File
@@ -8,55 +8,36 @@
> [!WARNING]
> This package is currently in development and the API is subject to change.
Centralized reference of LLM capabilities for LangChain chat models.
CLI tool for updating model profile data in LangChain integration packages.
## Quick Install
```bash
pip install "langchain[model-profiles]"
pip install langchain-model-profiles
```
## 🤔 What is this?
`langchain-model-profiles` enables programmatic access to model capabilities through a `.profile` property on LangChain chat models.
`langchain-model-profiles` is a CLI tool for fetching and updating model capability data from [models.dev](https://github.com/sst/models.dev) for use in LangChain integration packages.
This allows you to query model-specific features such as context window sizes, supported input/output modalities, structured output support, tool calling capabilities, and more.
## 📖 Documentation
For full documentation, see the [API reference](https://reference.langchain.com/python/langchain_model_profiles/). For conceptual guides, tutorials, and examples on using LangChain, see the [LangChain Docs](https://docs.langchain.com/oss/python/langchain/overview).
---
LangChain chat models expose a `.profile` field that provides programmatic access to model capabilities such as context window sizes, supported modalities, tool calling, structured output, and more. This CLI tool helps maintainers keep that data up-to-date.
## Data sources
This package is built on top of the excellent work by the [models.dev](https://github.com/sst/models.dev) project, an open source initiative that provides model capability data.
This package augments the data from models.dev with some additional fields. We intend to keep this aligned with the upstream project as it evolves.
LangChain model profiles augment the data from models.dev with some additional fields. We intend to keep this aligned with the upstream project as it evolves.
## 📖 Documentation
For full documentation, see the [API reference](https://reference.langchain.com/python/langchain_model_profiles/). For conceptual guides, tutorials, and examples on using LangChain, see the [LangChain Docs](https://docs.langchain.com/oss/python/langchain/overview).
## Usage
Access model capabilities through the `.profile` property on any LangChain chat model:
Update model profile data for a specific provider:
```python
# pip install "langchain[openai]"
from langchain.chat_models import init_chat_model
model = init_chat_model("openai:gpt-5")
profile = model.profile
# Check specific capabilities
if profile.get("structured_output"):
print(f"This model supports a dedicated structured output feature.")
if profile.get("max_input_tokens"):
print(f"Max input tokens: {profile.get('max_input_tokens')}")
if profile.get("..."):
...
```bash
langchain-profiles refresh --provider anthropic --data-dir ./langchain_anthropic/data
```
## Available fields
See `ModelProfile` in [`model_profile.py`](./langchain_model_profiles/model_profile.py) for the full list of available fields and their descriptions.
This downloads the latest model data from models.dev, merges it with any augmentations defined in `profile_augmentations.toml`, and generates a `profiles.py` file.
@@ -1,8 +0,0 @@
"""Model profiles entrypoint."""
from langchain_model_profiles.model_profile import ModelProfile, get_model_profile
__all__ = [
"ModelProfile",
"get_model_profile",
]
@@ -1,141 +0,0 @@
"""Data loader for model profiles with augmentation support."""
import json
import sys
from functools import cached_property
from pathlib import Path
from typing import Any
if sys.version_info >= (3, 11):
import tomllib
else:
import tomli as tomllib
class _DataLoader:
"""Loads and merges model profile data from base and augmentations.
See the README in `data/augmentations` directory for more details on the
augmentation structure and merge priority.
"""
def __init__(self) -> None:
"""Initialize the loader."""
self._data_dir = Path(__file__).parent / "data"
@property
def _base_data_path(self) -> Path:
"""Get path to base data file.
`models.json` is the downloaded data from models.dev.
"""
return self._data_dir / "models.json"
@property
def _augmentations_dir(self) -> Path:
"""Get path to augmentations directory."""
return self._data_dir / "augmentations"
@cached_property
def _merged_data(self) -> dict[str, Any]:
"""Load and merge all data once at startup.
Merging order:
1. Base data from `models.json`
2. Provider-level augmentations from `augmentations/providers/{provider}.toml`
3. Model-level augmentations from `augmentations/models/{provider}/{model}.toml`
Returns:
Fully merged provider data with all augmentations applied.
"""
# Load base data; let exceptions propagate to user
with self._base_data_path.open("r") as f:
data = json.load(f)
provider_augmentations = self._load_provider_augmentations()
model_augmentations = self._load_model_augmentations()
# Merge contents
for provider_id, provider_data in data.items():
models = provider_data.get("models", {})
provider_aug = provider_augmentations.get(provider_id, {})
for model_id, model_data in models.items():
if provider_aug:
model_data.update(provider_aug)
# Apply model-level augmentations (highest priority)
model_aug = model_augmentations.get(provider_id, {}).get(model_id, {})
if model_aug:
model_data.update(model_aug)
return data
def _load_provider_augmentations(self) -> dict[str, dict[str, Any]]:
"""Load all provider-level augmentations.
Returns:
`dict` mapping provider IDs to their augmentation data.
"""
augmentations: dict[str, dict[str, Any]] = {}
providers_dir = self._augmentations_dir / "providers"
if not providers_dir.exists():
return augmentations
for toml_file in providers_dir.glob("*.toml"):
provider_id = toml_file.stem
with toml_file.open("rb") as f:
data = tomllib.load(f)
if "profile" in data:
augmentations[provider_id] = data["profile"]
return augmentations
def _load_model_augmentations(self) -> dict[str, dict[str, dict[str, Any]]]:
"""Load all model-level augmentations.
Returns:
Nested `dict`: `provider_id` -> `model_id` -> augmentation data.
"""
augmentations: dict[str, dict[str, dict[str, Any]]] = {}
models_dir = self._augmentations_dir / "models"
if not models_dir.exists():
return augmentations
for provider_dir in models_dir.iterdir():
if not provider_dir.is_dir():
continue
provider_id = provider_dir.name
augmentations[provider_id] = {}
for toml_file in provider_dir.glob("*.toml"):
model_id = toml_file.stem
with toml_file.open("rb") as f:
data = tomllib.load(f)
if "profile" in data:
augmentations[provider_id][model_id] = data["profile"]
return augmentations
def get_profile_data(
self, provider_id: str, model_id: str
) -> dict[str, Any] | None:
"""Get merged profile data for a specific model.
Args:
provider_id: The provider identifier.
model_id: The model identifier.
Returns:
Merged model data `dict` or `None` if not found.
"""
provider = self._merged_data.get(provider_id)
if provider is None:
return None
models = provider.get("models", {})
return models.get(model_id)
@@ -0,0 +1,356 @@
"""CLI for refreshing model profile data from models.dev."""
import argparse
import json
import sys
import tempfile
from pathlib import Path
from typing import Any
import httpx
try:
import tomllib # type: ignore[import-not-found] # Python 3.11+
except ImportError:
import tomli as tomllib # type: ignore[import-not-found,no-redef]
def _validate_data_dir(data_dir: Path) -> Path:
"""Validate and canonicalize data directory path.
Args:
data_dir: User-provided data directory path.
Returns:
Resolved, canonical path.
Raises:
SystemExit: If user declines to write outside current directory.
"""
# Resolve to absolute, canonical path (follows symlinks)
try:
resolved = data_dir.resolve(strict=False)
except (OSError, RuntimeError) as e:
msg = f"Invalid data directory path: {e}"
print(f"❌ {msg}", file=sys.stderr)
sys.exit(1)
# Warn if writing outside current directory
cwd = Path.cwd().resolve()
try:
resolved.relative_to(cwd)
except ValueError:
# Not relative to cwd
print("⚠️ WARNING: Writing outside current directory", file=sys.stderr)
print(f" Current directory: {cwd}", file=sys.stderr)
print(f" Target directory: {resolved}", file=sys.stderr)
print(file=sys.stderr)
response = input("Continue? (y/N): ")
if response.lower() != "y":
print("Aborted.", file=sys.stderr)
sys.exit(1)
return resolved
def _load_augmentations(
data_dir: Path,
) -> tuple[dict[str, Any], dict[str, dict[str, Any]]]:
"""Load augmentations from profile_augmentations.toml.
Args:
data_dir: Directory containing profile_augmentations.toml.
Returns:
Tuple of (provider_augmentations, model_augmentations).
"""
aug_file = data_dir / "profile_augmentations.toml"
if not aug_file.exists():
return {}, {}
try:
with aug_file.open("rb") as f:
data = tomllib.load(f)
except PermissionError:
msg = f"Permission denied reading augmentations file: {aug_file}"
print(f"❌ {msg}", file=sys.stderr)
sys.exit(1)
except tomllib.TOMLDecodeError as e:
msg = f"Invalid TOML syntax in augmentations file: {e}"
print(f"❌ {msg}", file=sys.stderr)
sys.exit(1)
except OSError as e:
msg = f"Failed to read augmentations file: {e}"
print(f"❌ {msg}", file=sys.stderr)
sys.exit(1)
overrides = data.get("overrides", {})
provider_aug: dict[str, Any] = {}
model_augs: dict[str, dict[str, Any]] = {}
for key, value in overrides.items():
if isinstance(value, dict):
model_augs[key] = value
else:
provider_aug[key] = value
return provider_aug, model_augs
def _model_data_to_profile(model_data: dict[str, Any]) -> dict[str, Any]:
"""Convert raw models.dev data into the canonical profile structure."""
limit = model_data.get("limit") or {}
modalities = model_data.get("modalities") or {}
input_modalities = modalities.get("input") or []
output_modalities = modalities.get("output") or []
profile = {
"max_input_tokens": limit.get("context"),
"max_output_tokens": limit.get("output"),
"image_inputs": "image" in input_modalities,
"audio_inputs": "audio" in input_modalities,
"pdf_inputs": "pdf" in input_modalities or model_data.get("pdf_inputs"),
"video_inputs": "video" in input_modalities,
"image_outputs": "image" in output_modalities,
"audio_outputs": "audio" in output_modalities,
"video_outputs": "video" in output_modalities,
"reasoning_output": model_data.get("reasoning"),
"tool_calling": model_data.get("tool_call"),
"tool_choice": model_data.get("tool_choice"),
"structured_output": model_data.get("structured_output"),
"image_url_inputs": model_data.get("image_url_inputs"),
"image_tool_message": model_data.get("image_tool_message"),
"pdf_tool_message": model_data.get("pdf_tool_message"),
}
return {k: v for k, v in profile.items() if v is not None}
def _apply_overrides(
profile: dict[str, Any], *overrides: dict[str, Any] | None
) -> dict[str, Any]:
"""Merge provider and model overrides onto the canonical profile."""
merged = dict(profile)
for override in overrides:
if not override:
continue
for key, value in override.items():
if value is not None:
merged[key] = value # noqa: PERF403
return merged
def _ensure_safe_output_path(base_dir: Path, output_file: Path) -> None:
"""Ensure the resolved output path remains inside the expected directory."""
if base_dir.exists() and base_dir.is_symlink():
msg = f"Data directory {base_dir} is a symlink; refusing to write profiles."
print(f"❌ {msg}", file=sys.stderr)
sys.exit(1)
if output_file.exists() and output_file.is_symlink():
msg = (
f"profiles.py at {output_file} is a symlink; refusing to overwrite it.\n"
"Delete the symlink or point --data-dir to a safe location."
)
print(f"❌ {msg}", file=sys.stderr)
sys.exit(1)
try:
output_file.resolve(strict=False).relative_to(base_dir.resolve())
except (OSError, RuntimeError) as e:
msg = f"Failed to resolve output path: {e}"
print(f"❌ {msg}", file=sys.stderr)
sys.exit(1)
except ValueError:
msg = f"Refusing to write outside of data directory: {output_file}"
print(f"❌ {msg}", file=sys.stderr)
sys.exit(1)
def _write_profiles_file(output_file: Path, contents: str) -> None:
"""Write the generated module atomically without following symlinks."""
_ensure_safe_output_path(output_file.parent, output_file)
temp_path: Path | None = None
try:
with tempfile.NamedTemporaryFile(
mode="w", encoding="utf-8", dir=output_file.parent, delete=False
) as tmp_file:
tmp_file.write(contents)
temp_path = Path(tmp_file.name)
temp_path.replace(output_file)
except PermissionError:
msg = f"Permission denied writing file: {output_file}"
print(f"❌ {msg}", file=sys.stderr)
if temp_path:
temp_path.unlink(missing_ok=True)
sys.exit(1)
except OSError as e:
msg = f"Failed to write file: {e}"
print(f"❌ {msg}", file=sys.stderr)
if temp_path:
temp_path.unlink(missing_ok=True)
sys.exit(1)
MODULE_ADMONITION = """Auto-generated model profiles.
DO NOT EDIT THIS FILE MANUALLY.
This file is generated by the langchain-profiles CLI tool.
It contains data derived from the models.dev project.
Source: https://github.com/sst/models.dev
License: MIT License
To update these data, refer to the instructions here:
https://docs.langchain.com/oss/python/langchain/models#updating-or-overwriting-profile-data
"""
def refresh(provider: str, data_dir: Path) -> None: # noqa: C901, PLR0915
"""Download and merge model profile data for a specific provider.
Args:
provider: Provider ID from models.dev (e.g., 'anthropic', 'openai').
data_dir: Directory containing profile_augmentations.toml and where profiles.py
will be written.
"""
# Validate and canonicalize data directory path
data_dir = _validate_data_dir(data_dir)
api_url = "https://models.dev/api.json"
print(f"Provider: {provider}")
print(f"Data directory: {data_dir}")
print()
# Download data from models.dev
print(f"Downloading data from {api_url}...")
try:
response = httpx.get(api_url, timeout=30)
response.raise_for_status()
except httpx.TimeoutException:
msg = f"Request timed out connecting to {api_url}"
print(f"❌ {msg}", file=sys.stderr)
sys.exit(1)
except httpx.HTTPStatusError as e:
msg = f"HTTP error {e.response.status_code} from {api_url}"
print(f"❌ {msg}", file=sys.stderr)
sys.exit(1)
except httpx.RequestError as e:
msg = f"Failed to connect to {api_url}: {e}"
print(f"❌ {msg}", file=sys.stderr)
sys.exit(1)
try:
all_data = response.json()
except json.JSONDecodeError as e:
msg = f"Invalid JSON response from API: {e}"
print(f"❌ {msg}", file=sys.stderr)
sys.exit(1)
# Basic validation
if not isinstance(all_data, dict):
msg = "Expected API response to be a dictionary"
print(f"❌ {msg}", file=sys.stderr)
sys.exit(1)
provider_count = len(all_data)
model_count = sum(len(p.get("models", {})) for p in all_data.values())
print(f"Downloaded {provider_count} providers with {model_count} models")
# Extract data for this provider
if provider not in all_data:
msg = f"Provider '{provider}' not found in models.dev data"
print(msg, file=sys.stderr)
sys.exit(1)
provider_data = all_data[provider]
models = provider_data.get("models", {})
print(f"Extracted {len(models)} models for {provider}")
# Load augmentations
print("Loading augmentations...")
provider_aug, model_augs = _load_augmentations(data_dir)
# Merge and convert to profiles
profiles: dict[str, dict[str, Any]] = {}
for model_id, model_data in models.items():
base_profile = _model_data_to_profile(model_data)
profiles[model_id] = _apply_overrides(
base_profile, provider_aug, model_augs.get(model_id)
)
# Include new models defined purely via augmentations
extra_models = set(model_augs) - set(models)
if extra_models:
print(f"Adding {len(extra_models)} models from augmentations only...")
for model_id in sorted(extra_models):
profiles[model_id] = _apply_overrides({}, provider_aug, model_augs[model_id])
# Ensure directory exists
try:
data_dir.mkdir(parents=True, exist_ok=True, mode=0o755)
except PermissionError:
msg = f"Permission denied creating directory: {data_dir}"
print(f"❌ {msg}", file=sys.stderr)
sys.exit(1)
except OSError as e:
msg = f"Failed to create directory: {e}"
print(f"❌ {msg}", file=sys.stderr)
sys.exit(1)
# Write as Python module
output_file = data_dir / "_profiles.py"
print(f"Writing to {output_file}...")
module_content = [f'"""{MODULE_ADMONITION}"""\n', "from typing import Any\n\n"]
module_content.append("_PROFILES: dict[str, dict[str, Any]] = ")
json_str = json.dumps(profiles, indent=4)
json_str = (
json_str.replace("true", "True")
.replace("false", "False")
.replace("null", "None")
)
module_content.append(f"{json_str}\n")
_write_profiles_file(output_file, "".join(module_content))
print(
f"✓ Successfully refreshed {len(profiles)} model profiles "
f"({output_file.stat().st_size:,} bytes)"
)
def main() -> None:
"""CLI entrypoint."""
parser = argparse.ArgumentParser(
description="Refresh model profile data from models.dev",
prog="langchain-profiles",
)
subparsers = parser.add_subparsers(dest="command", required=True)
# refresh command
refresh_parser = subparsers.add_parser(
"refresh", help="Download and merge model profile data for a provider"
)
refresh_parser.add_argument(
"--provider",
required=True,
help="Provider ID from models.dev (e.g., 'anthropic', 'openai', 'google')",
)
refresh_parser.add_argument(
"--data-dir",
required=True,
type=Path,
help="Data directory containing profile_augmentations.toml",
)
args = parser.parse_args()
if args.command == "refresh":
refresh(args.provider, args.data_dir)
if __name__ == "__main__":
main()
@@ -1,11 +0,0 @@
Data Attribution
================
The models.json file in this directory contains data derived from the models.dev project.
Source: https://github.com/sst/models.dev
Copyright (c) 2025 models.dev contributors
License: MIT License
This package augments the original models.dev data with additional fields
specific to LangChain integration.
@@ -1,40 +0,0 @@
# Profile augmentations
This directory contains LangChain-specific augmentations to the base model data from models.dev.
## Structure
```txt
augmentations/
├── providers/ # Provider-level augmentations (apply to all models from a provider)
│ ├── anthropic.toml
│ ├── openai.toml
│ └── ...
└── models/ # Model-specific augmentations (override provider defaults)
├── anthropic/
│ └── claude-sonnet-4-5-20250929.toml
└── openai/
└── o1.toml
```
## Merge priority
Data is merged in the following order (later overrides earlier):
1. Base data from `models.json` (from models.dev API)
2. Provider-level augmentations from `augmentations/providers/{provider}.toml`
3. Model-level augmentations from `augmentations/models/{provider}/{model}.toml`
## TOML format
All augmentation files should use the following structure:
```toml
[profile]
# Add or override model profile fields
image_url_inputs = true
pdf_inputs = true
tool_choice = true
```
Available fields match the `ModelProfile` TypedDict in `model_profile.py`.
@@ -1,6 +0,0 @@
[profile]
image_url_inputs = false
pdf_inputs = false
pdf_tool_message = false
image_tool_message = false
structured_output = false
@@ -1,6 +0,0 @@
[profile]
image_url_inputs = true
pdf_inputs = true
pdf_tool_message = true
image_tool_message = true
structured_output = false
@@ -1,6 +0,0 @@
[profile]
image_url_inputs = true
pdf_inputs = true
pdf_tool_message = true
image_tool_message = true
structured_output = false
@@ -1,6 +0,0 @@
[profile]
image_url_inputs = true
pdf_inputs = true
image_tool_message = true
tool_choice = true
structured_output = true
@@ -1,6 +0,0 @@
[profile]
image_url_inputs = true
pdf_inputs = true
image_tool_message = true
tool_choice = true
structured_output = true
@@ -1,7 +0,0 @@
[profile]
image_url_inputs = true
pdf_inputs = true
pdf_tool_message = true
image_tool_message = true
tool_choice = true
structured_output = true
File diff suppressed because it is too large. Load diff
@@ -1,171 +0,0 @@
"""Model profiles package."""
import re
from typing_extensions import TypedDict
from langchain_model_profiles._data_loader import _DataLoader
class ModelProfile(TypedDict, total=False):
"""Model profile."""
# --- Input constraints ---
max_input_tokens: int
"""Maximum context window (tokens)"""
image_inputs: bool
"""Whether image inputs are supported."""
# TODO: add more detail about formats?
image_url_inputs: bool
"""Whether [image URL inputs](https://docs.langchain.com/oss/python/langchain/models#multimodal)
are supported."""
pdf_inputs: bool
"""Whether [PDF inputs](https://docs.langchain.com/oss/python/langchain/models#multimodal)
are supported."""
# TODO: add more detail about formats? e.g. bytes or base64
audio_inputs: bool
"""Whether [audio inputs](https://docs.langchain.com/oss/python/langchain/models#multimodal)
are supported."""
# TODO: add more detail about formats? e.g. bytes or base64
video_inputs: bool
"""Whether [video inputs](https://docs.langchain.com/oss/python/langchain/models#multimodal)
are supported."""
# TODO: add more detail about formats? e.g. bytes or base64
image_tool_message: bool
"""TODO: description."""
pdf_tool_message: bool
"""TODO: description."""
# --- Output constraints ---
max_output_tokens: int
"""Maximum output tokens"""
reasoning_output: bool
"""Whether the model supports [reasoning / chain-of-thought](https://docs.langchain.com/oss/python/langchain/models#reasoning)"""
image_outputs: bool
"""Whether [image outputs](https://docs.langchain.com/oss/python/langchain/models#multimodal)
are supported."""
audio_outputs: bool
"""Whether [audio outputs](https://docs.langchain.com/oss/python/langchain/models#multimodal)
are supported."""
video_outputs: bool
"""Whether [video outputs](https://docs.langchain.com/oss/python/langchain/models#multimodal)
are supported."""
# --- Tool calling ---
tool_calling: bool
"""Whether the model supports [tool calling](https://docs.langchain.com/oss/python/langchain/models#tool-calling)"""
tool_choice: bool
"""Whether the model supports [tool choice](https://docs.langchain.com/oss/python/langchain/models#forcing-tool-calls)"""
# --- Structured output ---
structured_output: bool
"""Whether the model supports a native [structured output](https://docs.langchain.com/oss/python/langchain/models#structured-outputs)
feature"""
_LOADER = _DataLoader()
_lc_type_to_provider_id = {
"openai-chat": "openai",
"azure-openai-chat": "azure",
"anthropic-chat": "anthropic",
"chat-google-generative-ai": "google",
"vertexai": "google-vertex",
"anthropic-chat-vertexai": "google-vertex-anthropic",
"amazon_bedrock_chat": "amazon-bedrock",
"amazon_bedrock_converse_chat": "amazon-bedrock",
"chat-ai21": "ai21",
"chat-deepseek": "deepseek",
"fireworks-chat": "fireworks-ai",
"groq-chat": "groq",
"huggingface-chat-wrapper": "huggingface",
"mistralai-chat": "mistral",
"chat-ollama": "ollama",
"perplexitychat": "perplexity",
"together-chat": "togetherai",
"upstage-chat": "upstage",
"xai-chat": "xai",
}
def _translate_provider_and_model_id(provider: str, model: str) -> tuple[str, str]:
"""Translate LangChain provider and model to models.dev equivalents.
Args:
provider: LangChain provider ID.
model: LangChain model ID.
Returns:
A tuple containing the models.dev provider ID and model ID.
"""
provider_id = _lc_type_to_provider_id.get(provider, provider)
if provider_id in ("google", "google-vertex"):
# convert models/gemini-2.0-flash-001 to gemini-2.0-flash
model_id = re.sub(r"-\d{3}$", "", model.replace("models/", ""))
elif provider_id == "amazon-bedrock":
# strip region prefixes like "us."
model_id = re.sub(r"^[A-Za-z]{2}\.", "", model)
else:
model_id = model
return provider_id, model_id
def get_model_profile(provider: str, model: str) -> ModelProfile | None:
"""Get the model capabilities for a given model.
Args:
provider: Identifier for provider (e.g., `'openai'`, `'anthropic'`).
model: Identifier for model (e.g., `'gpt-5'`,
`'claude-sonnet-4-5-20250929'`).
Returns:
The model capabilities or `None` if not found in the data.
"""
if not provider or not model:
return None
provider_id, model_id = _translate_provider_and_model_id(provider, model)
data = _LOADER.get_profile_data(provider_id, model_id)
if not data:
# If either (1) provider not found or (2) model not found under matched provider
return None
# Map models.dev & augmentation fields -> ModelProfile fields
# See schema reference to see fields dropped: https://github.com/sst/models.dev?tab=readme-ov-file#schema-reference
profile = {
"max_input_tokens": data.get("limit", {}).get("context"),
"image_inputs": "image" in data.get("modalities", {}).get("input", []),
"image_url_inputs": data.get("image_url_inputs"),
"image_tool_message": data.get("image_tool_message"),
"audio_inputs": "audio" in data.get("modalities", {}).get("input", []),
"pdf_inputs": "pdf" in data.get("modalities", {}).get("input", [])
or data.get("pdf_inputs"),
"pdf_tool_message": data.get("pdf_tool_message"),
"video_inputs": "video" in data.get("modalities", {}).get("input", []),
"max_output_tokens": data.get("limit", {}).get("output"),
"reasoning_output": data.get("reasoning"),
"image_outputs": "image" in data.get("modalities", {}).get("output", []),
"audio_outputs": "audio" in data.get("modalities", {}).get("output", []),
"video_outputs": "video" in data.get("modalities", {}).get("output", []),
"tool_calling": data.get("tool_call"),
"tool_choice": data.get("tool_choice"),
"structured_output": data.get("structured_output"),
}
return ModelProfile(**{k: v for k, v in profile.items() if v is not None}) # type: ignore[typeddict-item]
+7 -4
View File
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
[project]
name = "langchain-model-profiles"
description = "Centralized reference of LLM capabilities."
description = "CLI tool for updating model profile data in LangChain integration packages."
readme = "README.md"
license = { text = "MIT" }
authors = []
@@ -12,10 +12,14 @@ authors = []
version = "0.0.4"
requires-python = ">=3.10.0,<4.0.0"
dependencies = [
"httpx>=0.23.0,<1",
"tomli>=2.0.0,<3.0.0; python_version < '3.11'",
"typing-extensions>=4.7.0,<5.0.0",
]
[project.scripts]
langchain-profiles = "langchain_model_profiles.cli:main"
[project.urls]
Homepage = "https://docs.langchain.com/"
Documentation = "https://reference.langchain.com/python/langchain_model_profiles/"
@@ -25,9 +29,7 @@ Slack = "https://www.langchain.com/join-community"
Reddit = "https://www.reddit.com/r/LangChain/"
[dependency-groups]
dev = [
"httpx>=0.23.0,<1", # For refresh_data.py script
]
dev = []
test = [
"pytest>=8.0.0,<9.0.0",
@@ -80,6 +82,7 @@ ignore = [
"FIX002",
"TD002",
"TD003",
"T201", # Allow print statements (CLI tool)
]
unfixable = ["B028"] # People should intentionally tune the stacklevel
@@ -1,80 +0,0 @@
#!/usr/bin/env python3
"""Refresh model profile data from models.dev.
Update the bundled model data by running:
python scripts/refresh_data.py
"""
import json
from pathlib import Path
import httpx
PROVIDER_SUBSET = [
# This is done to limit the data size
"amazon-bedrock",
"anthropic",
"azure",
"baseten",
"cerebras",
"cloudflare-workers-ai",
"deepinfra",
"deepseek",
"fireworks-ai",
"google",
"google-vertex",
"google-vertex-anthropic",
"groq",
"huggingface",
"lmstudio",
"mistral",
"nebius",
"nvidia",
"openai",
"openrouter",
"perplexity",
"togetherai",
"upstage",
"xai",
]
def main() -> None:
"""Download and save the latest model data from models.dev."""
api_url = "https://models.dev/api.json"
output_dir = Path(__file__).parent.parent / "langchain_model_profiles" / "data"
output_file = output_dir / "models.json"
print(f"Downloading data from {api_url}...") # noqa: T201
response = httpx.get(api_url, timeout=30)
response.raise_for_status()
data = response.json()
# Basic validation
if not isinstance(data, dict):
msg = "Expected API response to be a dictionary"
raise TypeError(msg)
provider_count = len(data)
model_count = sum(len(provider.get("models", {})) for provider in data.values())
print(f"Downloaded {provider_count} providers with {model_count} models") # noqa: T201
# Subset providers
data = {k: v for k, v in data.items() if k in PROVIDER_SUBSET}
print(f"Filtered to {len(data)} providers based on subset") # noqa: T201
# Ensure directory exists
output_dir.mkdir(parents=True, exist_ok=True)
# Write with pretty formatting for readability
print(f"Writing to {output_file}...") # noqa: T201
with output_file.open("w") as f:
json.dump(data, f, indent=2, sort_keys=True)
print(f"✓ Successfully refreshed model data ({output_file.stat().st_size:,} bytes)") # noqa: T201
if __name__ == "__main__":
main()
@@ -1,18 +0,0 @@
"""End to end test for fetching model profiles from a chat model."""
from langchain.chat_models import init_chat_model
def test_chat_model() -> None:
"""Test that chat model gets profile data correctly."""
model = init_chat_model("openai:gpt-5", api_key="foo")
assert model.profile
assert model.profile["max_input_tokens"] == 400000
assert model.profile["structured_output"]
assert model.profile["pdf_inputs"]
def test_chat_model_no_data() -> None:
"""Test that chat model handles missing profile data."""
model = init_chat_model("openai:gpt-fake", api_key="foo")
assert model.profile == {}
@@ -0,0 +1,216 @@
"""Tests for CLI functionality."""
import importlib.util
from pathlib import Path
from unittest.mock import Mock, patch
import pytest
from langchain_model_profiles.cli import refresh
@pytest.fixture
def mock_models_dev_response() -> dict:
"""Create a mock response from models.dev API."""
return {
"anthropic": {
"id": "anthropic",
"name": "Anthropic",
"models": {
"claude-3-opus": {
"id": "claude-3-opus",
"name": "Claude 3 Opus",
"tool_call": True,
"limit": {"context": 200000, "output": 4096},
"modalities": {"input": ["text", "image"], "output": ["text"]},
},
"claude-3-sonnet": {
"id": "claude-3-sonnet",
"name": "Claude 3 Sonnet",
"tool_call": True,
"limit": {"context": 200000, "output": 4096},
"modalities": {"input": ["text", "image"], "output": ["text"]},
},
},
},
"openai": {
"id": "openai",
"name": "OpenAI",
"models": {
"gpt-4": {
"id": "gpt-4",
"name": "GPT-4",
"tool_call": True,
"limit": {"context": 8192, "output": 4096},
"modalities": {"input": ["text"], "output": ["text"]},
}
},
},
}
def test_refresh_generates_profiles_file(
tmp_path: Path, mock_models_dev_response: dict
) -> None:
"""Test that refresh command generates _profiles.py with merged data."""
data_dir = tmp_path / "data"
data_dir.mkdir()
# Create augmentations file
aug_file = data_dir / "profile_augmentations.toml"
aug_file.write_text("""
provider = "anthropic"
[overrides]
image_url_inputs = true
pdf_inputs = true
""")
# Mock the httpx.get call
mock_response = Mock()
mock_response.json.return_value = mock_models_dev_response
mock_response.raise_for_status = Mock()
with (
patch("langchain_model_profiles.cli.httpx.get", return_value=mock_response),
patch("builtins.input", return_value="y"),
):
refresh("anthropic", data_dir)
# Verify _profiles.py was created
profiles_file = data_dir / "_profiles.py"
assert profiles_file.exists()
# Import and verify content
profiles_content = profiles_file.read_text()
assert "DO NOT EDIT THIS FILE MANUALLY" in profiles_content
assert "PROFILES:" in profiles_content
assert "claude-3-opus" in profiles_content
assert "claude-3-sonnet" in profiles_content
# Check that augmentations were applied
assert "image_url_inputs" in profiles_content
assert "pdf_inputs" in profiles_content
def test_refresh_raises_error_for_missing_provider(
tmp_path: Path, mock_models_dev_response: dict
) -> None:
"""Test that refresh exits with error for non-existent provider."""
data_dir = tmp_path / "data"
data_dir.mkdir()
# Mock the httpx.get call
mock_response = Mock()
mock_response.json.return_value = mock_models_dev_response
mock_response.raise_for_status = Mock()
with (
patch("langchain_model_profiles.cli.httpx.get", return_value=mock_response),
patch("builtins.input", return_value="y"),
):
with pytest.raises(SystemExit) as exc_info:
refresh("nonexistent-provider", data_dir)
assert exc_info.value.code == 1
# Output file should not be created
profiles_file = data_dir / "_profiles.py"
assert not profiles_file.exists()
def test_refresh_works_without_augmentations(
tmp_path: Path, mock_models_dev_response: dict
) -> None:
"""Test that refresh works even without augmentations file."""
data_dir = tmp_path / "data"
data_dir.mkdir()
# Mock the httpx.get call
mock_response = Mock()
mock_response.json.return_value = mock_models_dev_response
mock_response.raise_for_status = Mock()
with (
patch("langchain_model_profiles.cli.httpx.get", return_value=mock_response),
patch("builtins.input", return_value="y"),
):
refresh("anthropic", data_dir)
# Verify _profiles.py was created
profiles_file = data_dir / "_profiles.py"
assert profiles_file.exists()
assert profiles_file.stat().st_size > 0
def test_refresh_aborts_when_user_declines_external_directory(
tmp_path: Path, mock_models_dev_response: dict
) -> None:
"""Test that refresh aborts when user declines writing to external directory."""
data_dir = tmp_path / "data"
data_dir.mkdir()
# Mock the httpx.get call
mock_response = Mock()
mock_response.json.return_value = mock_models_dev_response
mock_response.raise_for_status = Mock()
with (
patch("langchain_model_profiles.cli.httpx.get", return_value=mock_response),
patch("builtins.input", return_value="n"), # User declines
):
with pytest.raises(SystemExit) as exc_info:
refresh("anthropic", data_dir)
assert exc_info.value.code == 1
# Verify _profiles.py was NOT created
profiles_file = data_dir / "_profiles.py"
assert not profiles_file.exists()
def test_refresh_includes_models_defined_only_in_augmentations(
tmp_path: Path, mock_models_dev_response: dict
) -> None:
"""Ensure models that only exist in augmentations are emitted."""
data_dir = tmp_path / "data"
data_dir.mkdir()
aug_file = data_dir / "profile_augmentations.toml"
aug_file.write_text("""
provider = "anthropic"
[overrides."custom-offline-model"]
structured_output = true
pdf_inputs = true
max_input_tokens = 123
""")
mock_response = Mock()
mock_response.json.return_value = mock_models_dev_response
mock_response.raise_for_status = Mock()
with (
patch("langchain_model_profiles.cli.httpx.get", return_value=mock_response),
patch("builtins.input", return_value="y"),
):
refresh("anthropic", data_dir)
profiles_file = data_dir / "_profiles.py"
assert profiles_file.exists()
spec = importlib.util.spec_from_file_location(
"generated_profiles_aug_only", profiles_file
)
assert spec
assert spec.loader
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module) # type: ignore[union-attr]
assert "custom-offline-model" in module._PROFILES # type: ignore[attr-defined]
assert (
module._PROFILES["custom-offline-model"]["structured_output"] is True # type: ignore[index]
)
assert (
module._PROFILES["custom-offline-model"]["max_input_tokens"] == 123 # type: ignore[index]
)
@@ -1,142 +0,0 @@
"""Tests for data loader with augmentation support."""
import json
from pathlib import Path
import pytest
from langchain_model_profiles._data_loader import _DataLoader
@pytest.fixture
def temp_data_dir(tmp_path: Path) -> Path:
"""Create a temporary data directory structure."""
data_dir = tmp_path / "data"
data_dir.mkdir()
# Create base models.json
base_data = {
"test-provider": {
"id": "test-provider",
"models": {
"test-model": {
"id": "test-model",
"name": "Test Model",
"tool_call": True,
"limit": {"context": 8000, "output": 4000},
"modalities": {"input": ["text"], "output": ["text"]},
}
},
}
}
with (data_dir / "models.json").open("w") as f:
json.dump(base_data, f)
# Create augmentations directories
aug_dir = data_dir / "augmentations"
(aug_dir / "providers").mkdir(parents=True)
(aug_dir / "models" / "test-provider").mkdir(parents=True)
return data_dir
def test_load_base_data_only(temp_data_dir: Path) -> None:
"""Test loading base data without augmentations."""
loader = _DataLoader()
# Patch before any property access
loader._data_dir = temp_data_dir
result = loader.get_profile_data("test-provider", "test-model")
assert result is not None
assert result["id"] == "test-model"
assert result["name"] == "Test Model"
assert result["tool_call"] is True
def test_provider_level_augmentation(temp_data_dir: Path) -> None:
"""Test provider-level augmentations are applied."""
# Add provider augmentation
provider_toml = temp_data_dir / "augmentations" / "providers" / "test-provider.toml"
provider_toml.write_text("""
[profile]
image_url_inputs = true
pdf_inputs = true
""")
loader = _DataLoader()
loader._data_dir = temp_data_dir
result = loader.get_profile_data("test-provider", "test-model")
assert result is not None
assert result["image_url_inputs"] is True
assert result["pdf_inputs"] is True
# Base data should still be present
assert result["tool_call"] is True
def test_model_level_augmentation_overrides_provider(temp_data_dir: Path) -> None:
"""Test model-level augmentations override provider augmentations."""
# Add provider augmentation
provider_toml = temp_data_dir / "augmentations" / "providers" / "test-provider.toml"
provider_toml.write_text("""
[profile]
image_url_inputs = true
pdf_inputs = false
""")
# Add model augmentation that overrides
model_toml = (
temp_data_dir / "augmentations" / "models" / "test-provider" / "test-model.toml"
)
model_toml.write_text("""
[profile]
pdf_inputs = true
reasoning_output = true
""")
loader = _DataLoader()
loader._data_dir = temp_data_dir
result = loader.get_profile_data("test-provider", "test-model")
assert result is not None
# From provider
assert result["image_url_inputs"] is True
# Overridden by model
assert result["pdf_inputs"] is True
# From model only
assert result["reasoning_output"] is True
# From base
assert result["tool_call"] is True
def test_missing_provider(temp_data_dir: Path) -> None:
"""Test returns None for missing provider."""
loader = _DataLoader()
loader._data_dir = temp_data_dir
result = loader.get_profile_data("nonexistent-provider", "test-model")
assert result is None
def test_missing_model(temp_data_dir: Path) -> None:
"""Test returns None for missing model."""
loader = _DataLoader()
loader._data_dir = temp_data_dir
result = loader.get_profile_data("test-provider", "nonexistent-model")
assert result is None
def test_merged_data_is_cached(temp_data_dir: Path) -> None:
"""Test that merged data is cached after first access."""
loader = _DataLoader()
loader._data_dir = temp_data_dir
# First access
result1 = loader.get_profile_data("test-provider", "test-model")
# Second access should use cached data
result2 = loader.get_profile_data("test-provider", "test-model")
assert result1 == result2
# Verify it's using the cached property by checking _merged_data was accessed
assert hasattr(loader, "_merged_data")
@@ -1,31 +0,0 @@
"""Test provider and model ID mappings."""
import pytest
from langchain_model_profiles.model_profile import get_model_profile
@pytest.mark.parametrize(
("provider", "model_id"),
[
("openai-chat", "gpt-5"),
("azure-openai-chat", "gpt-5"),
("anthropic-chat", "claude-sonnet-4-5"),
("vertexai", "models/gemini-2.0-flash-001"),
("chat-google-generative-ai", "models/gemini-2.0-flash-001"),
("amazon_bedrock_chat", "anthropic.claude-sonnet-4-20250514-v1:0"),
("amazon_bedrock_converse_chat", "anthropic.claude-sonnet-4-20250514-v1:0"),
# ("chat-ai21", "jamba-mini"), # no data yet # noqa: ERA001
("chat-deepseek", "deepseek-reasoner"),
("fireworks-chat", "accounts/fireworks/models/gpt-oss-20b"),
("groq-chat", "llama-3.3-70b-versatile"),
("huggingface-chat-wrapper", "Qwen/Qwen3-235B-A22B-Thinking-2507"),
("mistralai-chat", "mistral-large-latest"),
# ("chat-ollama", "llama3.1"), # no data yet # noqa: ERA001
("perplexitychat", "sonar"),
("xai-chat", "grok-4"),
],
)
def test_id_translation(provider: str, model_id: str) -> None:
"""Test translation from LangChain to model / provider IDs."""
assert get_model_profile(provider, model_id)
+6 -10
View File
@@ -486,7 +486,6 @@ requires-dist = [
{ name = "langchain-groq", marker = "extra == 'groq'" },
{ name = "langchain-huggingface", marker = "extra == 'huggingface'" },
{ name = "langchain-mistralai", marker = "extra == 'mistralai'" },
{ name = "langchain-model-profiles", marker = "extra == 'model-profiles'" },
{ name = "langchain-ollama", marker = "extra == 'ollama'" },
{ name = "langchain-openai", marker = "extra == 'openai'", editable = "../partners/openai" },
{ name = "langchain-perplexity", marker = "extra == 'perplexity'" },
@@ -495,7 +494,7 @@ requires-dist = [
{ name = "langgraph", specifier = ">=1.0.2,<1.1.0" },
{ name = "pydantic", specifier = ">=2.7.4,<3.0.0" },
]
provides-extras = ["model-profiles", "community", "anthropic", "openai", "azure-ai", "google-vertexai", "google-genai", "fireworks", "ollama", "together", "mistralai", "huggingface", "groq", "aws", "deepseek", "xai", "perplexity"]
provides-extras = ["community", "anthropic", "openai", "azure-ai", "google-vertexai", "google-genai", "fireworks", "ollama", "together", "mistralai", "huggingface", "groq", "aws", "deepseek", "xai", "perplexity"]
[package.metadata.requires-dev]
lint = [{ name = "ruff", specifier = ">=0.12.2,<0.13.0" }]
@@ -528,7 +527,7 @@ typing = [
[[package]]
name = "langchain-core"
version = "1.0.4"
version = "1.0.7"
source = { editable = "../core" }
dependencies = [
{ name = "jsonpatch" },
@@ -562,7 +561,6 @@ test = [
{ name = "blockbuster", specifier = ">=1.5.18,<1.6.0" },
{ name = "freezegun", specifier = ">=1.2.2,<2.0.0" },
{ name = "grandalf", specifier = ">=0.8.0,<1.0.0" },
{ name = "langchain-model-profiles", directory = "." },
{ name = "langchain-tests", directory = "../standard-tests" },
{ name = "numpy", marker = "python_full_version < '3.13'", specifier = ">=1.26.4" },
{ name = "numpy", marker = "python_full_version >= '3.13'", specifier = ">=2.1.0" },
@@ -579,7 +577,6 @@ test = [
]
test-integration = []
typing = [
{ name = "langchain-model-profiles", directory = "." },
{ name = "langchain-text-splitters", directory = "../text-splitters" },
{ name = "mypy", specifier = ">=1.18.1,<1.19.0" },
{ name = "types-pyyaml", specifier = ">=6.0.12.2,<7.0.0.0" },
@@ -591,14 +588,12 @@ name = "langchain-model-profiles"
version = "0.0.4"
source = { editable = "." }
dependencies = [
{ name = "httpx" },
{ name = "tomli", marker = "python_full_version < '3.11'" },
{ name = "typing-extensions" },
]
[package.dev-dependencies]
dev = [
{ name = "httpx" },
]
lint = [
{ name = "langchain" },
{ name = "ruff" },
@@ -626,12 +621,13 @@ typing = [
[package.metadata]
requires-dist = [
{ name = "httpx", specifier = ">=0.23.0,<1" },
{ name = "tomli", marker = "python_full_version < '3.11'", specifier = ">=2.0.0,<3.0.0" },
{ name = "typing-extensions", specifier = ">=4.7.0,<5.0.0" },
]
[package.metadata.requires-dev]
dev = [{ name = "httpx", specifier = ">=0.23.0,<1" }]
dev = []
lint = [
{ name = "langchain", editable = "../langchain_v1" },
{ name = "ruff", specifier = ">=0.12.2,<0.13.0" },
@@ -657,7 +653,7 @@ typing = [
[[package]]
name = "langchain-openai"
version = "1.0.2"
version = "1.0.3"
source = { editable = "../partners/openai" }
dependencies = [
{ name = "langchain-core" },
@@ -17,7 +17,11 @@ from langchain_core.callbacks import (
CallbackManagerForLLMRun,
)
from langchain_core.exceptions import OutputParserException
from langchain_core.language_models import LanguageModelInput
from langchain_core.language_models import (
LanguageModelInput,
ModelProfile,
ModelProfileRegistry,
)
from langchain_core.language_models.chat_models import BaseChatModel, LangSmithParams
from langchain_core.messages import (
AIMessage,
@@ -50,13 +54,14 @@ from langchain_core.utils.function_calling import (
from langchain_core.utils.pydantic import is_basemodel_subclass
from langchain_core.utils.utils import _build_model_kwargs
from pydantic import BaseModel, ConfigDict, Field, SecretStr, model_validator
from typing_extensions import NotRequired, TypedDict
from typing_extensions import NotRequired, Self, TypedDict
from langchain_anthropic._client_utils import (
_get_default_async_httpx_client,
_get_default_httpx_client,
)
from langchain_anthropic._compat import _convert_from_v1_to_anthropic
from langchain_anthropic.data._profiles import _PROFILES
from langchain_anthropic.output_parsers import extract_tool_calls
_message_type_lookups = {
@@ -66,6 +71,13 @@ _message_type_lookups = {
"HumanMessageChunk": "user",
}
_MODEL_PROFILES = cast(ModelProfileRegistry, _PROFILES)
def _get_default_model_profile(model_name: str) -> ModelProfile:
default = _MODEL_PROFILES.get(model_name) or {}
return default.copy()
_MODEL_DEFAULT_MAX_OUTPUT_TOKENS: Final[dict[str, int]] = {
# Listed old to new
@@ -1610,6 +1622,13 @@ class ChatAnthropic(BaseChatModel):
all_required_field_names = get_pydantic_field_names(cls)
return _build_model_kwargs(values, all_required_field_names)
@model_validator(mode="after")
def _set_model_profile(self) -> Self:
"""Set model profile if not overridden."""
if self.profile is None:
self.profile = _get_default_model_profile(self.model)
return self
@cached_property
def _client_params(self) -> dict[str, Any]:
client_params: dict[str, Any] = {
@@ -0,0 +1 @@
"""Model profile data. All edits should be made in profile_augmentations.toml."""
@@ -0,0 +1,342 @@
"""Auto-generated model profiles.
DO NOT EDIT THIS FILE MANUALLY.
This file is generated by the langchain-profiles CLI tool.
It contains data derived from the models.dev project.
Source: https://github.com/sst/models.dev
License: MIT License
To update these data, refer to the instructions here:
https://docs.langchain.com/oss/python/langchain/models#updating-or-overwriting-profile-data
"""
from typing import Any
_PROFILES: dict[str, dict[str, Any]] = {
"claude-opus-4-0": {
"max_input_tokens": 200000,
"max_output_tokens": 32000,
"image_inputs": True,
"audio_inputs": False,
"video_inputs": False,
"image_outputs": False,
"audio_outputs": False,
"video_outputs": False,
"reasoning_output": True,
"tool_calling": True,
"image_url_inputs": True,
"pdf_inputs": True,
"pdf_tool_message": True,
"image_tool_message": True,
"structured_output": False,
},
"claude-3-5-sonnet-20241022": {
"max_input_tokens": 200000,
"max_output_tokens": 8192,
"image_inputs": True,
"audio_inputs": False,
"video_inputs": False,
"image_outputs": False,
"audio_outputs": False,
"video_outputs": False,
"reasoning_output": False,
"tool_calling": True,
"image_url_inputs": True,
"pdf_inputs": True,
"pdf_tool_message": True,
"image_tool_message": True,
"structured_output": False,
},
"claude-opus-4-1": {
"max_input_tokens": 200000,
"max_output_tokens": 32000,
"image_inputs": True,
"audio_inputs": False,
"video_inputs": False,
"image_outputs": False,
"audio_outputs": False,
"video_outputs": False,
"reasoning_output": True,
"tool_calling": True,
"image_url_inputs": True,
"pdf_inputs": True,
"pdf_tool_message": True,
"image_tool_message": True,
"structured_output": True,
},
"claude-haiku-4-5": {
"max_input_tokens": 200000,
"max_output_tokens": 64000,
"image_inputs": True,
"audio_inputs": False,
"video_inputs": False,
"image_outputs": False,
"audio_outputs": False,
"video_outputs": False,
"reasoning_output": True,
"tool_calling": True,
"image_url_inputs": True,
"pdf_inputs": True,
"pdf_tool_message": True,
"image_tool_message": True,
"structured_output": False,
},
"claude-3-5-sonnet-20240620": {
"max_input_tokens": 200000,
"max_output_tokens": 8192,
"image_inputs": True,
"audio_inputs": False,
"video_inputs": False,
"image_outputs": False,
"audio_outputs": False,
"video_outputs": False,
"reasoning_output": False,
"tool_calling": True,
"image_url_inputs": True,
"pdf_inputs": True,
"pdf_tool_message": True,
"image_tool_message": True,
"structured_output": False,
},
"claude-3-5-haiku-latest": {
"max_input_tokens": 200000,
"max_output_tokens": 8192,
"image_inputs": True,
"audio_inputs": False,
"video_inputs": False,
"image_outputs": False,
"audio_outputs": False,
"video_outputs": False,
"reasoning_output": False,
"tool_calling": True,
"image_url_inputs": True,
"pdf_inputs": True,
"pdf_tool_message": True,
"image_tool_message": True,
"structured_output": False,
},
"claude-3-opus-20240229": {
"max_input_tokens": 200000,
"max_output_tokens": 4096,
"image_inputs": True,
"audio_inputs": False,
"video_inputs": False,
"image_outputs": False,
"audio_outputs": False,
"video_outputs": False,
"reasoning_output": False,
"tool_calling": True,
"image_url_inputs": True,
"pdf_inputs": True,
"pdf_tool_message": True,
"image_tool_message": True,
"structured_output": False,
},
"claude-sonnet-4-5": {
"max_input_tokens": 200000,
"max_output_tokens": 64000,
"image_inputs": True,
"audio_inputs": False,
"video_inputs": False,
"image_outputs": False,
"audio_outputs": False,
"video_outputs": False,
"reasoning_output": True,
"tool_calling": True,
"image_url_inputs": True,
"pdf_inputs": True,
"pdf_tool_message": True,
"image_tool_message": True,
"structured_output": True,
},
"claude-sonnet-4-5-20250929": {
"max_input_tokens": 200000,
"max_output_tokens": 64000,
"image_inputs": True,
"audio_inputs": False,
"video_inputs": False,
"image_outputs": False,
"audio_outputs": False,
"video_outputs": False,
"reasoning_output": True,
"tool_calling": True,
"image_url_inputs": True,
"pdf_inputs": True,
"pdf_tool_message": True,
"image_tool_message": True,
"structured_output": False,
},
"claude-sonnet-4-20250514": {
"max_input_tokens": 200000,
"max_output_tokens": 64000,
"image_inputs": True,
"audio_inputs": False,
"video_inputs": False,
"image_outputs": False,
"audio_outputs": False,
"video_outputs": False,
"reasoning_output": True,
"tool_calling": True,
"image_url_inputs": True,
"pdf_inputs": True,
"pdf_tool_message": True,
"image_tool_message": True,
"structured_output": False,
},
"claude-opus-4-20250514": {
"max_input_tokens": 200000,
"max_output_tokens": 32000,
"image_inputs": True,
"audio_inputs": False,
"video_inputs": False,
"image_outputs": False,
"audio_outputs": False,
"video_outputs": False,
"reasoning_output": True,
"tool_calling": True,
"image_url_inputs": True,
"pdf_inputs": True,
"pdf_tool_message": True,
"image_tool_message": True,
"structured_output": False,
},
"claude-3-5-haiku-20241022": {
"max_input_tokens": 200000,
"max_output_tokens": 8192,
"image_inputs": True,
"audio_inputs": False,
"video_inputs": False,
"image_outputs": False,
"audio_outputs": False,
"video_outputs": False,
"reasoning_output": False,
"tool_calling": True,
"image_url_inputs": True,
"pdf_inputs": True,
"pdf_tool_message": True,
"image_tool_message": True,
"structured_output": False,
},
"claude-3-haiku-20240307": {
"max_input_tokens": 200000,
"max_output_tokens": 4096,
"image_inputs": True,
"audio_inputs": False,
"video_inputs": False,
"image_outputs": False,
"audio_outputs": False,
"video_outputs": False,
"reasoning_output": False,
"tool_calling": True,
"image_url_inputs": True,
"pdf_inputs": True,
"pdf_tool_message": True,
"image_tool_message": True,
"structured_output": False,
},
"claude-3-7-sonnet-20250219": {
"max_input_tokens": 200000,
"max_output_tokens": 64000,
"image_inputs": True,
"audio_inputs": False,
"video_inputs": False,
"image_outputs": False,
"audio_outputs": False,
"video_outputs": False,
"reasoning_output": True,
"tool_calling": True,
"image_url_inputs": True,
"pdf_inputs": True,
"pdf_tool_message": True,
"image_tool_message": True,
"structured_output": False,
},
"claude-3-7-sonnet-latest": {
"max_input_tokens": 200000,
"max_output_tokens": 64000,
"image_inputs": True,
"audio_inputs": False,
"video_inputs": False,
"image_outputs": False,
"audio_outputs": False,
"video_outputs": False,
"reasoning_output": True,
"tool_calling": True,
"image_url_inputs": True,
"pdf_inputs": True,
"pdf_tool_message": True,
"image_tool_message": True,
"structured_output": False,
},
"claude-sonnet-4-0": {
"max_input_tokens": 200000,
"max_output_tokens": 64000,
"image_inputs": True,
"audio_inputs": False,
"video_inputs": False,
"image_outputs": False,
"audio_outputs": False,
"video_outputs": False,
"reasoning_output": True,
"tool_calling": True,
"image_url_inputs": True,
"pdf_inputs": True,
"pdf_tool_message": True,
"image_tool_message": True,
"structured_output": False,
},
"claude-opus-4-1-20250805": {
"max_input_tokens": 200000,
"max_output_tokens": 32000,
"image_inputs": True,
"audio_inputs": False,
"video_inputs": False,
"image_outputs": False,
"audio_outputs": False,
"video_outputs": False,
"reasoning_output": True,
"tool_calling": True,
"image_url_inputs": True,
"pdf_inputs": True,
"pdf_tool_message": True,
"image_tool_message": True,
"structured_output": False,
},
"claude-3-sonnet-20240229": {
"max_input_tokens": 200000,
"max_output_tokens": 4096,
"image_inputs": True,
"audio_inputs": False,
"video_inputs": False,
"image_outputs": False,
"audio_outputs": False,
"video_outputs": False,
"reasoning_output": False,
"tool_calling": True,
"image_url_inputs": True,
"pdf_inputs": True,
"pdf_tool_message": True,
"image_tool_message": True,
"structured_output": False,
},
"claude-haiku-4-5-20251001": {
"max_input_tokens": 200000,
"max_output_tokens": 64000,
"image_inputs": True,
"audio_inputs": False,
"video_inputs": False,
"image_outputs": False,
"audio_outputs": False,
"video_outputs": False,
"reasoning_output": True,
"tool_calling": True,
"image_url_inputs": True,
"pdf_inputs": True,
"pdf_tool_message": True,
"image_tool_message": True,
"structured_output": False,
},
}
@@ -0,0 +1,14 @@
provider = "anthropic"
[overrides]
image_url_inputs = true
pdf_inputs = true
pdf_tool_message = true
image_tool_message = true
structured_output = false
[overrides."claude-sonnet-4-5"]
structured_output = true
[overrides."claude-opus-4-1"]
structured_output = true
+1
View File
@@ -29,6 +29,7 @@ Reddit = "https://www.reddit.com/r/LangChain/"
[dependency-groups]
test = [
"pytest>=7.3.0,<8.0.0",
"blockbuster>=1.5.5,<1.6",
"freezegun>=1.2.2,<2.0.0",
"pytest-mock>=3.10.0,<4.0.0",
"syrupy>=4.0.2,<5.0.0",
@@ -10,6 +10,7 @@ from unittest.mock import MagicMock, patch
import anthropic
import pytest
from anthropic.types import Message, TextBlock, Usage
from blockbuster import blockbuster_ctx
from langchain_core.messages import AIMessage, HumanMessage, SystemMessage, ToolMessage
from langchain_core.runnables import RunnableBinding
from langchain_core.tools import BaseTool
@@ -1597,3 +1598,33 @@ def test_strict_tool_use() -> None:
tool_definition = model_with_tools.kwargs["tools"][0] # type: ignore[attr-defined]
assert tool_definition["strict"] is True
def test_profile() -> None:
model = ChatAnthropic(model="claude-sonnet-4-20250514")
assert model.profile
assert not model.profile["structured_output"]
model = ChatAnthropic(model="claude-sonnet-4-5")
assert model.profile
assert model.profile["structured_output"]
assert model.profile["tool_calling"]
# Test overwriting a field
model.profile["tool_calling"] = False
assert not model.profile["tool_calling"]
# Test we didn't mutate
model = ChatAnthropic(model="claude-sonnet-4-5")
assert model.profile
assert model.profile["tool_calling"]
# Test passing in profile
model = ChatAnthropic(model="claude-sonnet-4-5", profile={"tool_calling": False})
assert model.profile == {"tool_calling": False}
async def test_model_profile_not_blocking() -> None:
with blockbuster_ctx():
model = ChatAnthropic(model="claude-sonnet-4-5")
_ = model.profile
+20 -2
View File
@@ -53,6 +53,18 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/15/b3/9b1a8074496371342ec1e796a96f99c82c945a339cd81a8e73de28b4cf9e/anyio-4.11.0-py3-none-any.whl", hash = "sha256:0287e96f4d26d4149305414d4e3bc32f0dcd0862365a4bddea19d7a1ec38c4fc", size = 109097, upload-time = "2025-09-23T09:19:10.601Z" },
]
[[package]]
name = "blockbuster"
version = "1.5.25"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "forbiddenfruit", marker = "implementation_name == 'cpython'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/7f/bc/57c49465decaeeedd58ce2d970b4cdfd93a74ba9993abff2dc498a31c283/blockbuster-1.5.25.tar.gz", hash = "sha256:b72f1d2aefdeecd2a820ddf1e1c8593bf00b96e9fdc4cd2199ebafd06f7cb8f0", size = 36058, upload-time = "2025-07-14T16:00:20.766Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/0b/01/dccc277c014f171f61a6047bb22c684e16c7f2db6bb5c8cce1feaf41ec55/blockbuster-1.5.25-py3-none-any.whl", hash = "sha256:cb06229762273e0f5f3accdaed3d2c5a3b61b055e38843de202311ede21bb0f5", size = 13196, upload-time = "2025-07-14T16:00:19.396Z" },
]
[[package]]
name = "certifi"
version = "2025.11.12"
@@ -290,6 +302,12 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/ab/84/02fc1827e8cdded4aa65baef11296a9bbe595c474f0d6d758af082d849fd/execnet-2.1.2-py3-none-any.whl", hash = "sha256:67fba928dd5a544b783f6056f449e5e3931a5c378b128bc18501f7ea79e296ec", size = 40708, upload-time = "2025-11-12T09:56:36.333Z" },
]
[[package]]
name = "forbiddenfruit"
version = "0.1.4"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/e6/79/d4f20e91327c98096d605646bdc6a5ffedae820f38d378d3515c42ec5e60/forbiddenfruit-0.1.4.tar.gz", hash = "sha256:e3f7e66561a29ae129aac139a85d610dbf3dd896128187ed5454b6421f624253", size = 43756, upload-time = "2021-01-16T21:03:35.401Z" }
[[package]]
name = "freezegun"
version = "1.5.5"
@@ -559,6 +577,7 @@ lint = [
{ name = "ruff" },
]
test = [
{ name = "blockbuster" },
{ name = "defusedxml" },
{ name = "freezegun" },
{ name = "langchain" },
@@ -597,6 +616,7 @@ requires-dist = [
dev = [{ name = "langchain-core", editable = "../../core" }]
lint = [{ name = "ruff", specifier = ">=0.13.1,<0.14.0" }]
test = [
{ name = "blockbuster", specifier = ">=1.5.5,<1.6" },
{ name = "defusedxml", specifier = ">=0.7.1,<1.0.0" },
{ name = "freezegun", specifier = ">=1.2.2,<2.0.0" },
{ name = "langchain", editable = "../../langchain_v1" },
@@ -660,7 +680,6 @@ test = [
{ name = "blockbuster", specifier = ">=1.5.18,<1.6.0" },
{ name = "freezegun", specifier = ">=1.2.2,<2.0.0" },
{ name = "grandalf", specifier = ">=0.8.0,<1.0.0" },
{ name = "langchain-model-profiles", directory = "../../model-profiles" },
{ name = "langchain-tests", directory = "../../standard-tests" },
{ name = "numpy", marker = "python_full_version < '3.13'", specifier = ">=1.26.4" },
{ name = "numpy", marker = "python_full_version >= '3.13'", specifier = ">=2.1.0" },
@@ -677,7 +696,6 @@ test = [
]
test-integration = []
typing = [
{ name = "langchain-model-profiles", directory = "../../model-profiles" },
{ name = "langchain-text-splitters", directory = "../../text-splitters" },
{ name = "mypy", specifier = ">=1.18.1,<1.19.0" },
{ name = "types-pyyaml", specifier = ">=6.0.12.2,<7.0.0.0" },
@@ -5,13 +5,18 @@ from __future__ import annotations
import json
from collections.abc import Callable, Iterator, Sequence
from json import JSONDecodeError
from typing import Any, Literal, TypeAlias
from typing import Any, Literal, TypeAlias, cast
import openai
from langchain_core.callbacks import (
CallbackManagerForLLMRun,
)
from langchain_core.language_models import LangSmithParams, LanguageModelInput
from langchain_core.language_models import (
LangSmithParams,
LanguageModelInput,
ModelProfile,
ModelProfileRegistry,
)
from langchain_core.messages import AIMessage, AIMessageChunk, BaseMessage
from langchain_core.outputs import ChatGenerationChunk, ChatResult
from langchain_core.runnables import Runnable
@@ -21,6 +26,8 @@ from langchain_openai.chat_models.base import BaseChatOpenAI
from pydantic import BaseModel, ConfigDict, Field, SecretStr, model_validator
from typing_extensions import Self
from langchain_deepseek.data._profiles import _PROFILES
DEFAULT_API_BASE = "https://api.deepseek.com/v1"
DEFAULT_BETA_API_BASE = "https://api.deepseek.com/beta"
@@ -28,6 +35,14 @@ _DictOrPydanticClass: TypeAlias = dict[str, Any] | type[BaseModel]
_DictOrPydantic: TypeAlias = dict[str, Any] | BaseModel
_MODEL_PROFILES = cast("ModelProfileRegistry", _PROFILES)
def _get_default_model_profile(model_name: str) -> ModelProfile:
default = _MODEL_PROFILES.get(model_name) or {}
return default.copy()
class ChatDeepSeek(BaseChatOpenAI):
"""DeepSeek chat model integration to access models hosted in DeepSeek's API.
@@ -232,6 +247,13 @@ class ChatDeepSeek(BaseChatOpenAI):
self.async_client = self.root_async_client.chat.completions
return self
@model_validator(mode="after")
def _set_model_profile(self) -> Self:
"""Set model profile if not overridden."""
if self.profile is None:
self.profile = _get_default_model_profile(self.model_name)
return self
def _get_request_payload(
self,
input_: LanguageModelInput,
@@ -0,0 +1 @@
"""Model profile data. All edits should be made in profile_augmentations.toml."""
@@ -0,0 +1,43 @@
"""Auto-generated model profiles.
DO NOT EDIT THIS FILE MANUALLY.
This file is generated by the langchain-profiles CLI tool.
It contains data derived from the models.dev project.
Source: https://github.com/sst/models.dev
License: MIT License
To update these data, refer to the instructions here:
https://docs.langchain.com/oss/python/langchain/models#updating-or-overwriting-profile-data
"""
from typing import Any
_PROFILES: dict[str, dict[str, Any]] = {
"deepseek-chat": {
"max_input_tokens": 128000,
"max_output_tokens": 8192,
"image_inputs": False,
"audio_inputs": False,
"video_inputs": False,
"image_outputs": False,
"audio_outputs": False,
"video_outputs": False,
"reasoning_output": False,
"tool_calling": True,
},
"deepseek-reasoner": {
"max_input_tokens": 128000,
"max_output_tokens": 128000,
"image_inputs": False,
"audio_inputs": False,
"video_inputs": False,
"image_outputs": False,
"audio_outputs": False,
"video_outputs": False,
"reasoning_output": True,
"tool_calling": True,
},
}
@@ -307,3 +307,10 @@ class TestChatDeepSeekStrictMode:
# The structured model should work with beta endpoint
assert structured_model is not None
def test_profile() -> None:
"""Test that model profile is loaded correctly."""
model = ChatDeepSeek(model="deepseek-reasoner", api_key=SecretStr("test_key"))
assert model.profile is not None
assert model.profile["reasoning_output"]
@@ -18,7 +18,11 @@ from langchain_core.callbacks import (
AsyncCallbackManagerForLLMRun,
CallbackManagerForLLMRun,
)
from langchain_core.language_models import LanguageModelInput
from langchain_core.language_models import (
LanguageModelInput,
ModelProfile,
ModelProfileRegistry,
)
from langchain_core.language_models.chat_models import (
BaseChatModel,
LangSmithParams,
@@ -79,10 +83,19 @@ from pydantic import (
from typing_extensions import Self
from langchain_fireworks._compat import _convert_from_v1_to_chat_completions
from langchain_fireworks.data._profiles import _PROFILES
logger = logging.getLogger(__name__)
_MODEL_PROFILES = cast("ModelProfileRegistry", _PROFILES)
def _get_default_model_profile(model_name: str) -> ModelProfile:
default = _MODEL_PROFILES.get(model_name) or {}
return default.copy()
def _convert_dict_to_message(_dict: Mapping[str, Any]) -> BaseMessage:
"""Convert a dictionary to a LangChain message.
@@ -404,6 +417,13 @@ class ChatFireworks(BaseChatModel):
self.async_client._max_retries = self.max_retries
return self
@model_validator(mode="after")
def _set_model_profile(self) -> Self:
"""Set model profile if not overridden."""
if self.profile is None:
self.profile = _get_default_model_profile(self.model_name)
return self
@property
def _default_params(self) -> dict[str, Any]:
"""Get the default parameters for calling Fireworks API."""
@@ -0,0 +1 @@
"""Model profile data. All edits should be made in profile_augmentations.toml."""
@@ -0,0 +1,151 @@
"""Auto-generated model profiles.
DO NOT EDIT THIS FILE MANUALLY.
This file is generated by the langchain-profiles CLI tool.
It contains data derived from the models.dev project.
Source: https://github.com/sst/models.dev
License: MIT License
To update these data, refer to the instructions here:
https://docs.langchain.com/oss/python/langchain/models#updating-or-overwriting-profile-data
"""
from typing import Any
_PROFILES: dict[str, dict[str, Any]] = {
"accounts/fireworks/models/deepseek-r1-0528": {
"max_input_tokens": 160000,
"max_output_tokens": 16384,
"image_inputs": False,
"audio_inputs": False,
"video_inputs": False,
"image_outputs": False,
"audio_outputs": False,
"video_outputs": False,
"reasoning_output": True,
"tool_calling": True,
},
"accounts/fireworks/models/deepseek-v3p1": {
"max_input_tokens": 163840,
"max_output_tokens": 163840,
"image_inputs": False,
"audio_inputs": False,
"video_inputs": False,
"image_outputs": False,
"audio_outputs": False,
"video_outputs": False,
"reasoning_output": True,
"tool_calling": True,
},
"accounts/fireworks/models/minimax-m2": {
"max_input_tokens": 128000,
"max_output_tokens": 16384,
"image_inputs": False,
"audio_inputs": False,
"video_inputs": False,
"image_outputs": False,
"audio_outputs": False,
"video_outputs": False,
"reasoning_output": True,
"tool_calling": True,
},
"accounts/fireworks/models/deepseek-v3-0324": {
"max_input_tokens": 160000,
"max_output_tokens": 16384,
"image_inputs": False,
"audio_inputs": False,
"video_inputs": False,
"image_outputs": False,
"audio_outputs": False,
"video_outputs": False,
"reasoning_output": False,
"tool_calling": True,
},
"accounts/fireworks/models/kimi-k2-instruct": {
"max_input_tokens": 128000,
"max_output_tokens": 16384,
"image_inputs": False,
"audio_inputs": False,
"video_inputs": False,
"image_outputs": False,
"audio_outputs": False,
"video_outputs": False,
"reasoning_output": False,
"tool_calling": True,
},
"accounts/fireworks/models/qwen3-235b-a22b": {
"max_input_tokens": 128000,
"max_output_tokens": 16384,
"image_inputs": False,
"audio_inputs": False,
"video_inputs": False,
"image_outputs": False,
"audio_outputs": False,
"video_outputs": False,
"reasoning_output": True,
"tool_calling": True,
},
"accounts/fireworks/models/gpt-oss-20b": {
"max_input_tokens": 131072,
"max_output_tokens": 32768,
"image_inputs": False,
"audio_inputs": False,
"video_inputs": False,
"image_outputs": False,
"audio_outputs": False,
"video_outputs": False,
"reasoning_output": True,
"tool_calling": True,
},
"accounts/fireworks/models/gpt-oss-120b": {
"max_input_tokens": 131072,
"max_output_tokens": 32768,
"image_inputs": False,
"audio_inputs": False,
"video_inputs": False,
"image_outputs": False,
"audio_outputs": False,
"video_outputs": False,
"reasoning_output": True,
"tool_calling": True,
},
"accounts/fireworks/models/glm-4p5-air": {
"max_input_tokens": 131072,
"max_output_tokens": 131072,
"image_inputs": False,
"audio_inputs": False,
"video_inputs": False,
"image_outputs": False,
"audio_outputs": False,
"video_outputs": False,
"reasoning_output": True,
"tool_calling": True,
},
"accounts/fireworks/models/qwen3-coder-480b-a35b-instruct": {
"max_input_tokens": 256000,
"max_output_tokens": 32768,
"image_inputs": False,
"audio_inputs": False,
"video_inputs": False,
"image_outputs": False,
"audio_outputs": False,
"video_outputs": False,
"reasoning_output": False,
"tool_calling": True,
},
"accounts/fireworks/models/glm-4p5": {
"max_input_tokens": 131072,
"max_output_tokens": 131072,
"image_inputs": False,
"audio_inputs": False,
"video_inputs": False,
"image_outputs": False,
"audio_outputs": False,
"video_outputs": False,
"reasoning_output": True,
"tool_calling": True,
},
}
@@ -35,3 +35,12 @@ class TestFireworksStandard(ChatModelUnitTests):
"fireworks_api_base": "https://base.com",
},
)
def test_profile() -> None:
"""Test that model profile is loaded correctly."""
model = ChatFireworks(
model="accounts/fireworks/models/gpt-oss-20b",
api_key="test_key", # type: ignore[arg-type]
)
assert model.profile
@@ -12,7 +12,11 @@ from langchain_core.callbacks import (
AsyncCallbackManagerForLLMRun,
CallbackManagerForLLMRun,
)
from langchain_core.language_models import LanguageModelInput
from langchain_core.language_models import (
LanguageModelInput,
ModelProfile,
ModelProfileRegistry,
)
from langchain_core.language_models.chat_models import (
BaseChatModel,
LangSmithParams,
@@ -63,8 +67,16 @@ from pydantic import BaseModel, ConfigDict, Field, SecretStr, model_validator
from typing_extensions import Self
from langchain_groq._compat import _convert_from_v1_to_groq
from langchain_groq.data._profiles import _PROFILES
from langchain_groq.version import __version__
_MODEL_PROFILES = cast("ModelProfileRegistry", _PROFILES)
def _get_default_model_profile(model_name: str) -> ModelProfile:
default = _MODEL_PROFILES.get(model_name) or {}
return default.copy()
class ChatGroq(BaseChatModel):
r"""Groq Chat large language models API.
@@ -490,6 +502,13 @@ class ChatGroq(BaseChatModel):
raise ImportError(msg) from exc
return self
@model_validator(mode="after")
def _set_model_profile(self) -> Self:
"""Set model profile if not overridden."""
if self.profile is None:
self.profile = _get_default_model_profile(self.model_name)
return self
#
# Serializable class method overrides
#
@@ -0,0 +1 @@
"""Model profile data. All edits should be made in profile_augmentations.toml."""
@@ -0,0 +1,223 @@
"""Auto-generated model profiles.
DO NOT EDIT THIS FILE MANUALLY.
This file is generated by the langchain-profiles CLI tool.
It contains data derived from the models.dev project.
Source: https://github.com/sst/models.dev
License: MIT License
To update these data, refer to the instructions here:
https://docs.langchain.com/oss/python/langchain/models#updating-or-overwriting-profile-data
"""
from typing import Any
_PROFILES: dict[str, dict[str, Any]] = {
"llama-3.1-8b-instant": {
"max_input_tokens": 131072,
"max_output_tokens": 8192,
"image_inputs": False,
"audio_inputs": False,
"video_inputs": False,
"image_outputs": False,
"audio_outputs": False,
"video_outputs": False,
"reasoning_output": False,
"tool_calling": True,
},
"mistral-saba-24b": {
"max_input_tokens": 32768,
"max_output_tokens": 32768,
"image_inputs": False,
"audio_inputs": False,
"video_inputs": False,
"image_outputs": False,
"audio_outputs": False,
"video_outputs": False,
"reasoning_output": False,
"tool_calling": True,
},
"llama3-8b-8192": {
"max_input_tokens": 8192,
"max_output_tokens": 8192,
"image_inputs": False,
"audio_inputs": False,
"video_inputs": False,
"image_outputs": False,
"audio_outputs": False,
"video_outputs": False,
"reasoning_output": False,
"tool_calling": True,
},
"qwen-qwq-32b": {
"max_input_tokens": 131072,
"max_output_tokens": 16384,
"image_inputs": False,
"audio_inputs": False,
"video_inputs": False,
"image_outputs": False,
"audio_outputs": False,
"video_outputs": False,
"reasoning_output": True,
"tool_calling": True,
},
"llama3-70b-8192": {
"max_input_tokens": 8192,
"max_output_tokens": 8192,
"image_inputs": False,
"audio_inputs": False,
"video_inputs": False,
"image_outputs": False,
"audio_outputs": False,
"video_outputs": False,
"reasoning_output": False,
"tool_calling": True,
},
"deepseek-r1-distill-llama-70b": {
"max_input_tokens": 131072,
"max_output_tokens": 8192,
"image_inputs": False,
"audio_inputs": False,
"video_inputs": False,
"image_outputs": False,
"audio_outputs": False,
"video_outputs": False,
"reasoning_output": True,
"tool_calling": True,
},
"llama-guard-3-8b": {
"max_input_tokens": 8192,
"max_output_tokens": 8192,
"image_inputs": False,
"audio_inputs": False,
"video_inputs": False,
"image_outputs": False,
"audio_outputs": False,
"video_outputs": False,
"reasoning_output": False,
"tool_calling": False,
},
"gemma2-9b-it": {
"max_input_tokens": 8192,
"max_output_tokens": 8192,
"image_inputs": False,
"audio_inputs": False,
"video_inputs": False,
"image_outputs": False,
"audio_outputs": False,
"video_outputs": False,
"reasoning_output": False,
"tool_calling": True,
},
"llama-3.3-70b-versatile": {
"max_input_tokens": 131072,
"max_output_tokens": 32768,
"image_inputs": False,
"audio_inputs": False,
"video_inputs": False,
"image_outputs": False,
"audio_outputs": False,
"video_outputs": False,
"reasoning_output": False,
"tool_calling": True,
},
"moonshotai/kimi-k2-instruct-0905": {
"max_input_tokens": 262144,
"max_output_tokens": 16384,
"image_inputs": False,
"audio_inputs": False,
"video_inputs": False,
"image_outputs": False,
"audio_outputs": False,
"video_outputs": False,
"reasoning_output": False,
"tool_calling": True,
},
"moonshotai/kimi-k2-instruct": {
"max_input_tokens": 131072,
"max_output_tokens": 16384,
"image_inputs": False,
"audio_inputs": False,
"video_inputs": False,
"image_outputs": False,
"audio_outputs": False,
"video_outputs": False,
"reasoning_output": False,
"tool_calling": True,
},
"openai/gpt-oss-20b": {
"max_input_tokens": 131072,
"max_output_tokens": 32768,
"image_inputs": False,
"audio_inputs": False,
"video_inputs": False,
"image_outputs": False,
"audio_outputs": False,
"video_outputs": False,
"reasoning_output": True,
"tool_calling": True,
},
"openai/gpt-oss-120b": {
"max_input_tokens": 131072,
"max_output_tokens": 32768,
"image_inputs": False,
"audio_inputs": False,
"video_inputs": False,
"image_outputs": False,
"audio_outputs": False,
"video_outputs": False,
"reasoning_output": True,
"tool_calling": True,
},
"qwen/qwen3-32b": {
"max_input_tokens": 131072,
"max_output_tokens": 16384,
"image_inputs": False,
"audio_inputs": False,
"video_inputs": False,
"image_outputs": False,
"audio_outputs": False,
"video_outputs": False,
"reasoning_output": True,
"tool_calling": True,
},
"meta-llama/llama-4-scout-17b-16e-instruct": {
"max_input_tokens": 131072,
"max_output_tokens": 8192,
"image_inputs": True,
"audio_inputs": False,
"video_inputs": False,
"image_outputs": False,
"audio_outputs": False,
"video_outputs": False,
"reasoning_output": False,
"tool_calling": True,
},
"meta-llama/llama-4-maverick-17b-128e-instruct": {
"max_input_tokens": 131072,
"max_output_tokens": 8192,
"image_inputs": True,
"audio_inputs": False,
"video_inputs": False,
"image_outputs": False,
"audio_outputs": False,
"video_outputs": False,
"reasoning_output": False,
"tool_calling": True,
},
"meta-llama/llama-guard-4-12b": {
"max_input_tokens": 131072,
"max_output_tokens": 128,
"image_inputs": True,
"audio_inputs": False,
"video_inputs": False,
"image_outputs": False,
"audio_outputs": False,
"video_outputs": False,
"reasoning_output": False,
"tool_calling": False,
},
}
@@ -939,3 +939,8 @@ def test_combine_llm_outputs_with_missing_details() -> None:
assert result["token_usage"]["total_tokens"] == 450
assert result["token_usage"]["output_tokens_details"]["reasoning_tokens"] == 40
assert "input_tokens_details" not in result["token_usage"]
def test_profile() -> None:
model = ChatGroq(model="openai/gpt-oss-20b")
assert model.profile
@@ -13,7 +13,11 @@ from langchain_core.callbacks.manager import (
AsyncCallbackManagerForLLMRun,
CallbackManagerForLLMRun,
)
from langchain_core.language_models import LanguageModelInput
from langchain_core.language_models import (
LanguageModelInput,
ModelProfile,
ModelProfileRegistry,
)
from langchain_core.language_models.chat_models import (
BaseChatModel,
agenerate_from_stream,
@@ -61,9 +65,17 @@ from langchain_core.utils.pydantic import is_basemodel_subclass
from pydantic import BaseModel, Field, model_validator
from typing_extensions import Self
from langchain_huggingface.data._profiles import _PROFILES
from langchain_huggingface.llms.huggingface_endpoint import HuggingFaceEndpoint
from langchain_huggingface.llms.huggingface_pipeline import HuggingFacePipeline
_MODEL_PROFILES = cast("ModelProfileRegistry", _PROFILES)
def _get_default_model_profile(model_name: str) -> ModelProfile:
default = _MODEL_PROFILES.get(model_name) or {}
return default.copy()
@dataclass
class TGI_RESPONSE:
@@ -580,6 +592,13 @@ class ChatHuggingFace(BaseChatModel):
raise TypeError(msg)
return self
@model_validator(mode="after")
def _set_model_profile(self) -> Self:
"""Set model profile if not overridden."""
if self.profile is None and self.model_id:
self.profile = _get_default_model_profile(self.model_id)
return self
def _create_chat_result(self, response: dict) -> ChatResult:
generations = []
token_usage = response.get("usage", {})
@@ -0,0 +1 @@
"""Model profile data. All edits should be made in profile_augmentations.toml."""
@@ -0,0 +1,187 @@
"""Auto-generated model profiles.
DO NOT EDIT THIS FILE MANUALLY.
This file is generated by the langchain-profiles CLI tool.
It contains data derived from the models.dev project.
Source: https://github.com/sst/models.dev
License: MIT License
To update these data, refer to the instructions here:
https://docs.langchain.com/oss/python/langchain/models#updating-or-overwriting-profile-data
"""
from typing import Any
_PROFILES: dict[str, dict[str, Any]] = {
"moonshotai/Kimi-K2-Instruct": {
"max_input_tokens": 131072,
"max_output_tokens": 16384,
"image_inputs": False,
"audio_inputs": False,
"video_inputs": False,
"image_outputs": False,
"audio_outputs": False,
"video_outputs": False,
"reasoning_output": False,
"tool_calling": True,
},
"moonshotai/Kimi-K2-Instruct-0905": {
"max_input_tokens": 262144,
"max_output_tokens": 16384,
"image_inputs": False,
"audio_inputs": False,
"video_inputs": False,
"image_outputs": False,
"audio_outputs": False,
"video_outputs": False,
"reasoning_output": False,
"tool_calling": True,
},
"MiniMaxAI/MiniMax-M2": {
"max_input_tokens": 204800,
"max_output_tokens": 204800,
"image_inputs": False,
"audio_inputs": False,
"video_inputs": False,
"image_outputs": False,
"audio_outputs": False,
"video_outputs": False,
"reasoning_output": True,
"tool_calling": True,
},
"Qwen/Qwen3-Embedding-8B": {
"max_input_tokens": 32000,
"max_output_tokens": 4096,
"image_inputs": False,
"audio_inputs": False,
"video_inputs": False,
"image_outputs": False,
"audio_outputs": False,
"video_outputs": False,
"reasoning_output": False,
"tool_calling": False,
},
"Qwen/Qwen3-Embedding-4B": {
"max_input_tokens": 32000,
"max_output_tokens": 2048,
"image_inputs": False,
"audio_inputs": False,
"video_inputs": False,
"image_outputs": False,
"audio_outputs": False,
"video_outputs": False,
"reasoning_output": False,
"tool_calling": False,
},
"Qwen/Qwen3-Coder-480B-A35B-Instruct": {
"max_input_tokens": 262144,
"max_output_tokens": 66536,
"image_inputs": False,
"audio_inputs": False,
"video_inputs": False,
"image_outputs": False,
"audio_outputs": False,
"video_outputs": False,
"reasoning_output": False,
"tool_calling": True,
},
"Qwen/Qwen3-235B-A22B-Thinking-2507": {
"max_input_tokens": 262144,
"max_output_tokens": 131072,
"image_inputs": False,
"audio_inputs": False,
"video_inputs": False,
"image_outputs": False,
"audio_outputs": False,
"video_outputs": False,
"reasoning_output": True,
"tool_calling": True,
},
"Qwen/Qwen3-Next-80B-A3B-Instruct": {
"max_input_tokens": 262144,
"max_output_tokens": 66536,
"image_inputs": False,
"audio_inputs": False,
"video_inputs": False,
"image_outputs": False,
"audio_outputs": False,
"video_outputs": False,
"reasoning_output": False,
"tool_calling": True,
},
"Qwen/Qwen3-Next-80B-A3B-Thinking": {
"max_input_tokens": 262144,
"max_output_tokens": 131072,
"image_inputs": False,
"audio_inputs": False,
"video_inputs": False,
"image_outputs": False,
"audio_outputs": False,
"video_outputs": False,
"reasoning_output": False,
"tool_calling": True,
},
"zai-org/GLM-4.5": {
"max_input_tokens": 131072,
"max_output_tokens": 98304,
"image_inputs": False,
"audio_inputs": False,
"video_inputs": False,
"image_outputs": False,
"audio_outputs": False,
"video_outputs": False,
"reasoning_output": True,
"tool_calling": True,
},
"zai-org/GLM-4.6": {
"max_input_tokens": 200000,
"max_output_tokens": 128000,
"image_inputs": False,
"audio_inputs": False,
"video_inputs": False,
"image_outputs": False,
"audio_outputs": False,
"video_outputs": False,
"reasoning_output": True,
"tool_calling": True,
},
"zai-org/GLM-4.5-Air": {
"max_input_tokens": 128000,
"max_output_tokens": 96000,
"image_inputs": False,
"audio_inputs": False,
"video_inputs": False,
"image_outputs": False,
"audio_outputs": False,
"video_outputs": False,
"reasoning_output": True,
"tool_calling": True,
},
"deepseek-ai/Deepseek-V3-0324": {
"max_input_tokens": 16384,
"max_output_tokens": 8192,
"image_inputs": False,
"audio_inputs": False,
"video_inputs": False,
"image_outputs": False,
"audio_outputs": False,
"video_outputs": False,
"reasoning_output": False,
"tool_calling": True,
},
"deepseek-ai/DeepSeek-R1-0528": {
"max_input_tokens": 163840,
"max_output_tokens": 163840,
"image_inputs": False,
"audio_inputs": False,
"video_inputs": False,
"image_outputs": False,
"audio_outputs": False,
"video_outputs": False,
"reasoning_output": True,
"tool_calling": True,
},
}
@@ -325,3 +325,15 @@ def test_inheritance_with_empty_llm() -> None:
# relevant attrs
assert chat.max_tokens is None
assert chat.temperature is None
def test_profile() -> None:
empty_llm = Mock(spec=HuggingFaceEndpoint)
empty_llm.repo_id = "test/model"
empty_llm.model = "test/model"
model = ChatHuggingFace(
model_id="moonshotai/Kimi-K2-Instruct-0905",
llm=empty_llm,
)
assert model.profile
@@ -23,7 +23,11 @@ from langchain_core.callbacks import (
AsyncCallbackManagerForLLMRun,
CallbackManagerForLLMRun,
)
from langchain_core.language_models import LanguageModelInput
from langchain_core.language_models import (
LanguageModelInput,
ModelProfile,
ModelProfileRegistry,
)
from langchain_core.language_models.chat_models import BaseChatModel, LangSmithParams
from langchain_core.language_models.llms import create_base_retry_decorator
from langchain_core.messages import (
@@ -70,6 +74,7 @@ from pydantic import (
from typing_extensions import Self
from langchain_mistralai._compat import _convert_from_v1_to_mistral
from langchain_mistralai.data._profiles import _PROFILES
if TYPE_CHECKING:
from collections.abc import AsyncIterator, Iterator, Sequence
@@ -86,6 +91,14 @@ TOOL_CALL_ID_PATTERN = re.compile(r"^[a-zA-Z0-9]{9}$")
global_ssl_context = ssl.create_default_context(cafile=certifi.where())
_MODEL_PROFILES = cast("ModelProfileRegistry", _PROFILES)
def _get_default_model_profile(model_name: str) -> ModelProfile:
default = _MODEL_PROFILES.get(model_name) or {}
return default.copy()
def _create_retry_decorator(
llm: ChatMistralAI,
run_manager: AsyncCallbackManagerForLLMRun | CallbackManagerForLLMRun | None = None,
@@ -632,6 +645,13 @@ class ChatMistralAI(BaseChatModel):
return self
@model_validator(mode="after")
def _set_model_profile(self) -> Self:
"""Set model profile if not overridden."""
if self.profile is None:
self.profile = _get_default_model_profile(self.model)
return self
def _generate(
self,
messages: list[BaseMessage],
@@ -0,0 +1 @@
"""Model profile data. All edits should be made in profile_augmentations.toml."""
@@ -0,0 +1,247 @@
"""Auto-generated model profiles.
DO NOT EDIT THIS FILE MANUALLY.
This file is generated by the langchain-profiles CLI tool.
It contains data derived from the models.dev project.
Source: https://github.com/sst/models.dev
License: MIT License
To update these data, refer to the instructions here:
https://docs.langchain.com/oss/python/langchain/models#updating-or-overwriting-profile-data
"""
from typing import Any
_PROFILES: dict[str, dict[str, Any]] = {
"devstral-medium-2507": {
"max_input_tokens": 128000,
"max_output_tokens": 128000,
"image_inputs": False,
"audio_inputs": False,
"video_inputs": False,
"image_outputs": False,
"audio_outputs": False,
"video_outputs": False,
"reasoning_output": False,
"tool_calling": True,
},
"open-mixtral-8x22b": {
"max_input_tokens": 64000,
"max_output_tokens": 64000,
"image_inputs": False,
"audio_inputs": False,
"video_inputs": False,
"image_outputs": False,
"audio_outputs": False,
"video_outputs": False,
"reasoning_output": False,
"tool_calling": True,
},
"ministral-8b-latest": {
"max_input_tokens": 128000,
"max_output_tokens": 128000,
"image_inputs": False,
"audio_inputs": False,
"video_inputs": False,
"image_outputs": False,
"audio_outputs": False,
"video_outputs": False,
"reasoning_output": False,
"tool_calling": True,
},
"pixtral-large-latest": {
"max_input_tokens": 128000,
"max_output_tokens": 128000,
"image_inputs": True,
"audio_inputs": False,
"video_inputs": False,
"image_outputs": False,
"audio_outputs": False,
"video_outputs": False,
"reasoning_output": False,
"tool_calling": True,
},
"ministral-3b-latest": {
"max_input_tokens": 128000,
"max_output_tokens": 128000,
"image_inputs": False,
"audio_inputs": False,
"video_inputs": False,
"image_outputs": False,
"audio_outputs": False,
"video_outputs": False,
"reasoning_output": False,
"tool_calling": True,
},
"pixtral-12b": {
"max_input_tokens": 128000,
"max_output_tokens": 128000,
"image_inputs": True,
"audio_inputs": False,
"video_inputs": False,
"image_outputs": False,
"audio_outputs": False,
"video_outputs": False,
"reasoning_output": False,
"tool_calling": True,
},
"mistral-medium-2505": {
"max_input_tokens": 131072,
"max_output_tokens": 131072,
"image_inputs": True,
"audio_inputs": False,
"video_inputs": False,
"image_outputs": False,
"audio_outputs": False,
"video_outputs": False,
"reasoning_output": False,
"tool_calling": True,
},
"devstral-small-2505": {
"max_input_tokens": 128000,
"max_output_tokens": 128000,
"image_inputs": False,
"audio_inputs": False,
"video_inputs": False,
"image_outputs": False,
"audio_outputs": False,
"video_outputs": False,
"reasoning_output": False,
"tool_calling": True,
},
"mistral-medium-2508": {
"max_input_tokens": 262144,
"max_output_tokens": 262144,
"image_inputs": True,
"audio_inputs": False,
"video_inputs": False,
"image_outputs": False,
"audio_outputs": False,
"video_outputs": False,
"reasoning_output": False,
"tool_calling": True,
},
"mistral-small-latest": {
"max_input_tokens": 128000,
"max_output_tokens": 16384,
"image_inputs": True,
"audio_inputs": False,
"video_inputs": False,
"image_outputs": False,
"audio_outputs": False,
"video_outputs": False,
"reasoning_output": False,
"tool_calling": True,
},
"magistral-small": {
"max_input_tokens": 128000,
"max_output_tokens": 128000,
"image_inputs": False,
"audio_inputs": False,
"video_inputs": False,
"image_outputs": False,
"audio_outputs": False,
"video_outputs": False,
"reasoning_output": True,
"tool_calling": True,
},
"devstral-small-2507": {
"max_input_tokens": 128000,
"max_output_tokens": 128000,
"image_inputs": False,
"audio_inputs": False,
"video_inputs": False,
"image_outputs": False,
"audio_outputs": False,
"video_outputs": False,
"reasoning_output": False,
"tool_calling": True,
},
"codestral-latest": {
"max_input_tokens": 256000,
"max_output_tokens": 4096,
"image_inputs": False,
"audio_inputs": False,
"video_inputs": False,
"image_outputs": False,
"audio_outputs": False,
"video_outputs": False,
"reasoning_output": False,
"tool_calling": True,
},
"open-mixtral-8x7b": {
"max_input_tokens": 32000,
"max_output_tokens": 32000,
"image_inputs": False,
"audio_inputs": False,
"video_inputs": False,
"image_outputs": False,
"audio_outputs": False,
"video_outputs": False,
"reasoning_output": False,
"tool_calling": True,
},
"mistral-nemo": {
"max_input_tokens": 128000,
"max_output_tokens": 128000,
"image_inputs": False,
"audio_inputs": False,
"video_inputs": False,
"image_outputs": False,
"audio_outputs": False,
"video_outputs": False,
"reasoning_output": False,
"tool_calling": True,
},
"open-mistral-7b": {
"max_input_tokens": 8000,
"max_output_tokens": 8000,
"image_inputs": False,
"audio_inputs": False,
"video_inputs": False,
"image_outputs": False,
"audio_outputs": False,
"video_outputs": False,
"reasoning_output": False,
"tool_calling": True,
},
"mistral-large-latest": {
"max_input_tokens": 131072,
"max_output_tokens": 16384,
"image_inputs": False,
"audio_inputs": False,
"video_inputs": False,
"image_outputs": False,
"audio_outputs": False,
"video_outputs": False,
"reasoning_output": False,
"tool_calling": True,
},
"mistral-medium-latest": {
"max_input_tokens": 128000,
"max_output_tokens": 16384,
"image_inputs": True,
"audio_inputs": False,
"video_inputs": False,
"image_outputs": False,
"audio_outputs": False,
"video_outputs": False,
"reasoning_output": False,
"tool_calling": True,
},
"magistral-medium-latest": {
"max_input_tokens": 128000,
"max_output_tokens": 16384,
"image_inputs": False,
"audio_inputs": False,
"video_inputs": False,
"image_outputs": False,
"audio_outputs": False,
"video_outputs": False,
"reasoning_output": True,
"tool_calling": True,
},
}
@@ -350,3 +350,8 @@ def test_no_duplicate_tool_calls_when_multiple_tools() -> None:
ids = [tc.get("id") for tc in tool_calls if isinstance(tc, dict)]
assert len(ids) == 2
assert len(set(ids)) == 2, f"Duplicate tool call IDs found: {ids}"
def test_profile() -> None:
model = ChatMistralAI(model="mistral-large-latest") # type: ignore[call-arg]
assert model.profile
@@ -17,7 +17,7 @@ from langchain_core.utils.pydantic import is_basemodel_subclass
from pydantic import BaseModel, Field, SecretStr, model_validator
from typing_extensions import Self
from langchain_openai.chat_models.base import BaseChatOpenAI
from langchain_openai.chat_models.base import BaseChatOpenAI, _get_default_model_profile
logger = logging.getLogger(__name__)
@@ -701,6 +701,13 @@ class AzureChatOpenAI(BaseChatOpenAI):
self.async_client = self.root_async_client.chat.completions
return self
@model_validator(mode="after")
def _set_model_profile(self) -> Self:
"""Set model profile if not overridden."""
if self.profile is None and self.deployment_name is not None:
self.profile = _get_default_model_profile(self.deployment_name)
return self
@property
def _identifying_params(self) -> dict[str, Any]:
"""Get the identifying parameters."""
@@ -40,7 +40,10 @@ from langchain_core.callbacks import (
AsyncCallbackManagerForLLMRun,
CallbackManagerForLLMRun,
)
from langchain_core.language_models import LanguageModelInput
from langchain_core.language_models import (
LanguageModelInput,
ModelProfileRegistry,
)
from langchain_core.language_models.chat_models import (
BaseChatModel,
LangSmithParams,
@@ -123,8 +126,10 @@ from langchain_openai.chat_models._compat import (
_convert_from_v1_to_responses,
_convert_to_v03_ai_message,
)
from langchain_openai.data._profiles import _PROFILES
if TYPE_CHECKING:
from langchain_core.language_models import ModelProfile
from openai.types.responses import Response
logger = logging.getLogger(__name__)
@@ -133,6 +138,14 @@ logger = logging.getLogger(__name__)
# https://www.python-httpx.org/advanced/ssl/#configuring-client-instances
global_ssl_context = ssl.create_default_context(cafile=certifi.where())
_MODEL_PROFILES = cast(ModelProfileRegistry, _PROFILES)
def _get_default_model_profile(model_name: str) -> ModelProfile:
default = _MODEL_PROFILES.get(model_name) or {}
return default.copy()
WellKnownTools = (
"file_search",
"web_search_preview",
@@ -952,6 +965,13 @@ class BaseChatOpenAI(BaseChatModel):
self.async_client = self.root_async_client.chat.completions
return self
@model_validator(mode="after")
def _set_model_profile(self) -> Self:
"""Set model profile if not overridden."""
if self.profile is None:
self.profile = _get_default_model_profile(self.model_name)
return self
@property
def _default_params(self) -> dict[str, Any]:
"""Get the default parameters for calling OpenAI API."""
@@ -0,0 +1 @@
"""Model profile data. All edits should be made in profile_augmentations.toml."""
@@ -0,0 +1,641 @@
"""Auto-generated model profiles.
DO NOT EDIT THIS FILE MANUALLY.
This file is generated by the langchain-profiles CLI tool.
It contains data derived from the models.dev project.
Source: https://github.com/sst/models.dev
License: MIT License
To update these data, refer to the instructions here:
https://docs.langchain.com/oss/python/langchain/models#updating-or-overwriting-profile-data
"""
from typing import Any
_PROFILES: dict[str, dict[str, Any]] = {
"gpt-4.1-nano": {
"max_input_tokens": 1047576,
"max_output_tokens": 32768,
"image_inputs": True,
"audio_inputs": False,
"video_inputs": False,
"image_outputs": False,
"audio_outputs": False,
"video_outputs": False,
"reasoning_output": False,
"tool_calling": True,
"structured_output": True,
"image_url_inputs": True,
"pdf_inputs": True,
"pdf_tool_message": True,
"image_tool_message": True,
"tool_choice": True,
},
"text-embedding-3-small": {
"max_input_tokens": 8191,
"max_output_tokens": 1536,
"image_inputs": False,
"audio_inputs": False,
"video_inputs": False,
"image_outputs": False,
"audio_outputs": False,
"video_outputs": False,
"reasoning_output": False,
"tool_calling": False,
"image_url_inputs": True,
"pdf_inputs": True,
"pdf_tool_message": True,
"image_tool_message": True,
"tool_choice": True,
},
"gpt-4": {
"max_input_tokens": 8192,
"max_output_tokens": 8192,
"image_inputs": False,
"audio_inputs": False,
"video_inputs": False,
"image_outputs": False,
"audio_outputs": False,
"video_outputs": False,
"reasoning_output": False,
"tool_calling": True,
"structured_output": False,
"image_url_inputs": True,
"pdf_inputs": True,
"pdf_tool_message": True,
"image_tool_message": True,
"tool_choice": True,
},
"o1-pro": {
"max_input_tokens": 200000,
"max_output_tokens": 100000,
"image_inputs": True,
"audio_inputs": False,
"video_inputs": False,
"image_outputs": False,
"audio_outputs": False,
"video_outputs": False,
"reasoning_output": True,
"tool_calling": True,
"structured_output": True,
"image_url_inputs": True,
"pdf_inputs": True,
"pdf_tool_message": True,
"image_tool_message": True,
"tool_choice": True,
},
"gpt-4o-2024-05-13": {
"max_input_tokens": 128000,
"max_output_tokens": 4096,
"image_inputs": True,
"audio_inputs": False,
"video_inputs": False,
"image_outputs": False,
"audio_outputs": False,
"video_outputs": False,
"reasoning_output": False,
"tool_calling": True,
"structured_output": True,
"image_url_inputs": True,
"pdf_inputs": True,
"pdf_tool_message": True,
"image_tool_message": True,
"tool_choice": True,
},
"gpt-5.1-codex": {
"max_input_tokens": 400000,
"max_output_tokens": 128000,
"image_inputs": True,
"audio_inputs": False,
"video_inputs": False,
"image_outputs": True,
"audio_outputs": False,
"video_outputs": False,
"reasoning_output": True,
"tool_calling": True,
"structured_output": True,
"image_url_inputs": True,
"pdf_inputs": True,
"pdf_tool_message": True,
"image_tool_message": True,
"tool_choice": True,
},
"gpt-4o-2024-08-06": {
"max_input_tokens": 128000,
"max_output_tokens": 16384,
"image_inputs": True,
"audio_inputs": False,
"video_inputs": False,
"image_outputs": False,
"audio_outputs": False,
"video_outputs": False,
"reasoning_output": False,
"tool_calling": True,
"structured_output": True,
"image_url_inputs": True,
"pdf_inputs": True,
"pdf_tool_message": True,
"image_tool_message": True,
"tool_choice": True,
},
"gpt-4.1-mini": {
"max_input_tokens": 1047576,
"max_output_tokens": 32768,
"image_inputs": True,
"audio_inputs": False,
"video_inputs": False,
"image_outputs": False,
"audio_outputs": False,
"video_outputs": False,
"reasoning_output": False,
"tool_calling": True,
"structured_output": True,
"image_url_inputs": True,
"pdf_inputs": True,
"pdf_tool_message": True,
"image_tool_message": True,
"tool_choice": True,
},
"o3-deep-research": {
"max_input_tokens": 200000,
"max_output_tokens": 100000,
"image_inputs": True,
"audio_inputs": False,
"video_inputs": False,
"image_outputs": False,
"audio_outputs": False,
"video_outputs": False,
"reasoning_output": True,
"tool_calling": True,
"image_url_inputs": True,
"pdf_inputs": True,
"pdf_tool_message": True,
"image_tool_message": True,
"tool_choice": True,
},
"gpt-3.5-turbo": {
"max_input_tokens": 16385,
"max_output_tokens": 4096,
"image_inputs": False,
"audio_inputs": False,
"video_inputs": False,
"image_outputs": False,
"audio_outputs": False,
"video_outputs": False,
"reasoning_output": False,
"tool_calling": False,
"structured_output": False,
"image_url_inputs": False,
"pdf_inputs": False,
"pdf_tool_message": False,
"image_tool_message": False,
"tool_choice": True,
},
"text-embedding-3-large": {
"max_input_tokens": 8191,
"max_output_tokens": 3072,
"image_inputs": False,
"audio_inputs": False,
"video_inputs": False,
"image_outputs": False,
"audio_outputs": False,
"video_outputs": False,
"reasoning_output": False,
"tool_calling": False,
"image_url_inputs": True,
"pdf_inputs": True,
"pdf_tool_message": True,
"image_tool_message": True,
"tool_choice": True,
},
"gpt-4-turbo": {
"max_input_tokens": 128000,
"max_output_tokens": 4096,
"image_inputs": True,
"audio_inputs": False,
"video_inputs": False,
"image_outputs": False,
"audio_outputs": False,
"video_outputs": False,
"reasoning_output": False,
"tool_calling": True,
"structured_output": False,
"image_url_inputs": True,
"pdf_inputs": True,
"pdf_tool_message": True,
"image_tool_message": True,
"tool_choice": True,
},
"o1-preview": {
"max_input_tokens": 128000,
"max_output_tokens": 32768,
"image_inputs": False,
"audio_inputs": False,
"video_inputs": False,
"image_outputs": False,
"audio_outputs": False,
"video_outputs": False,
"reasoning_output": True,
"tool_calling": False,
"image_url_inputs": True,
"pdf_inputs": True,
"pdf_tool_message": True,
"image_tool_message": True,
"tool_choice": True,
},
"gpt-5.1-codex-mini": {
"max_input_tokens": 400000,
"max_output_tokens": 128000,
"image_inputs": True,
"audio_inputs": False,
"video_inputs": False,
"image_outputs": True,
"audio_outputs": False,
"video_outputs": False,
"reasoning_output": True,
"tool_calling": True,
"structured_output": True,
"image_url_inputs": True,
"pdf_inputs": True,
"pdf_tool_message": True,
"image_tool_message": True,
"tool_choice": True,
},
"o3-mini": {
"max_input_tokens": 200000,
"max_output_tokens": 100000,
"image_inputs": False,
"audio_inputs": False,
"video_inputs": False,
"image_outputs": False,
"audio_outputs": False,
"video_outputs": False,
"reasoning_output": True,
"tool_calling": True,
"structured_output": True,
"image_url_inputs": True,
"pdf_inputs": True,
"pdf_tool_message": True,
"image_tool_message": True,
"tool_choice": True,
},
"gpt-5.1": {
"max_input_tokens": 400000,
"max_output_tokens": 128000,
"image_inputs": True,
"audio_inputs": False,
"video_inputs": False,
"image_outputs": False,
"audio_outputs": False,
"video_outputs": False,
"reasoning_output": True,
"tool_calling": True,
"image_url_inputs": True,
"pdf_inputs": True,
"pdf_tool_message": True,
"image_tool_message": True,
"tool_choice": True,
},
"codex-mini-latest": {
"max_input_tokens": 200000,
"max_output_tokens": 100000,
"image_inputs": False,
"audio_inputs": False,
"video_inputs": False,
"image_outputs": False,
"audio_outputs": False,
"video_outputs": False,
"reasoning_output": True,
"tool_calling": True,
"image_url_inputs": True,
"pdf_inputs": True,
"pdf_tool_message": True,
"image_tool_message": True,
"tool_choice": True,
},
"gpt-5-nano": {
"max_input_tokens": 400000,
"max_output_tokens": 128000,
"image_inputs": True,
"audio_inputs": False,
"video_inputs": False,
"image_outputs": False,
"audio_outputs": False,
"video_outputs": False,
"reasoning_output": True,
"tool_calling": True,
"structured_output": True,
"image_url_inputs": True,
"pdf_inputs": True,
"pdf_tool_message": True,
"image_tool_message": True,
"tool_choice": True,
},
"gpt-5-codex": {
"max_input_tokens": 400000,
"max_output_tokens": 128000,
"image_inputs": True,
"audio_inputs": False,
"video_inputs": False,
"image_outputs": False,
"audio_outputs": False,
"video_outputs": False,
"reasoning_output": True,
"tool_calling": True,
"structured_output": True,
"image_url_inputs": True,
"pdf_inputs": True,
"pdf_tool_message": True,
"image_tool_message": True,
"tool_choice": True,
},
"gpt-4o": {
"max_input_tokens": 128000,
"max_output_tokens": 16384,
"image_inputs": True,
"audio_inputs": False,
"video_inputs": False,
"image_outputs": False,
"audio_outputs": False,
"video_outputs": False,
"reasoning_output": False,
"tool_calling": True,
"structured_output": True,
"image_url_inputs": True,
"pdf_inputs": True,
"pdf_tool_message": True,
"image_tool_message": True,
"tool_choice": True,
},
"gpt-4.1": {
"max_input_tokens": 1047576,
"max_output_tokens": 32768,
"image_inputs": True,
"audio_inputs": False,
"video_inputs": False,
"image_outputs": False,
"audio_outputs": False,
"video_outputs": False,
"reasoning_output": False,
"tool_calling": True,
"structured_output": True,
"image_url_inputs": True,
"pdf_inputs": True,
"pdf_tool_message": True,
"image_tool_message": True,
"tool_choice": True,
},
"o4-mini": {
"max_input_tokens": 200000,
"max_output_tokens": 100000,
"image_inputs": True,
"audio_inputs": False,
"video_inputs": False,
"image_outputs": False,
"audio_outputs": False,
"video_outputs": False,
"reasoning_output": True,
"tool_calling": True,
"structured_output": True,
"image_url_inputs": True,
"pdf_inputs": True,
"pdf_tool_message": True,
"image_tool_message": True,
"tool_choice": True,
},
"o1": {
"max_input_tokens": 200000,
"max_output_tokens": 100000,
"image_inputs": True,
"audio_inputs": False,
"video_inputs": False,
"image_outputs": False,
"audio_outputs": False,
"video_outputs": False,
"reasoning_output": True,
"tool_calling": True,
"structured_output": True,
"image_url_inputs": True,
"pdf_inputs": True,
"pdf_tool_message": True,
"image_tool_message": True,
"tool_choice": True,
},
"gpt-5-mini": {
"max_input_tokens": 400000,
"max_output_tokens": 128000,
"image_inputs": True,
"audio_inputs": False,
"video_inputs": False,
"image_outputs": False,
"audio_outputs": False,
"video_outputs": False,
"reasoning_output": True,
"tool_calling": True,
"structured_output": True,
"image_url_inputs": True,
"pdf_inputs": True,
"pdf_tool_message": True,
"image_tool_message": True,
"tool_choice": True,
},
"o1-mini": {
"max_input_tokens": 128000,
"max_output_tokens": 65536,
"image_inputs": False,
"audio_inputs": False,
"video_inputs": False,
"image_outputs": False,
"audio_outputs": False,
"video_outputs": False,
"reasoning_output": True,
"tool_calling": False,
"structured_output": True,
"image_url_inputs": True,
"pdf_inputs": True,
"pdf_tool_message": True,
"image_tool_message": True,
"tool_choice": True,
},
"text-embedding-ada-002": {
"max_input_tokens": 8192,
"max_output_tokens": 1536,
"image_inputs": False,
"audio_inputs": False,
"video_inputs": False,
"image_outputs": False,
"audio_outputs": False,
"video_outputs": False,
"reasoning_output": False,
"tool_calling": False,
"image_url_inputs": True,
"pdf_inputs": True,
"pdf_tool_message": True,
"image_tool_message": True,
"tool_choice": True,
},
"o3-pro": {
"max_input_tokens": 200000,
"max_output_tokens": 100000,
"image_inputs": True,
"audio_inputs": False,
"video_inputs": False,
"image_outputs": False,
"audio_outputs": False,
"video_outputs": False,
"reasoning_output": True,
"tool_calling": True,
"structured_output": True,
"image_url_inputs": True,
"pdf_inputs": True,
"pdf_tool_message": True,
"image_tool_message": True,
"tool_choice": True,
},
"gpt-4o-2024-11-20": {
"max_input_tokens": 128000,
"max_output_tokens": 16384,
"image_inputs": True,
"audio_inputs": False,
"video_inputs": False,
"image_outputs": False,
"audio_outputs": False,
"video_outputs": False,
"reasoning_output": False,
"tool_calling": True,
"structured_output": True,
"image_url_inputs": True,
"pdf_inputs": True,
"pdf_tool_message": True,
"image_tool_message": True,
"tool_choice": True,
},
"o3": {
"max_input_tokens": 200000,
"max_output_tokens": 100000,
"image_inputs": True,
"audio_inputs": False,
"video_inputs": False,
"image_outputs": False,
"audio_outputs": False,
"video_outputs": False,
"reasoning_output": True,
"tool_calling": True,
"structured_output": True,
"image_url_inputs": True,
"pdf_inputs": True,
"pdf_tool_message": True,
"image_tool_message": True,
"tool_choice": True,
},
"o4-mini-deep-research": {
"max_input_tokens": 200000,
"max_output_tokens": 100000,
"image_inputs": True,
"audio_inputs": False,
"video_inputs": False,
"image_outputs": False,
"audio_outputs": False,
"video_outputs": False,
"reasoning_output": True,
"tool_calling": True,
"image_url_inputs": True,
"pdf_inputs": True,
"pdf_tool_message": True,
"image_tool_message": True,
"tool_choice": True,
},
"gpt-5-chat-latest": {
"max_input_tokens": 400000,
"max_output_tokens": 128000,
"image_inputs": True,
"audio_inputs": False,
"video_inputs": False,
"image_outputs": False,
"audio_outputs": False,
"video_outputs": False,
"reasoning_output": True,
"tool_calling": False,
"structured_output": True,
"image_url_inputs": True,
"pdf_inputs": True,
"pdf_tool_message": True,
"image_tool_message": True,
"tool_choice": True,
},
"gpt-4o-mini": {
"max_input_tokens": 128000,
"max_output_tokens": 16384,
"image_inputs": True,
"audio_inputs": False,
"video_inputs": False,
"image_outputs": False,
"audio_outputs": False,
"video_outputs": False,
"reasoning_output": False,
"tool_calling": True,
"structured_output": True,
"image_url_inputs": True,
"pdf_inputs": True,
"pdf_tool_message": True,
"image_tool_message": True,
"tool_choice": True,
},
"gpt-5": {
"max_input_tokens": 400000,
"max_output_tokens": 128000,
"image_inputs": True,
"audio_inputs": False,
"video_inputs": False,
"image_outputs": False,
"audio_outputs": False,
"video_outputs": False,
"reasoning_output": True,
"tool_calling": True,
"structured_output": True,
"image_url_inputs": True,
"pdf_inputs": True,
"pdf_tool_message": True,
"image_tool_message": True,
"tool_choice": True,
},
"gpt-5-pro": {
"max_input_tokens": 400000,
"max_output_tokens": 272000,
"image_inputs": True,
"audio_inputs": False,
"video_inputs": False,
"image_outputs": False,
"audio_outputs": False,
"video_outputs": False,
"reasoning_output": True,
"tool_calling": True,
"structured_output": True,
"image_url_inputs": True,
"pdf_inputs": True,
"pdf_tool_message": True,
"image_tool_message": True,
"tool_choice": True,
},
"gpt-5.1-chat-latest": {
"max_input_tokens": 128000,
"max_output_tokens": 16384,
"image_inputs": True,
"audio_inputs": False,
"video_inputs": False,
"image_outputs": False,
"audio_outputs": False,
"video_outputs": False,
"reasoning_output": True,
"tool_calling": True,
"structured_output": True,
"image_url_inputs": True,
"pdf_inputs": True,
"pdf_tool_message": True,
"image_tool_message": True,
"tool_choice": True,
},
}
@@ -0,0 +1,14 @@
provider = "openai"
[overrides]
image_url_inputs = true
pdf_inputs = true
pdf_tool_message = true
image_tool_message = true
tool_choice = true
[overrides."gpt-3.5-turbo"]
image_url_inputs = false
pdf_inputs = false
pdf_tool_message = false
image_tool_message = false
@@ -120,6 +120,30 @@ def test_openai_client_caching() -> None:
assert llm1.root_client._client is not llm7.root_client._client
def test_profile() -> None:
model = ChatOpenAI(model="gpt-4")
assert model.profile
assert not model.profile["structured_output"]
model = ChatOpenAI(model="gpt-5")
assert model.profile
assert model.profile["structured_output"]
assert model.profile["tool_calling"]
# Test overwriting a field
model.profile["tool_calling"] = False
assert not model.profile["tool_calling"]
# Test we didn't mutate
model = ChatOpenAI(model="gpt-5")
assert model.profile
assert model.profile["tool_calling"]
# Test passing in profile
model = ChatOpenAI(model="gpt-5", profile={"tool_calling": False})
assert model.profile == {"tool_calling": False}
def test_openai_o1_temperature() -> None:
llm = ChatOpenAI(model="o1-preview")
assert llm.temperature == 1
@@ -5,11 +5,15 @@ from __future__ import annotations
import logging
from collections.abc import Iterator, Mapping
from operator import itemgetter
from typing import Any, Literal, TypeAlias
from typing import Any, Literal, TypeAlias, cast
import openai
from langchain_core.callbacks import CallbackManagerForLLMRun
from langchain_core.language_models import LanguageModelInput
from langchain_core.language_models import (
LanguageModelInput,
ModelProfile,
ModelProfileRegistry,
)
from langchain_core.language_models.chat_models import (
BaseChatModel,
generate_from_stream,
@@ -41,6 +45,7 @@ from langchain_core.utils.pydantic import is_basemodel_subclass
from pydantic import BaseModel, ConfigDict, Field, SecretStr, model_validator
from typing_extensions import Self
from langchain_perplexity.data._profiles import _PROFILES
from langchain_perplexity.output_parsers import (
ReasoningJsonOutputParser,
ReasoningStructuredOutputParser,
@@ -52,6 +57,14 @@ _DictOrPydantic: TypeAlias = dict | BaseModel
logger = logging.getLogger(__name__)
_MODEL_PROFILES = cast("ModelProfileRegistry", _PROFILES)
def _get_default_model_profile(model_name: str) -> ModelProfile:
default = _MODEL_PROFILES.get(model_name) or {}
return default.copy()
def _is_pydantic_class(obj: Any) -> bool:
return isinstance(obj, type) and is_basemodel_subclass(obj)
@@ -249,6 +262,13 @@ class ChatPerplexity(BaseChatModel):
)
return self
@model_validator(mode="after")
def _set_model_profile(self) -> Self:
"""Set model profile if not overridden."""
if self.profile is None:
self.profile = _get_default_model_profile(self.model)
return self
@property
def _default_params(self) -> dict[str, Any]:
"""Get the default parameters for calling PerplexityChat API."""
@@ -0,0 +1 @@
"""Model profile data. All edits should be made in profile_augmentations.toml."""
@@ -0,0 +1,67 @@
"""Auto-generated model profiles.
DO NOT EDIT THIS FILE MANUALLY.
This file is generated by the langchain-profiles CLI tool.
It contains data derived from the models.dev project.
Source: https://github.com/sst/models.dev
License: MIT License
To update these data, refer to the instructions here:
https://docs.langchain.com/oss/python/langchain/models#updating-or-overwriting-profile-data
"""
from typing import Any
_PROFILES: dict[str, dict[str, Any]] = {
"sonar-reasoning": {
"max_input_tokens": 128000,
"max_output_tokens": 4096,
"image_inputs": False,
"audio_inputs": False,
"video_inputs": False,
"image_outputs": False,
"audio_outputs": False,
"video_outputs": False,
"reasoning_output": True,
"tool_calling": False,
},
"sonar": {
"max_input_tokens": 128000,
"max_output_tokens": 4096,
"image_inputs": False,
"audio_inputs": False,
"video_inputs": False,
"image_outputs": False,
"audio_outputs": False,
"video_outputs": False,
"reasoning_output": False,
"tool_calling": False,
},
"sonar-pro": {
"max_input_tokens": 200000,
"max_output_tokens": 8192,
"image_inputs": True,
"audio_inputs": False,
"video_inputs": False,
"image_outputs": False,
"audio_outputs": False,
"video_outputs": False,
"reasoning_output": False,
"tool_calling": False,
},
"sonar-reasoning-pro": {
"max_input_tokens": 128000,
"max_output_tokens": 4096,
"image_inputs": True,
"audio_inputs": False,
"video_inputs": False,
"image_outputs": False,
"audio_outputs": False,
"video_outputs": False,
"reasoning_output": True,
"tool_calling": False,
},
}
@@ -523,3 +523,8 @@ def test_perplexity_stream_includes_num_search_queries(mocker: MockerFixture) ->
assert usage_chunk.usage_metadata["output_token_details"]["citation_tokens"] == 3 # type: ignore[typeddict-item]
patcher.assert_called_once()
def test_profile() -> None:
model = ChatPerplexity(model="sonar")
assert model.profile
+22 -1
View File
@@ -2,7 +2,7 @@
from __future__ import annotations
from typing import TYPE_CHECKING, Any, Literal, TypeAlias
from typing import TYPE_CHECKING, Any, Literal, TypeAlias, cast
import openai
from langchain_core.messages import AIMessageChunk
@@ -11,7 +11,13 @@ from langchain_openai.chat_models.base import BaseChatOpenAI
from pydantic import BaseModel, ConfigDict, Field, SecretStr, model_validator
from typing_extensions import Self
from langchain_xai.data._profiles import _PROFILES
if TYPE_CHECKING:
from langchain_core.language_models import (
ModelProfile,
ModelProfileRegistry,
)
from langchain_core.language_models.chat_models import (
LangSmithParams,
LanguageModelInput,
@@ -23,6 +29,14 @@ _DictOrPydanticClass: TypeAlias = dict[str, Any] | type[BaseModel] | type
_DictOrPydantic: TypeAlias = dict | BaseModel
_MODEL_PROFILES = cast("ModelProfileRegistry", _PROFILES)
def _get_default_model_profile(model_name: str) -> ModelProfile:
default = _MODEL_PROFILES.get(model_name) or {}
return default.copy()
class ChatXAI(BaseChatOpenAI): # type: ignore[override]
r"""ChatXAI chat model.
@@ -514,6 +528,13 @@ class ChatXAI(BaseChatOpenAI): # type: ignore[override]
)
return self
@model_validator(mode="after")
def _set_model_profile(self) -> Self:
"""Set model profile if not overridden."""
if self.profile is None:
self.profile = _get_default_model_profile(self.model_name)
return self
@property
def _default_params(self) -> dict[str, Any]:
"""Get default parameters."""
@@ -0,0 +1 @@
"""Model profile data. All edits should be made in profile_augmentations.toml."""
@@ -0,0 +1,283 @@
"""Auto-generated model profiles.
DO NOT EDIT THIS FILE MANUALLY.
This file is generated by the langchain-profiles CLI tool.
It contains data derived from the models.dev project.
Source: https://github.com/sst/models.dev
License: MIT License
To update these data, refer to the instructions here:
https://docs.langchain.com/oss/python/langchain/models#updating-or-overwriting-profile-data
"""
from typing import Any
_PROFILES: dict[str, dict[str, Any]] = {
"grok-4-fast-non-reasoning": {
"max_input_tokens": 2000000,
"max_output_tokens": 30000,
"image_inputs": True,
"audio_inputs": False,
"video_inputs": False,
"image_outputs": False,
"audio_outputs": False,
"video_outputs": False,
"reasoning_output": False,
"tool_calling": True,
},
"grok-3-fast": {
"max_input_tokens": 131072,
"max_output_tokens": 8192,
"image_inputs": False,
"audio_inputs": False,
"video_inputs": False,
"image_outputs": False,
"audio_outputs": False,
"video_outputs": False,
"reasoning_output": False,
"tool_calling": True,
},
"grok-4": {
"max_input_tokens": 256000,
"max_output_tokens": 64000,
"image_inputs": False,
"audio_inputs": False,
"video_inputs": False,
"image_outputs": False,
"audio_outputs": False,
"video_outputs": False,
"reasoning_output": True,
"tool_calling": True,
},
"grok-2-vision": {
"max_input_tokens": 8192,
"max_output_tokens": 4096,
"image_inputs": True,
"audio_inputs": False,
"video_inputs": False,
"image_outputs": False,
"audio_outputs": False,
"video_outputs": False,
"reasoning_output": False,
"tool_calling": True,
},
"grok-code-fast-1": {
"max_input_tokens": 256000,
"max_output_tokens": 10000,
"image_inputs": False,
"audio_inputs": False,
"video_inputs": False,
"image_outputs": False,
"audio_outputs": False,
"video_outputs": False,
"reasoning_output": True,
"tool_calling": True,
},
"grok-2": {
"max_input_tokens": 131072,
"max_output_tokens": 8192,
"image_inputs": False,
"audio_inputs": False,
"video_inputs": False,
"image_outputs": False,
"audio_outputs": False,
"video_outputs": False,
"reasoning_output": False,
"tool_calling": True,
},
"grok-3-mini-fast-latest": {
"max_input_tokens": 131072,
"max_output_tokens": 8192,
"image_inputs": False,
"audio_inputs": False,
"video_inputs": False,
"image_outputs": False,
"audio_outputs": False,
"video_outputs": False,
"reasoning_output": True,
"tool_calling": True,
},
"grok-2-vision-1212": {
"max_input_tokens": 8192,
"max_output_tokens": 4096,
"image_inputs": True,
"audio_inputs": False,
"video_inputs": False,
"image_outputs": False,
"audio_outputs": False,
"video_outputs": False,
"reasoning_output": False,
"tool_calling": True,
},
"grok-3": {
"max_input_tokens": 131072,
"max_output_tokens": 8192,
"image_inputs": False,
"audio_inputs": False,
"video_inputs": False,
"image_outputs": False,
"audio_outputs": False,
"video_outputs": False,
"reasoning_output": False,
"tool_calling": True,
},
"grok-4-fast": {
"max_input_tokens": 2000000,
"max_output_tokens": 30000,
"image_inputs": True,
"audio_inputs": False,
"video_inputs": False,
"image_outputs": False,
"audio_outputs": False,
"video_outputs": False,
"reasoning_output": True,
"tool_calling": True,
},
"grok-2-latest": {
"max_input_tokens": 131072,
"max_output_tokens": 8192,
"image_inputs": False,
"audio_inputs": False,
"video_inputs": False,
"image_outputs": False,
"audio_outputs": False,
"video_outputs": False,
"reasoning_output": False,
"tool_calling": True,
},
"grok-4-1-fast": {
"max_input_tokens": 2000000,
"max_output_tokens": 30000,
"image_inputs": True,
"audio_inputs": False,
"video_inputs": False,
"image_outputs": False,
"audio_outputs": False,
"video_outputs": False,
"reasoning_output": True,
"tool_calling": True,
},
"grok-2-1212": {
"max_input_tokens": 131072,
"max_output_tokens": 8192,
"image_inputs": False,
"audio_inputs": False,
"video_inputs": False,
"image_outputs": False,
"audio_outputs": False,
"video_outputs": False,
"reasoning_output": False,
"tool_calling": True,
},
"grok-3-fast-latest": {
"max_input_tokens": 131072,
"max_output_tokens": 8192,
"image_inputs": False,
"audio_inputs": False,
"video_inputs": False,
"image_outputs": False,
"audio_outputs": False,
"video_outputs": False,
"reasoning_output": False,
"tool_calling": True,
},
"grok-3-latest": {
"max_input_tokens": 131072,
"max_output_tokens": 8192,
"image_inputs": False,
"audio_inputs": False,
"video_inputs": False,
"image_outputs": False,
"audio_outputs": False,
"video_outputs": False,
"reasoning_output": False,
"tool_calling": True,
},
"grok-2-vision-latest": {
"max_input_tokens": 8192,
"max_output_tokens": 4096,
"image_inputs": True,
"audio_inputs": False,
"video_inputs": False,
"image_outputs": False,
"audio_outputs": False,
"video_outputs": False,
"reasoning_output": False,
"tool_calling": True,
},
"grok-vision-beta": {
"max_input_tokens": 8192,
"max_output_tokens": 4096,
"image_inputs": True,
"audio_inputs": False,
"video_inputs": False,
"image_outputs": False,
"audio_outputs": False,
"video_outputs": False,
"reasoning_output": False,
"tool_calling": True,
},
"grok-3-mini": {
"max_input_tokens": 131072,
"max_output_tokens": 8192,
"image_inputs": False,
"audio_inputs": False,
"video_inputs": False,
"image_outputs": False,
"audio_outputs": False,
"video_outputs": False,
"reasoning_output": True,
"tool_calling": True,
},
"grok-beta": {
"max_input_tokens": 131072,
"max_output_tokens": 4096,
"image_inputs": False,
"audio_inputs": False,
"video_inputs": False,
"image_outputs": False,
"audio_outputs": False,
"video_outputs": False,
"reasoning_output": False,
"tool_calling": True,
},
"grok-3-mini-latest": {
"max_input_tokens": 131072,
"max_output_tokens": 8192,
"image_inputs": False,
"audio_inputs": False,
"video_inputs": False,
"image_outputs": False,
"audio_outputs": False,
"video_outputs": False,
"reasoning_output": True,
"tool_calling": True,
},
"grok-4-1-fast-non-reasoning": {
"max_input_tokens": 2000000,
"max_output_tokens": 30000,
"image_inputs": True,
"audio_inputs": False,
"video_inputs": False,
"image_outputs": False,
"audio_outputs": False,
"video_outputs": False,
"reasoning_output": False,
"tool_calling": True,
},
"grok-3-mini-fast": {
"max_input_tokens": 131072,
"max_output_tokens": 8192,
"image_inputs": False,
"audio_inputs": False,
"video_inputs": False,
"image_outputs": False,
"audio_outputs": False,
"video_outputs": False,
"reasoning_output": True,
"tool_calling": True,
},
}
@@ -23,6 +23,11 @@ def test_initialization() -> None:
ChatXAI(model=MODEL_NAME)
def test_profile() -> None:
model = ChatXAI(model="grok-4")
assert model.profile
def test_xai_model_param() -> None:
llm = ChatXAI(model="foo")
assert llm.model_name == "foo"