Skip to content

Commit dd5a7d8

Browse files
committed
feat: enhance provider detection and model resolution in Agent class
1 parent df5dc13 commit dd5a7d8

5 files changed

Lines changed: 152 additions & 15 deletions

File tree

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,3 +84,5 @@ site
8484

8585
graphify-out/
8686
service_account.json
87+
88+
eval_reports/

agentflow/core/graph/agent.py

Lines changed: 7 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -264,27 +264,24 @@ class MyState(AgentState):
264264
**kwargs,
265265
)
266266

267-
# check user sending model and provider as prefix, if provider is not explicitly provided
268-
if "/" in model and provider is None:
269-
provider, model = model.split("/", 1)
270-
self.model = model
271-
272267
# Store output type
273268
self.output_type = output_type.lower()
274269
self.output_schema = output_schema
275270
self._validate_output_schema_output_type()
276271

277272
# Determine provider; self.llm_kwargs is set by super().__init__ and is
278273
# already available here for _create_client().
274+
self.base_url = base_url
279275
if provider is not None:
276+
# Provider explicitly supplied — trust it as-is.
280277
self.provider = provider.lower()
281-
self.base_url = base_url
282278
self.client = self._create_client(self.provider, base_url, use_vertex_ai)
283279
else:
284-
# Auto-detect provider from model name
285-
self.provider = self._detect_provider_from_model(model, use_vertex_ai)
286-
self.base_url = base_url
287-
self.client = self._create_client(self.provider, base_url)
280+
# Resolve provider (and strip a recognised ``provider/`` prefix) from
281+
# the model string. Unknown prefixes resolve to ``openai`` and keep
282+
# the full model name (e.g. OpenAI-compatible/self-hosted models).
283+
self.provider, self.model = self._resolve_provider_and_model(model, use_vertex_ai)
284+
self.client = self._create_client(self.provider, base_url, use_vertex_ai)
288285

289286
# Validate that provider supports the output type
290287
self._validate_output_type()

agentflow/core/graph/agent_internal/providers.py

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,11 @@
55
import logging
66
from typing import Any, Protocol
77

8-
from agentflow.core.llm.client_factory import create_llm_client, detect_provider
8+
from agentflow.core.llm.client_factory import (
9+
create_llm_client,
10+
detect_provider,
11+
resolve_provider_and_model,
12+
)
913

1014
from .constants import (
1115
CLIENT_CONSTRUCTOR_KWARGS,
@@ -53,6 +57,18 @@ def _detect_provider_from_model(self, model: str, use_vertex_ai: bool = False) -
5357
"""Infer the provider from the model name when not explicitly supplied."""
5458
return detect_provider(model, use_vertex_ai=use_vertex_ai)
5559

60+
def _resolve_provider_and_model(
61+
self, model: str, use_vertex_ai: bool = False
62+
) -> tuple[str, str]:
63+
"""Resolve a model string into a ``(provider, model)`` pair.
64+
65+
Recognised ``provider/`` prefixes (``gemini``, ``google``, ``openai``,
66+
``gpt``) select the provider and are stripped from the model name.
67+
Unknown prefixes are kept intact and resolve to the ``openai`` provider
68+
so OpenAI-compatible / self-hosted models work out of the box.
69+
"""
70+
return resolve_provider_and_model(model, use_vertex_ai=use_vertex_ai)
71+
5672
def _create_google_vertex_ai_client(self) -> Any:
5773
return create_llm_client("google", use_vertex_ai=True)
5874

agentflow/core/llm/client_factory.py

Lines changed: 41 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,16 @@
1414

1515
logger = logging.getLogger("agentflow.llm")
1616

17+
# Recognised ``provider/`` prefixes mapped to the concrete provider the client
18+
# factory can build. Anything not listed here is an unknown prefix and resolves
19+
# to ``"openai"`` (the OpenAI SDK is used for OpenAI-compatible endpoints).
20+
_PROVIDER_PREFIXES = {
21+
"gemini": "google",
22+
"google": "google",
23+
"openai": "openai",
24+
"gpt": "openai",
25+
}
26+
1727
# Keys allowed in the AsyncOpenAI constructor but NOT in per-request calls.
1828
_CLIENT_CONSTRUCTOR_KWARGS = frozenset(
1929
{
@@ -44,10 +54,8 @@ def detect_provider(model: str, use_vertex_ai: bool = False) -> str:
4454

4555
if "/" in model:
4656
prefix = model.split("/", 1)[0].lower()
47-
if prefix in ("gemini", "google"):
48-
return "google"
49-
if prefix in ("openai", "gpt"):
50-
return "openai"
57+
if prefix in _PROVIDER_PREFIXES:
58+
return _PROVIDER_PREFIXES[prefix]
5159
# Unknown prefix — fall through to name-based detection using the suffix
5260
model = model.split("/", 1)[1]
5361

@@ -64,6 +72,35 @@ def detect_provider(model: str, use_vertex_ai: bool = False) -> str:
6472
return "openai"
6573

6674

75+
def resolve_provider_and_model(
76+
model: str, use_vertex_ai: bool = False
77+
) -> tuple[str, str]:
78+
"""Resolve a model string into a concrete ``(provider, model)`` pair.
79+
80+
Unlike :func:`detect_provider`, this also returns the model name that should
81+
be sent to the provider. A *recognised* ``provider/`` prefix (e.g.
82+
``"gemini/..."``, ``"openai/..."``) is stripped, since the provider is
83+
selected from the prefix. An *unrecognised* prefix is kept intact: it may be
84+
an OpenAI-compatible / HuggingFace-style identifier (e.g.
85+
``"meta-llama/Llama-3-70b"``) where the slash is part of the real model name.
86+
Such models always resolve to the ``"openai"`` provider.
87+
88+
Args:
89+
model: Model identifier, optionally prefixed with ``"provider/"``.
90+
use_vertex_ai: When True, always selects the ``"google"`` provider.
91+
92+
Returns:
93+
A ``(provider, model)`` tuple where provider is ``"google"`` or
94+
``"openai"``.
95+
"""
96+
if "/" in model:
97+
prefix, rest = model.split("/", 1)
98+
if prefix.lower() in _PROVIDER_PREFIXES:
99+
return detect_provider(model, use_vertex_ai=use_vertex_ai), rest
100+
101+
return detect_provider(model, use_vertex_ai=use_vertex_ai), model
102+
103+
67104
def create_llm_client(
68105
provider: str,
69106
*,

tests/graph/test_agent_internal.py

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -281,6 +281,56 @@ def test_deepseek_defaults_to_openai(self):
281281
agent = _make_openai_agent()
282282
assert agent._detect_provider_from_model("deepseek-chat") == "openai"
283283

284+
def test_unknown_prefix_falls_back_to_openai(self):
285+
agent = _make_openai_agent()
286+
assert agent._detect_provider_from_model("ollama/llama3") == "openai"
287+
assert agent._detect_provider_from_model("anthropic/claude-3") == "openai"
288+
289+
290+
class TestResolveProviderAndModel:
291+
"""``_resolve_provider_and_model`` returns ``(provider, model)``: it strips
292+
recognised provider aliases and defaults unknown prefixes to openai."""
293+
294+
def test_gemini_alias_maps_to_google(self):
295+
agent = _make_openai_agent()
296+
assert agent._resolve_provider_and_model("gemini/gemini-2.5-flash") == (
297+
"google",
298+
"gemini-2.5-flash",
299+
)
300+
301+
def test_google_alias_maps_to_google(self):
302+
agent = _make_openai_agent()
303+
assert agent._resolve_provider_and_model("google/gemini-2.0-flash") == (
304+
"google",
305+
"gemini-2.0-flash",
306+
)
307+
308+
def test_openai_alias_maps_to_openai(self):
309+
agent = _make_openai_agent()
310+
assert agent._resolve_provider_and_model("openai/gpt-4o") == ("openai", "gpt-4o")
311+
312+
def test_gpt_alias_maps_to_openai(self):
313+
agent = _make_openai_agent()
314+
assert agent._resolve_provider_and_model("gpt/gpt-4o") == ("openai", "gpt-4o")
315+
316+
def test_unknown_prefix_defaults_to_openai_and_keeps_full_model(self):
317+
agent = _make_openai_agent()
318+
assert agent._resolve_provider_and_model("meta-llama/Llama-3-70b") == (
319+
"openai",
320+
"meta-llama/Llama-3-70b",
321+
)
322+
323+
def test_bare_unknown_model_defaults_to_openai(self):
324+
agent = _make_openai_agent()
325+
assert agent._resolve_provider_and_model("llama3:70b") == ("openai", "llama3:70b")
326+
327+
def test_use_vertex_ai_forces_google(self):
328+
agent = _make_openai_agent()
329+
assert agent._resolve_provider_and_model("llama3:70b", use_vertex_ai=True) == (
330+
"google",
331+
"llama3:70b",
332+
)
333+
284334

285335
class TestValidateOutputType:
286336
def test_valid_text_type_does_not_raise(self):
@@ -1955,6 +2005,41 @@ def test_unknown_model_without_provider_auto_detects_openai(self):
19552005
agent = Agent(model="llama3:70b", reasoning_config=None)
19562006
assert agent.provider == "openai"
19572007

2008+
def test_gemini_slash_prefix_maps_to_google_provider(self):
2009+
"""The ``gemini/`` alias must resolve to the ``google`` provider."""
2010+
with patch.object(Agent, "_create_client", return_value=MagicMock()):
2011+
agent = Agent(model="gemini/gemini-2.5-flash", reasoning_config=None)
2012+
assert agent.provider == "google"
2013+
assert agent.model == "gemini-2.5-flash"
2014+
2015+
def test_gpt_slash_prefix_maps_to_openai_provider(self):
2016+
"""The ``gpt/`` alias must resolve to the ``openai`` provider."""
2017+
with patch.object(Agent, "_create_client", return_value=MagicMock()):
2018+
agent = Agent(model="gpt/gpt-4o", reasoning_config=None)
2019+
assert agent.provider == "openai"
2020+
assert agent.model == "gpt-4o"
2021+
2022+
def test_unknown_prefix_resolves_to_openai_and_keeps_full_model(self):
2023+
"""An unrecognised prefix must default to openai, not google, and keep
2024+
the full model string (it may be an OpenAI-compatible / HF-style name)."""
2025+
with patch.object(Agent, "_create_client", return_value=MagicMock()):
2026+
agent = Agent(model="meta-llama/Llama-3-70b", reasoning_config=None)
2027+
assert agent.provider == "openai"
2028+
assert agent.model == "meta-llama/Llama-3-70b"
2029+
2030+
def test_anthropic_prefix_resolves_to_openai(self):
2031+
"""Claude via an OpenAI-compatible endpoint should not select google."""
2032+
with patch.object(Agent, "_create_client", return_value=MagicMock()):
2033+
agent = Agent(model="anthropic/claude-3", reasoning_config=None)
2034+
assert agent.provider == "openai"
2035+
assert agent.model == "anthropic/claude-3"
2036+
2037+
def test_ollama_prefix_resolves_to_openai(self):
2038+
with patch.object(Agent, "_create_client", return_value=MagicMock()):
2039+
agent = Agent(model="ollama/llama3", reasoning_config=None)
2040+
assert agent.provider == "openai"
2041+
assert agent.model == "ollama/llama3"
2042+
19582043
# ── reasoning config normalization ────────────────────────────────────
19592044

19602045
def test_default_sentinel_produces_medium_effort(self):

0 commit comments

Comments
 (0)