mirror of
https://github.com/langchain-ai/deepagents.git
synced 2026-07-22 01:35:28 -04:00
fix(code): infer additional model providers (#4675)
Deep Agents Code now automatically detects the provider for additional bare model names supported by its provider extras — Cohere Command, Mistral AI, DeepSeek, xAI Grok, and Perplexity Sonar — and recognizes Amazon Bedrock model IDs by their vendor-namespaced structure, including versioned and cross-region inference-profile IDs. --- Deep Agents Code resolves a model provider before calling `init_chat_model` so it can apply provider-specific configuration and report credential errors clearly. Its local inference table did not recognize several bare model names that LangChain already supports, even though the corresponding dcode provider extras are available. For the direct-API providers, mirror LangChain's supported prefix mappings: OpenAI `text-davinci*`, Cohere `command*`, Mistral AI `mistral*`/`mixtral*`, DeepSeek `deepseek*`, xAI `grok*`, and Perplexity `sonar*`. Bedrock is handled structurally rather than by an enumerated prefix list: any `[<region>.]<vendor>.<model>` ID (e.g. `meta.llama3-70b-instruct-v1:0`, `mistral.mistral-large-2402-v1:0`, or the cross-region profile `us.anthropic.claude-3-5-sonnet-20241022-v2:0`) is detected as Bedrock. Keying off the dotted vendor-namespace signature avoids maintaining AWS's ever-growing vendor list while keeping bare direct-API names (`mistral-large`, `command-r-plus`) routed to their own APIs. Because Bedrock IDs can carry a `:<version>` suffix, `create_model` now treats a detected Bedrock spec as a bare model name so the suffix is not misparsed as a `provider:model` separator. An explicit `provider:model` spec whose provider is configured still takes precedence. Existing dcode-specific handling for `o4*`, NVIDIA, credential-aware Claude and Gemini routing, and fully qualified Fireworks model IDs remains unchanged. `solar*` remains unrecognized because dcode does not yet package or configure Upstage.
This commit is contained in:
@@ -3784,6 +3784,36 @@ def get_default_coding_instructions() -> str:
|
||||
return default_prompt_path.read_text()
|
||||
|
||||
|
||||
_BEDROCK_REGION_PREFIXES = ("us.", "eu.", "apac.", "us-gov.")
|
||||
"""Cross-region inference-profile prefixes that front a vendor namespace.
|
||||
|
||||
E.g. `us.anthropic.claude-3-5-sonnet-20241022-v2:0`. Only stripped when a vendor
|
||||
namespace follows, so a bare name merely starting with `us`/`eu` is untouched.
|
||||
"""
|
||||
|
||||
|
||||
def _is_bedrock_model_id(model_lower: str) -> bool:
|
||||
"""Return whether *model_lower* is a bare Bedrock model ID.
|
||||
|
||||
Bedrock IDs have the shape `[<region>.]<vendor>.<model>[:<version>]`, e.g.
|
||||
`meta.llama3-70b-instruct-v1:0` or the cross-region inference profile
|
||||
`us.anthropic.claude-3-5-sonnet-20241022-v2:0`. Rather than enumerate AWS's
|
||||
ever-growing vendor list, this keys off the structural signature: an
|
||||
alphanumeric vendor token immediately followed by a dot. Bare direct-API
|
||||
names don't fit -- they either have no dot (`mistral-large`, `command-r`),
|
||||
carry a hyphen before their version dot (`claude-3.5`, `gemini-2.5`), or are
|
||||
already claimed by an earlier prefix check (`gpt-4.1`). Case is folded by the
|
||||
caller, and the explicit `bedrock:<model>` syntax is handled upstream via
|
||||
`provider:model` parsing.
|
||||
"""
|
||||
for region in _BEDROCK_REGION_PREFIXES:
|
||||
if model_lower.startswith(region):
|
||||
model_lower = model_lower.removeprefix(region)
|
||||
break
|
||||
vendor, dot, _ = model_lower.partition(".")
|
||||
return bool(dot) and vendor.isalnum()
|
||||
|
||||
|
||||
def detect_provider(model_name: str) -> str | None:
|
||||
"""Auto-detect provider from model name.
|
||||
|
||||
@@ -3799,15 +3829,37 @@ def detect_provider(model_name: str) -> str | None:
|
||||
model_name: Model name to detect provider from.
|
||||
|
||||
Returns:
|
||||
Provider name (openai, anthropic, google_genai, google_vertexai,
|
||||
nvidia, fireworks) or `None` if the provider cannot be determined
|
||||
from the name alone.
|
||||
Provider name inferred from the model name (some names, e.g. `claude`
|
||||
and `gemini`, are disambiguated using configured credentials), or
|
||||
`None` if the provider cannot be determined.
|
||||
"""
|
||||
model_lower = model_name.lower()
|
||||
|
||||
if model_lower.startswith(("gpt-", "o1", "o3", "o4", "chatgpt")):
|
||||
if model_lower.startswith(("gpt-", "o1", "o3", "o4", "chatgpt", "text-davinci")):
|
||||
return "openai"
|
||||
|
||||
# Bedrock uses dotted, vendor-namespaced IDs. Match them before the bare
|
||||
# `mistral`/`deepseek` prefixes below (which would otherwise swallow
|
||||
# `mistral.`/`deepseek.` IDs) and before the fall-through `None`, so a
|
||||
# `:version` suffix is never misparsed as a `provider:model` separator.
|
||||
if _is_bedrock_model_id(model_lower):
|
||||
return "bedrock"
|
||||
|
||||
if model_lower.startswith("command"):
|
||||
return "cohere"
|
||||
|
||||
if model_lower.startswith(("mistral", "mixtral")):
|
||||
return "mistralai"
|
||||
|
||||
if model_lower.startswith("deepseek"):
|
||||
return "deepseek"
|
||||
|
||||
if model_lower.startswith("grok"):
|
||||
return "xai"
|
||||
|
||||
if model_lower.startswith("sonar"):
|
||||
return "perplexity"
|
||||
|
||||
if model_lower.startswith("claude"):
|
||||
s = _get_settings()
|
||||
if not s.has_anthropic and s.has_vertex_ai:
|
||||
@@ -4359,11 +4411,19 @@ def create_model(
|
||||
if not model_spec:
|
||||
model_spec = _get_default_model_spec()
|
||||
|
||||
# Parse provider:model syntax
|
||||
# Parse provider:model syntax. Bedrock model IDs can include a version suffix
|
||||
# such as `:0`, so resolve their distinctive bare-ID prefixes unless the
|
||||
# parsed provider is explicitly configured.
|
||||
provider: str
|
||||
model_name: str
|
||||
config = ModelConfig.load()
|
||||
inferred_provider = detect_provider(model_spec)
|
||||
parsed = ModelSpec.try_parse(model_spec)
|
||||
if parsed:
|
||||
if parsed and parsed.provider in config.providers:
|
||||
provider, model_name = parsed.provider, parsed.model
|
||||
elif inferred_provider == "bedrock":
|
||||
provider, model_name = inferred_provider, model_spec
|
||||
elif parsed:
|
||||
# Explicit provider:model (e.g., "anthropic:claude-sonnet-4-5")
|
||||
provider, model_name = parsed.provider, parsed.model
|
||||
elif ":" in model_spec:
|
||||
@@ -4382,7 +4442,7 @@ def create_model(
|
||||
else:
|
||||
# Bare model name — auto-detect provider or let init_chat_model infer
|
||||
model_name = model_spec
|
||||
provider = detect_provider(model_spec) or ""
|
||||
provider = inferred_provider or ""
|
||||
|
||||
# Stored API keys (added via `/auth`) take effect by being copied onto
|
||||
# the env var name LangChain reads. Apply before the credential check so
|
||||
@@ -4473,7 +4533,6 @@ def create_model(
|
||||
kwargs[_resolve_retry_param_name(provider)] = cli_max_retries
|
||||
|
||||
# Check if this provider uses a custom BaseChatModel class
|
||||
config = ModelConfig.load()
|
||||
class_path = config.get_class_path(provider) if provider else None
|
||||
|
||||
if provider == CODEX_PROVIDER:
|
||||
|
||||
@@ -4369,6 +4369,38 @@ temperature = 0
|
||||
assert result.model_name == "my-model"
|
||||
assert result.provider == "custom"
|
||||
|
||||
def test_configured_provider_takes_precedence_over_bedrock_inference(
|
||||
self, tmp_path: Path
|
||||
) -> None:
|
||||
"""A configured explicit provider is not treated as a bare Bedrock ID."""
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
from langchain_core.language_models import BaseChatModel
|
||||
|
||||
config_path = tmp_path / "config.toml"
|
||||
config_path.write_text("""
|
||||
[models.providers."meta.custom"]
|
||||
class_path = "my_pkg.models:MyChatModel"
|
||||
models = ["my-model"]
|
||||
""")
|
||||
mock_instance = MagicMock(spec=BaseChatModel)
|
||||
mock_instance.profile = None
|
||||
|
||||
with (
|
||||
patch.object(model_config, "DEFAULT_CONFIG_PATH", config_path),
|
||||
patch(
|
||||
"deepagents_code.config._create_model_from_class",
|
||||
return_value=mock_instance,
|
||||
) as mock_factory,
|
||||
):
|
||||
result = create_model("meta.custom:my-model")
|
||||
|
||||
mock_factory.assert_called_once()
|
||||
assert mock_factory.call_args.args[1:3] == ("my-model", "meta.custom")
|
||||
assert result.model is mock_instance
|
||||
assert result.model_name == "my-model"
|
||||
assert result.provider == "meta.custom"
|
||||
|
||||
def test_create_model_falls_through_without_class_path(
|
||||
self, tmp_path: Path
|
||||
) -> None:
|
||||
@@ -4500,6 +4532,96 @@ class TestCreateModelEdgeCaseParsing:
|
||||
# Should have detected 'anthropic' provider and used 'claude-opus-4-6'
|
||||
assert result.model_name == "claude-opus-4-6"
|
||||
|
||||
@patch("langchain.chat_models.init_chat_model")
|
||||
def test_versioned_bedrock_id_treated_as_bare_model(
|
||||
self, mock_init_chat_model: Mock
|
||||
) -> None:
|
||||
"""A Bedrock version suffix is not parsed as a provider separator."""
|
||||
model_id = "meta.llama3-70b-instruct-v1:0"
|
||||
mock_model = Mock()
|
||||
mock_model.profile = None
|
||||
mock_init_chat_model.return_value = mock_model
|
||||
|
||||
result = create_model(model_id)
|
||||
|
||||
assert result.provider == "bedrock"
|
||||
assert result.model_name == model_id
|
||||
assert mock_init_chat_model.call_args.args == (model_id,)
|
||||
assert mock_init_chat_model.call_args.kwargs["model_provider"] == "bedrock"
|
||||
|
||||
@patch("langchain.chat_models.init_chat_model")
|
||||
def test_versioned_bedrock_vendor_id_not_misparsed(
|
||||
self, mock_init_chat_model: Mock
|
||||
) -> None:
|
||||
"""A Bedrock vendor namespace is not split on its `:version` suffix.
|
||||
|
||||
Regression: `mistral.` collides with the bare `mistral` prefix, so
|
||||
without dotted-namespace detection this ID parses to the garbage pair
|
||||
`provider='mistral.mistral-large-2402-v1', model='0'`.
|
||||
"""
|
||||
model_id = "mistral.mistral-large-2402-v1:0"
|
||||
mock_model = Mock()
|
||||
mock_model.profile = None
|
||||
mock_init_chat_model.return_value = mock_model
|
||||
|
||||
result = create_model(model_id)
|
||||
|
||||
assert result.provider == "bedrock"
|
||||
assert result.model_name == model_id
|
||||
assert mock_init_chat_model.call_args.args == (model_id,)
|
||||
assert mock_init_chat_model.call_args.kwargs["model_provider"] == "bedrock"
|
||||
|
||||
@patch("langchain.chat_models.init_chat_model")
|
||||
def test_cross_region_bedrock_id_treated_as_bare_model(
|
||||
self, mock_init_chat_model: Mock
|
||||
) -> None:
|
||||
"""A cross-region inference-profile ID resolves to Bedrock intact."""
|
||||
model_id = "us.anthropic.claude-3-5-sonnet-20241022-v2:0"
|
||||
mock_model = Mock()
|
||||
mock_model.profile = None
|
||||
mock_init_chat_model.return_value = mock_model
|
||||
|
||||
result = create_model(model_id)
|
||||
|
||||
assert result.provider == "bedrock"
|
||||
assert result.model_name == model_id
|
||||
assert mock_init_chat_model.call_args.args == (model_id,)
|
||||
assert mock_init_chat_model.call_args.kwargs["model_provider"] == "bedrock"
|
||||
|
||||
@patch("langchain.chat_models.init_chat_model")
|
||||
def test_non_versioned_bedrock_id_treated_as_bare_model(
|
||||
self, mock_init_chat_model: Mock
|
||||
) -> None:
|
||||
"""A Bedrock ID without a `:version` suffix still routes to Bedrock."""
|
||||
model_id = "amazon.titan-text-express-v1"
|
||||
mock_model = Mock()
|
||||
mock_model.profile = None
|
||||
mock_init_chat_model.return_value = mock_model
|
||||
|
||||
result = create_model(model_id)
|
||||
|
||||
assert result.provider == "bedrock"
|
||||
assert result.model_name == model_id
|
||||
assert mock_init_chat_model.call_args.kwargs["model_provider"] == "bedrock"
|
||||
|
||||
@patch("langchain.chat_models.init_chat_model")
|
||||
def test_explicit_provider_not_hijacked_by_bedrock(
|
||||
self, mock_init_chat_model: Mock
|
||||
) -> None:
|
||||
"""An explicit `provider:model` spec wins over Bedrock inference.
|
||||
|
||||
`anthropic.` (dot) is a Bedrock namespace, but `anthropic:` (colon) is
|
||||
the explicit-provider syntax and must resolve to Anthropic, not Bedrock.
|
||||
"""
|
||||
mock_model = Mock()
|
||||
mock_model.profile = None
|
||||
mock_init_chat_model.return_value = mock_model
|
||||
|
||||
result = create_model("anthropic:claude-sonnet-4-5")
|
||||
|
||||
assert result.provider == "anthropic"
|
||||
assert result.model_name == "claude-sonnet-4-5"
|
||||
|
||||
def test_trailing_colon_raises_error(self) -> None:
|
||||
"""Trailing colon (e.g., 'anthropic:') raises ModelConfigError."""
|
||||
with pytest.raises(ModelConfigError, match="model name is required"):
|
||||
@@ -4749,6 +4871,34 @@ class TestDetectProvider:
|
||||
("o1-preview", "openai"),
|
||||
("o3-mini", "openai"),
|
||||
("o4-mini", "openai"),
|
||||
("text-davinci-003", "openai"),
|
||||
("command-r-plus", "cohere"),
|
||||
("amazon.titan-text-express-v1", "bedrock"),
|
||||
("anthropic.claude-3-sonnet", "bedrock"),
|
||||
("meta.llama3-70b-instruct-v1:0", "bedrock"),
|
||||
# Bedrock vendor namespaces that collide with the bare direct-API
|
||||
# prefixes below: the dotted form must win so the `:version` suffix
|
||||
# is not misparsed as a `provider:model` separator.
|
||||
("mistral.mistral-large-2402-v1:0", "bedrock"),
|
||||
("deepseek.r1-v1:0", "bedrock"),
|
||||
("cohere.command-r-v1:0", "bedrock"),
|
||||
("ai21.jamba-1-5-large-v1:0", "bedrock"),
|
||||
("writer.palmyra-x5-v1:0", "bedrock"),
|
||||
# Structural detection covers vendors with no hardcoded entry.
|
||||
("qwen.qwen3-32b-v1:0", "bedrock"),
|
||||
("google.gemma-3-27b-v1:0", "bedrock"),
|
||||
# Cross-region inference-profile IDs front the vendor with a region.
|
||||
("us.anthropic.claude-3-5-sonnet-20241022-v2:0", "bedrock"),
|
||||
("eu.meta.llama3-2-3b-instruct-v1:0", "bedrock"),
|
||||
("apac.anthropic.claude-3-5-sonnet-20241022-v2:0", "bedrock"),
|
||||
("US.Anthropic.Claude-3-5-Sonnet-20241022-v2:0", "bedrock"),
|
||||
# A bare name that merely starts with a region token is not Bedrock.
|
||||
("useful-model", None),
|
||||
("mistral-large", "mistralai"),
|
||||
("mixtral-8x7b-instruct", "mistralai"),
|
||||
("deepseek-chat", "deepseek"),
|
||||
("grok-4", "xai"),
|
||||
("sonar-pro", "perplexity"),
|
||||
("claude-sonnet-4-5", "anthropic"),
|
||||
("claude-opus-4-5", "anthropic"),
|
||||
("gemini-3.1-pro-preview", "google_genai"),
|
||||
@@ -4763,7 +4913,7 @@ class TestDetectProvider:
|
||||
# is what anchors the match to the exact account namespace.
|
||||
("accounts/fireworks-enterprise/models/kimi-k2p7-code", None),
|
||||
("llama3", None),
|
||||
("mistral-large", None),
|
||||
("solar-pro", None),
|
||||
("some-unknown-model", None),
|
||||
],
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user