Files
langchain/libs/partners/openrouter/tests/integration_tests/test_standard.py
Mason Daugherty f9fd7be695 feat(openrouter): add langchain-openrouter provider package (#35211)
Add a first-party `langchain-openrouter` partner package
(`ChatOpenRouter`) that wraps the official `openrouter` Python SDK,
providing native support for OpenRouter-specific features that
`ChatOpenAI` intentionally does not handle.

Also adds scope-clarifying docstrings to `ChatOpenAI` / `BaseChatOpenAI`
warning users away from using `base_url` overrides with third-party
providers.

---

Closes #31325
Closes #32967
Closes #32977
Closes #32981
Closes #33643
Closes #33757
Closes #34056
Closes #34797
Closes #34962

Supersedes #33902, #34867 (thank you @elonfeng and @okamototk for your
initial work on this!)

---

Bugs with upstream sdk:
- https://github.com/OpenRouterTeam/python-sdk/issues/38
- https://github.com/OpenRouterTeam/python-sdk/issues/51
- https://github.com/OpenRouterTeam/python-sdk/issues/52
2026-02-15 02:09:13 -05:00

121 lines
3.1 KiB
Python

"""Standard integration tests for `ChatOpenRouter`."""
from langchain_core.messages import AIMessage, AIMessageChunk
from langchain_tests.integration_tests import ChatModelIntegrationTests
from langchain_openrouter.chat_models import ChatOpenRouter
MODEL_NAME = "openai/gpt-4o-mini"
class TestChatOpenRouter(ChatModelIntegrationTests):
"""Test `ChatOpenRouter` chat model."""
@property
def chat_model_class(self) -> type[ChatOpenRouter]:
"""Return class of chat model being tested."""
return ChatOpenRouter
@property
def chat_model_params(self) -> dict:
"""Parameters to create chat model instance for testing."""
return {
"model": MODEL_NAME,
"temperature": 0,
}
@property
def returns_usage_metadata(self) -> bool:
# Don't want to implement tests for now
return False
@property
def supports_json_mode(self) -> bool:
return False
@property
def supports_image_inputs(self) -> bool:
return True
@property
def supports_image_urls(self) -> bool:
return True
@property
def supports_video_inputs(self) -> bool:
return True
@property
def model_override_value(self) -> str:
return "openai/gpt-4o"
AUDIO_MODEL = "google/gemini-2.5-flash"
REASONING_MODEL = "openai/o3-mini"
class TestChatOpenRouterMultiModal(ChatModelIntegrationTests):
"""Tests for audio input and reasoning output capabilities.
Uses an audio-capable model as the base and creates separate model
instances for reasoning tests.
"""
@property
def chat_model_class(self) -> type[ChatOpenRouter]:
return ChatOpenRouter
@property
def chat_model_params(self) -> dict:
return {
"model": AUDIO_MODEL,
"temperature": 0,
}
@property
def returns_usage_metadata(self) -> bool:
# Don't want to implement tests for now
return False
@property
def supports_json_mode(self) -> bool:
return False
@property
def supports_image_inputs(self) -> bool:
return True
@property
def supports_image_urls(self) -> bool:
return True
@property
def supports_audio_inputs(self) -> bool:
return True
@property
def supports_video_inputs(self) -> bool:
return True
@property
def model_override_value(self) -> str:
return "openai/gpt-4o"
def invoke_with_reasoning_output(self, *, stream: bool = False) -> AIMessage:
"""Invoke a reasoning model to exercise reasoning token tracking."""
llm = ChatOpenRouter(
model=REASONING_MODEL,
reasoning={"effort": "medium"},
)
prompt = (
"Explain the relationship between the 2008/9 economic crisis and "
"the startup ecosystem in the early 2010s"
)
if stream:
full: AIMessageChunk | None = None
for chunk in llm.stream(prompt):
full = chunk if full is None else full + chunk # type: ignore[assignment]
assert full is not None
return full
return llm.invoke(prompt)