Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions tests/tools/test_langchain_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,25 @@ def test_build_chat_client_anthropic_fallback(monkeypatch: pytest.MonkeyPatch) -
assert resolved.model == "claude-sonnet-4-6"


def test_build_chat_client_anthropic_without_openai_package(
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""Claude can run in Anthropic-only environments without langchain_openai."""
monkeypatch.setitem(sys.modules, "langchain_openai", None)
FakeChatAnthropic = _install_fake_langchain_anthropic(monkeypatch)
monkeypatch.delenv("OPENAI_API_KEY", raising=False)
monkeypatch.delenv("GITHUB_TOKEN", raising=False)
monkeypatch.setenv(langchain_client.ENV_ANTHROPIC_KEY, "claude-token")
monkeypatch.delenv(langchain_client.ENV_PROVIDER, raising=False)

resolved = langchain_client.build_chat_client()

assert resolved is not None
assert resolved.provider == langchain_client.PROVIDER_ANTHROPIC
assert isinstance(resolved.client, FakeChatAnthropic)
assert resolved.client.kwargs["anthropic_api_key"] == "claude-token"


def test_build_chat_client_env_provider_override(monkeypatch: pytest.MonkeyPatch) -> None:
"""Provider override env var should force OpenAI when set."""
FakeChatOpenAI = _install_fake_langchain_openai(monkeypatch)
Expand Down Expand Up @@ -524,6 +543,24 @@ def __init__(self, **kwargs):
assert isinstance(clients[0].client, FakeChatOpenAI)


def test_build_chat_clients_anthropic_without_openai_package(
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""Multi-client resolution should not make Anthropic depend on OpenAI imports."""
monkeypatch.setitem(sys.modules, "langchain_openai", None)
FakeChatAnthropic = _install_fake_langchain_anthropic(monkeypatch)
monkeypatch.delenv("OPENAI_API_KEY", raising=False)
monkeypatch.delenv("GITHUB_TOKEN", raising=False)
monkeypatch.setenv(langchain_client.ENV_ANTHROPIC_KEY, "claude-token")
monkeypatch.delenv(langchain_client.ENV_PROVIDER, raising=False)

clients = langchain_client.build_chat_clients()

assert len(clients) == 1
assert clients[0].provider == langchain_client.PROVIDER_ANTHROPIC
assert isinstance(clients[0].client, FakeChatAnthropic)


# --- Reasoning model temperature handling ---


Expand Down
24 changes: 24 additions & 0 deletions tests/tools/test_llm_provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -589,6 +589,18 @@ def test_openai_provider_reports_configured_client_model(self):
mock_build.assert_called_once_with(provider="openai", model="gpt-configured")
assert result.model_name == "gpt-configured"

def test_openai_provider_preserves_blocked_model_signal(self):
provider = OpenAIProvider()

with (
patch("tools.llm_registry.configured_model_for_provider", return_value=""),
patch("tools.langchain_client.build_chat_client") as mock_build,
pytest.raises(RuntimeError, match="LangChain OpenAI not available"),
):
provider.analyze_completion("output", ["task1"])

mock_build.assert_not_called()

def test_anthropic_provider_reports_configured_client_model(self):
provider = AnthropicProvider()
mock_client = MagicMock()
Expand Down Expand Up @@ -616,6 +628,18 @@ def test_anthropic_provider_reports_configured_client_model(self):
mock_build.assert_called_once_with(provider="anthropic", model="claude-configured")
assert result.model_name == "claude-configured"

def test_anthropic_provider_preserves_blocked_model_signal(self):
provider = AnthropicProvider()

with (
patch("tools.llm_registry.configured_model_for_provider", return_value=""),
patch("tools.langchain_client.build_chat_client") as mock_build,
pytest.raises(RuntimeError, match="LangChain Anthropic not available"),
):
provider.analyze_completion("output", ["task1"])

mock_build.assert_not_called()


class TestRegexFallbackProvider:
"""Test regex-based analysis."""
Expand Down
48 changes: 26 additions & 22 deletions tools/langchain_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -204,7 +204,9 @@ def build_chat_client(
try:
from langchain_openai import ChatOpenAI
except ImportError:
return None
chat_openai_cls = None

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Sync the consumer template copies

This source change is consumer-facing, but the matching templates/consumer-repo/tools/langchain_client.py still returns immediately when langchain_openai is missing, and templates/consumer-repo/tools/llm_provider.py still converts a registry "" block signal back to the fallback model. Repos bootstrapped from templates/consumer-repo therefore keep the old behavior: Anthropic-only installs still fail client resolution and blocked fallback models can still be used. Please mirror these tools/ changes into the consumer template copies.

Useful? React with 👍 / 👎.

else:
chat_openai_cls = ChatOpenAI

try:
from langchain_anthropic import ChatAnthropic
Expand All @@ -231,11 +233,11 @@ def build_chat_client(
return None

if selected_provider == PROVIDER_GITHUB:
if not github_token:
if not github_token or not chat_openai_cls:
return None
try:
client = _build_github_client(
ChatOpenAI,
chat_openai_cls,
model=selected_model,
token=github_token,
timeout=selected_timeout,
Expand All @@ -246,11 +248,11 @@ def build_chat_client(
return None

if selected_provider == PROVIDER_OPENAI:
if not openai_token:
if not openai_token or not chat_openai_cls:
return None
try:
client = _build_openai_client(
ChatOpenAI,
chat_openai_cls,
model=selected_model,
token=openai_token,
timeout=selected_timeout,
Expand Down Expand Up @@ -292,15 +294,15 @@ def build_chat_client(
(
slot.provider == PROVIDER_OPENAI and openai_token,
slot.provider == PROVIDER_ANTHROPIC and anthropic_token and chat_anthropic_cls,
slot.provider == PROVIDER_GITHUB and github_token,
slot.provider == PROVIDER_GITHUB and github_token and chat_openai_cls,
)
)
if not slot_available:
continue
if slot.provider == PROVIDER_OPENAI and openai_token:
if slot.provider == PROVIDER_OPENAI and openai_token and chat_openai_cls:
with contextlib.suppress(Exception):
client = _build_openai_client(
ChatOpenAI,
chat_openai_cls,
model=slot_model,
token=openai_token,
timeout=selected_timeout,
Expand All @@ -319,10 +321,10 @@ def build_chat_client(
)
used_override = True
return ClientInfo(client=client, provider=PROVIDER_ANTHROPIC, model=slot_model)
if slot.provider == PROVIDER_GITHUB and github_token:
if slot.provider == PROVIDER_GITHUB and github_token and chat_openai_cls:
with contextlib.suppress(Exception):
client = _build_github_client(
ChatOpenAI,
chat_openai_cls,
model=slot_model,
token=github_token,
timeout=selected_timeout,
Expand All @@ -345,7 +347,9 @@ def build_chat_clients(
try:
from langchain_openai import ChatOpenAI
except ImportError:
return []
chat_openai_cls = None
else:
chat_openai_cls = ChatOpenAI

try:
from langchain_anthropic import ChatAnthropic
Expand Down Expand Up @@ -382,12 +386,12 @@ def build_chat_clients(
clients: list[ClientInfo] = []

if selected_provider:
if selected_provider == PROVIDER_GITHUB and github_token:
if selected_provider == PROVIDER_GITHUB and github_token and chat_openai_cls:
with contextlib.suppress(Exception):
clients.append(
ClientInfo(
client=_build_github_client(
ChatOpenAI,
chat_openai_cls,
model=first_model,
token=github_token,
timeout=selected_timeout,
Expand All @@ -402,7 +406,7 @@ def build_chat_clients(
clients.append(
ClientInfo(
client=_build_github_client(
ChatOpenAI,
chat_openai_cls,
model=second_model,
token=github_token,
timeout=selected_timeout,
Expand All @@ -412,12 +416,12 @@ def build_chat_clients(
model=second_model,
)
)
elif selected_provider == PROVIDER_OPENAI and openai_token:
elif selected_provider == PROVIDER_OPENAI and openai_token and chat_openai_cls:
with contextlib.suppress(Exception):
clients.append(
ClientInfo(
client=_build_openai_client(
ChatOpenAI,
chat_openai_cls,
model=first_model,
token=openai_token,
timeout=selected_timeout,
Expand All @@ -432,7 +436,7 @@ def build_chat_clients(
clients.append(
ClientInfo(
client=_build_openai_client(
ChatOpenAI,
chat_openai_cls,
model=second_model,
token=openai_token,
timeout=selected_timeout,
Expand Down Expand Up @@ -482,7 +486,7 @@ def build_chat_clients(
(
slot.provider == PROVIDER_OPENAI and openai_token,
slot.provider == PROVIDER_ANTHROPIC and anthropic_token and chat_anthropic_cls,
slot.provider == PROVIDER_GITHUB and github_token,
slot.provider == PROVIDER_GITHUB and github_token and chat_openai_cls,
)
):
candidate_slots.append(slot)
Expand All @@ -498,12 +502,12 @@ def build_chat_clients(
if _is_model_blocked(slot.provider, slot_model, registry=registry):
logger.warning("Skipping blocked LLM model override: %s/%s", slot.provider, slot_model)
continue
if slot.provider == PROVIDER_OPENAI and openai_token:
if slot.provider == PROVIDER_OPENAI and openai_token and chat_openai_cls:
with contextlib.suppress(Exception):
clients.append(
ClientInfo(
client=_build_openai_client(
ChatOpenAI,
chat_openai_cls,
model=slot_model,
token=openai_token,
timeout=selected_timeout,
Expand All @@ -528,12 +532,12 @@ def build_chat_clients(
model=slot_model,
)
)
if slot.provider == PROVIDER_GITHUB and github_token:
if slot.provider == PROVIDER_GITHUB and github_token and chat_openai_cls:
with contextlib.suppress(Exception):
clients.append(
ClientInfo(
client=_build_github_client(
ChatOpenAI,
chat_openai_cls,
model=slot_model,
token=github_token,
timeout=selected_timeout,
Expand Down
7 changes: 6 additions & 1 deletion tools/llm_provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,8 @@ def _configured_langchain_model(provider: str, *, fallback: str) -> str:
from tools.llm_registry import configured_model_for_provider
except ImportError:
return fallback
return configured_model_for_provider(provider, fallback=fallback) or fallback
configured = configured_model_for_provider(provider, fallback=fallback)
return fallback if configured is None else configured


def _setup_langsmith_tracing() -> bool:
Expand Down Expand Up @@ -601,6 +602,8 @@ def _get_client(self):
return None

model_name = _configured_langchain_model("openai", fallback=DEFAULT_OPENAI_ANALYSIS_MODEL)
if not model_name:
return None
resolved = build_chat_client(provider="openai", model=model_name)
if resolved:
self._model_name = resolved.model
Expand Down Expand Up @@ -674,6 +677,8 @@ def _get_client(self):
model_name = _configured_langchain_model(
"anthropic", fallback=DEFAULT_ANTHROPIC_ANALYSIS_MODEL
)
if not model_name:
return None
resolved = build_chat_client(provider="anthropic", model=model_name)
if resolved:
self._model_name = resolved.model
Expand Down
Loading