Files
2026-07-13 11:58:32 +08:00

296 lines
9.5 KiB
Python

import os
from unittest import mock
import pytest
from langchain_core.language_models import BaseChatModel
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.runnables import RunnableConfig, RunnableSequence
from pydantic import SecretStr
from langchain_classic.chat_models.base import __all__, init_chat_model
OPENAI_TEST_MODEL = "gpt-5.5"
EXPECTED_ALL = [
"BaseChatModel",
"SimpleChatModel",
"agenerate_from_stream",
"generate_from_stream",
"init_chat_model",
]
def test_all_imports() -> None:
assert set(__all__) == set(EXPECTED_ALL)
@pytest.mark.requires(
"langchain_openai",
"langchain_anthropic",
"langchain_fireworks",
"langchain_groq",
)
@pytest.mark.parametrize(
("model_name", "model_provider"),
[
(OPENAI_TEST_MODEL, "openai"),
("claude-opus-4-1", "anthropic"),
("accounts/fireworks/models/mixtral-8x7b-instruct", "fireworks"),
("mixtral-8x7b-32768", "groq"),
],
)
def test_init_chat_model(model_name: str, model_provider: str | None) -> None:
llm1: BaseChatModel = init_chat_model(
model_name,
model_provider=model_provider,
api_key="foo",
)
llm2: BaseChatModel = init_chat_model(
f"{model_provider}:{model_name}",
api_key="foo",
)
assert llm1.dict() == llm2.dict()
def test_init_missing_dep() -> None:
with pytest.raises(ImportError):
init_chat_model("mixtral-8x7b-32768", model_provider="groq")
def test_init_unknown_provider() -> None:
with pytest.raises(ValueError, match="Unsupported model_provider='bar'"):
init_chat_model("foo", model_provider="bar")
@pytest.mark.requires("langchain_openai")
@mock.patch.dict(
os.environ,
{"OPENAI_API_KEY": "foo", "ANTHROPIC_API_KEY": "bar"},
clear=True,
)
def test_configurable() -> None:
"""Test configurable chat model behavior without default parameters.
Verifies that a configurable chat model initialized without default parameters:
- Has access to all standard runnable methods (`invoke`, `stream`, etc.)
- Blocks access to non-configurable methods until configuration is provided
- Supports declarative operations (`bind_tools`) without mutating original model
- Can chain declarative operations and configuration to access full functionality
- Properly resolves to the configured model type when parameters are provided
Example:
```python
# This creates a configurable model without specifying which model
model = init_chat_model()
# This will FAIL - no model specified yet
model.get_num_tokens("hello") # AttributeError!
# This works - provides model at runtime
response = model.invoke("Hello", config={"configurable": {"model": "gpt-5.5"}})
```
"""
model = init_chat_model()
for method in (
"invoke",
"ainvoke",
"batch",
"abatch",
"stream",
"astream",
"batch_as_completed",
"abatch_as_completed",
):
assert hasattr(model, method)
# Doesn't have access non-configurable, non-declarative methods until a config is
# provided.
for method in ("get_num_tokens", "get_num_tokens_from_messages"):
with pytest.raises(AttributeError):
getattr(model, method)
# Can call declarative methods even without a default model.
model_with_tools = model.bind_tools(
[{"name": "foo", "description": "foo", "parameters": {}}],
)
# Check that original model wasn't mutated by declarative operation.
assert model._queued_declarative_operations == []
# Can iteratively call declarative methods.
model_with_config = model_with_tools.with_config(
RunnableConfig(tags=["foo"]),
configurable={"model": OPENAI_TEST_MODEL},
)
assert model_with_config.model_name == OPENAI_TEST_MODEL # type: ignore[attr-defined]
for method in ("get_num_tokens", "get_num_tokens_from_messages"):
assert hasattr(model_with_config, method)
assert model_with_config.model_dump() == { # type: ignore[attr-defined]
"name": None,
"bound": {
"name": None,
"disable_streaming": False,
"disabled_params": None,
"model_name": OPENAI_TEST_MODEL,
"temperature": None,
"model_kwargs": {},
"openai_api_key": SecretStr("foo"),
"openai_api_base": None,
"openai_organization": None,
"openai_proxy": None,
"output_version": None,
"request_timeout": None,
"max_retries": None,
"presence_penalty": None,
"reasoning": None,
"reasoning_effort": None,
"verbosity": None,
"frequency_penalty": None,
"context_management": None,
"include": None,
"seed": None,
"prompt_cache_options": None,
"service_tier": None,
"logprobs": None,
"top_logprobs": None,
"logit_bias": None,
"streaming": False,
"n": None,
"top_p": None,
"truncation": None,
"max_tokens": None,
"tiktoken_model_name": None,
"default_headers": None,
"default_query": None,
"stop": None,
"store": None,
"extra_body": None,
"include_response_headers": False,
"stream_usage": True,
"use_previous_response_id": False,
"use_responses_api": None,
},
"kwargs": {
"tools": [
{
"type": "function",
"function": {"name": "foo", "description": "foo", "parameters": {}},
},
],
},
"config": {"tags": ["foo"], "configurable": {}},
"config_factories": [],
"custom_input_type": None,
"custom_output_type": None,
}
@pytest.mark.requires("langchain_openai", "langchain_anthropic")
@mock.patch.dict(
os.environ,
{"OPENAI_API_KEY": "foo", "ANTHROPIC_API_KEY": "bar"},
clear=True,
)
def test_configurable_with_default() -> None:
"""Test configurable chat model behavior with default parameters.
Verifies that a configurable chat model initialized with default parameters:
- Has access to all standard runnable methods (`invoke`, `stream`, etc.)
- Provides immediate access to non-configurable methods (e.g. `get_num_tokens`)
- Supports model switching through runtime configuration using `config_prefix`
- Maintains proper model identity and attributes when reconfigured
- Can be used in chains with different model providers via configuration
Example:
```python
# This creates a configurable model with default parameters (model)
model = init_chat_model("gpt-5.5", configurable_fields="any", config_prefix="bar")
# This works immediately - uses default gpt-5.5
tokens = model.get_num_tokens("hello")
# This also works - switches to Claude at runtime
response = model.invoke(
"Hello",
config={"configurable": {"my_model_model": "claude-3-sonnet-20240229"}},
)
```
"""
model = init_chat_model(
OPENAI_TEST_MODEL, configurable_fields="any", config_prefix="bar"
)
for method in (
"invoke",
"ainvoke",
"batch",
"abatch",
"stream",
"astream",
"batch_as_completed",
"abatch_as_completed",
):
assert hasattr(model, method)
# Does have access non-configurable, non-declarative methods since default params
# are provided.
for method in ("get_num_tokens", "get_num_tokens_from_messages", "dict"):
assert hasattr(model, method)
assert model.model_name == OPENAI_TEST_MODEL
model_with_tools = model.bind_tools(
[{"name": "foo", "description": "foo", "parameters": {}}],
)
model_with_config = model_with_tools.with_config(
RunnableConfig(tags=["foo"]),
configurable={"bar_model": "claude-sonnet-4-5-20250929"},
)
assert model_with_config.model == "claude-sonnet-4-5-20250929" # type: ignore[attr-defined]
assert model_with_config.model_dump() == { # type: ignore[attr-defined]
"name": None,
"bound": {
"name": None,
"disable_streaming": False,
"model": "claude-sonnet-4-5-20250929",
"mcp_servers": None,
"max_tokens": 64000,
"temperature": None,
"thinking": None,
"effort": None,
"top_k": None,
"top_p": None,
"default_request_timeout": None,
"max_retries": 2,
"stop_sequences": None,
"anthropic_api_url": "https://api.anthropic.com",
"anthropic_proxy": None,
"context_management": None,
"anthropic_api_key": SecretStr("bar"),
"betas": None,
"default_headers": None,
"model_kwargs": {},
"reuse_last_container": None,
"inference_geo": None,
"streaming": False,
"stream_usage": True,
"output_version": None,
"output_config": None,
},
"kwargs": {
"tools": [{"name": "foo", "description": "foo", "input_schema": {}}],
},
"config": {"tags": ["foo"], "configurable": {}},
"config_factories": [],
"custom_input_type": None,
"custom_output_type": None,
}
prompt = ChatPromptTemplate.from_messages([("system", "foo")])
chain = prompt | model_with_config
assert isinstance(chain, RunnableSequence)