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:
Mason Daugherty
2026-07-13 03:19:52 -04:00
committed by GitHub
parent 2f8c11176f
commit 4ceed24d37
2 changed files with 218 additions and 9 deletions
+67 -8
View File
@@ -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:
+151 -1
View File
@@ -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),
],
)