mirror of
https://github.com/langchain-ai/langchain.git
synced 2026-10-05 09:25:14 +03:00
feat(model-profiles): distribute data across packages (#34024)
This commit is contained in:
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."""
|
||||
@@ -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",
|
||||
]
|
||||
|
||||
|
||||
Generated
-44
@@ -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"
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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 }
|
||||
|
||||
+3
-7
@@ -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."""
|
||||
|
||||
Generated
+1
-51
@@ -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"
|
||||
|
||||
@@ -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`.
|
||||
-6
@@ -1,6 +0,0 @@
|
||||
[profile]
|
||||
image_url_inputs = false
|
||||
pdf_inputs = false
|
||||
pdf_tool_message = false
|
||||
image_tool_message = false
|
||||
structured_output = false
|
||||
-6
@@ -1,6 +0,0 @@
|
||||
[profile]
|
||||
image_url_inputs = true
|
||||
pdf_inputs = true
|
||||
pdf_tool_message = true
|
||||
image_tool_message = true
|
||||
structured_output = false
|
||||
-6
@@ -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
|
||||
-6
@@ -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]
|
||||
@@ -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)
|
||||
Generated
+6
-10
@@ -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
|
||||
@@ -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
|
||||
Generated
+20
-2
@@ -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
|
||||
@@ -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"
|
||||
|
||||
Reference in new issue
Block a user