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
51 changes: 49 additions & 2 deletions libs/code/deepagents_code/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -2645,6 +2645,7 @@ def _parse_extra_skills_dirs(
"nvidia_api_key",
"tavily_api_key",
"google_cloud_project",
"google_cloud_location",
"deepagents_langchain_project",
"project_root",
"shell_allow_list",
Expand Down Expand Up @@ -2696,6 +2697,9 @@ class Settings:
google_cloud_project: str | None
"""Google Cloud project ID for VertexAI authentication."""

google_cloud_location: str | None
"""Google Cloud region for Anthropic models on Vertex AI."""

deepagents_langchain_project: str | None
"""LangSmith project name for deepagents agent tracing."""

Expand Down Expand Up @@ -2810,6 +2814,7 @@ def from_environment(cls, *, start_path: Path | None = None) -> Settings:
nvidia_key = resolve_env_var("NVIDIA_API_KEY")
tavily_key = resolve_env_var("TAVILY_API_KEY")
google_cloud_project = resolve_env_var("GOOGLE_CLOUD_PROJECT")
google_cloud_location = resolve_env_var("GOOGLE_CLOUD_LOCATION")

# Detect LangSmith configuration
# DEEPAGENTS_CODE_LANGSMITH_PROJECT: Project for deepagents agent tracing
Expand Down Expand Up @@ -2901,6 +2906,7 @@ def from_environment(cls, *, start_path: Path | None = None) -> Settings:
nvidia_api_key=nvidia_key,
tavily_api_key=tavily_key,
google_cloud_project=google_cloud_project,
google_cloud_location=google_cloud_location,
deepagents_langchain_project=deepagents_langchain_project,
user_langchain_project=user_langchain_project,
project_root=project_root,
Expand Down Expand Up @@ -3059,6 +3065,9 @@ def _reload_values(
"nvidia_api_key": _resolve_env_var_from(env, "NVIDIA_API_KEY"),
"tavily_api_key": _resolve_env_var_from(env, "TAVILY_API_KEY"),
"google_cloud_project": _resolve_env_var_from(env, "GOOGLE_CLOUD_PROJECT"),
"google_cloud_location": _resolve_env_var_from(
env, "GOOGLE_CLOUD_LOCATION"
),
"deepagents_langchain_project": _resolve_env_var_from(
env,
LANGSMITH_PROJECT,
Expand Down Expand Up @@ -4773,7 +4782,7 @@ def detect_provider(model_name: str) -> str | None:
if model_lower.startswith("claude"):
s = _get_settings()
if not s.has_anthropic and s.has_vertex_ai:
return "google_vertexai"
return "google_anthropic_vertex"
return "anthropic"

if model_lower.startswith("gemini"):
Expand Down Expand Up @@ -4947,7 +4956,7 @@ def _get_provider_kwargs(
)
if api_key_env:
api_key = resolve_env_var(api_key_env)
if api_key:
if api_key and provider != "google_anthropic_vertex":
result["api_key"] = api_key

# `langchain-ollama` has no `api_key` kwarg; hosted Ollama (Cloud or
Expand Down Expand Up @@ -4991,6 +5000,32 @@ def _get_provider_kwargs(
return result


def _apply_google_anthropic_vertex_kwargs(
provider: str, kwargs: dict[str, Any]
) -> None:
"""Apply required Claude-on-Vertex project and location defaults.

Raises:
ModelConfigError: If no location is configured.
"""
if provider != "google_anthropic_vertex":
return
settings = _get_settings()
if settings.google_cloud_project:
kwargs.setdefault("project", settings.google_cloud_project)
if settings.google_cloud_location:
kwargs.setdefault("location", settings.google_cloud_location)
if not kwargs.get("location"):
from deepagents_code.model_config import ModelConfigError

msg = (
"Google Cloud location is required for provider "
"'google_anthropic_vertex'. Set GOOGLE_CLOUD_LOCATION or "
"DEEPAGENTS_CODE_GOOGLE_CLOUD_LOCATION, or pass 'location' in model params."
)
raise ModelConfigError(msg)


def _compose_openai_reasoning_effort(
provider: str,
kwargs: dict[str, Any],
Expand Down Expand Up @@ -5131,6 +5166,7 @@ def _create_model_via_init(
package_map = {
"anthropic": "langchain-anthropic",
"openai": "langchain-openai",
"google_anthropic_vertex": "langchain-google-vertexai",
"google_genai": "langchain-google-genai",
"google_vertexai": "langchain-google-vertexai",
"nvidia": "langchain-nvidia-ai-endpoints",
Expand Down Expand Up @@ -5368,6 +5404,15 @@ def create_model(
model_name = model_spec
provider = inferred_provider or ""

if provider == "google_vertexai" and model_name.lower().startswith("claude-"):
msg = (
f"Claude model '{model_name}' uses the Anthropic Messages API on "
"Vertex AI. Use "
f"'google_anthropic_vertex:{model_name}' instead of "
f"'google_vertexai:{model_name}'."
)
raise ModelConfigError(msg)

# Stored API keys (added via `/auth`) take effect by being copied onto
# the env var name LangChain reads. Apply before the credential check so
# `has_provider_credentials` and the downstream SDK see the same value.
Expand Down Expand Up @@ -5466,6 +5511,8 @@ def create_model(
if cli_max_retries is not None:
kwargs[_resolve_retry_param_name(provider)] = cli_max_retries

_apply_google_anthropic_vertex_kwargs(provider, kwargs)

# Check if this provider uses a custom BaseChatModel class
class_path = config.get_class_path(provider) if provider else None

Expand Down
14 changes: 10 additions & 4 deletions libs/code/deepagents_code/config_manifest.py
Original file line number Diff line number Diff line change
Expand Up @@ -1342,6 +1342,7 @@ def resolve_recursion_limit(
"cohere": ("langchain_cohere", "cohere"),
"deepseek": ("langchain_deepseek", "deepseek"),
"fireworks": ("langchain_fireworks", "fireworks"),
"google_anthropic_vertex": ("langchain_google_vertexai", "vertex"),
"google_genai": ("langchain_google_genai", "google-genai"),
"google_vertexai": ("langchain_google_vertexai", "vertex"),
"groq": ("langchain_groq", "groq"),
Expand Down Expand Up @@ -1468,12 +1469,8 @@ def _credential_options() -> tuple[ConfigOption, ...]:
from deepagents_code.model_config import PROVIDER_API_KEY_ENV

options: list[ConfigOption] = []
seen: set[str] = set()
sources = {**PROVIDER_API_KEY_ENV, **_EXTRA_CREDENTIAL_ENV}
for name, env_var in sorted(sources.items()):
if env_var in seen:
continue
seen.add(env_var)
redacted = _is_secret_env(env_var)
summary = (
f"Credential for the {name} provider."
Expand Down Expand Up @@ -1502,6 +1499,15 @@ def _credential_options() -> tuple[ConfigOption, ...]:
# drift test asserts every `DEEPAGENTS_CODE_*` constant in `_env_vars` appears
# here (or in `NON_OPTION_ENV_VARS`).
_STATIC_OPTIONS: tuple[ConfigOption, ...] = (
# --- Credentials ----------------------------------------------------
ConfigOption(
key="credentials.google_cloud_location",
group="Credentials",
summary="Google Cloud region for Anthropic models on Vertex AI.",
kind=OptionKind.NON_EMPTY_STR,
env_var="GOOGLE_CLOUD_LOCATION",
settings_field="google_cloud_location",
),
# --- Display / UI ---------------------------------------------------
ConfigOption(
key="display.charset",
Expand Down
1 change: 1 addition & 0 deletions libs/code/deepagents_code/cost_tracking.py
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@
_PROVIDER_ALIASES: dict[str, str] = {
"azure_openai": "azure",
"bedrock": "aws",
"google_anthropic_vertex": "google",
"google_genai": "google",
"google_vertexai": "google",
"mistralai": "mistral",
Expand Down
6 changes: 5 additions & 1 deletion libs/code/deepagents_code/model_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -635,6 +635,7 @@ def is_langsmith_gateway_host(host: str | None) -> bool:
"cohere": "COHERE_API_KEY",
"deepseek": "DEEPSEEK_API_KEY",
"fireworks": "FIREWORKS_API_KEY",
"google_anthropic_vertex": "GOOGLE_CLOUD_PROJECT",
"google_genai": "GOOGLE_API_KEY",
"google_vertexai": "GOOGLE_CLOUD_PROJECT",
"groq": "GROQ_API_KEY",
Expand Down Expand Up @@ -727,6 +728,7 @@ def is_langsmith_gateway_host(host: str | None) -> bool:
"bedrock": "max_retries",
"deepseek": "max_retries",
"fireworks": "max_retries",
"google_anthropic_vertex": "max_retries",
"google_genai": "max_retries",
"google_vertexai": "max_retries",
"groq": "max_retries",
Expand Down Expand Up @@ -853,7 +855,9 @@ def _canonical_base_url_env(provider: str) -> str | None:
return names[0] if names else None


IMPLICIT_AUTH_PROVIDERS: frozenset[str] = frozenset({"google_vertexai"})
IMPLICIT_AUTH_PROVIDERS: frozenset[str] = frozenset(
{"google_anthropic_vertex", "google_vertexai"}
)
"""Providers that support ambient auth outside app env-var checks.

These providers can authenticate without the env var listed in
Expand Down
1 change: 1 addition & 0 deletions libs/code/deepagents_code/tui/widgets/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,7 @@ def _region_for_endpoint(base_url: str) -> Region:
"cohere": "Cohere",
"deepseek": "DeepSeek",
"fireworks": "Fireworks",
"google_anthropic_vertex": "Google Vertex AI (Anthropic)",
"google_genai": "Google Gemini",
"google_vertexai": "Google Vertex AI",
"groq": "Groq",
Expand Down
100 changes: 98 additions & 2 deletions libs/code/tests/unit_tests/test_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -1350,6 +1350,7 @@ def test_resolve_retry_kwargs_global(self) -> None:
("bedrock", "max_retries"),
("deepseek", "max_retries"),
("fireworks", "max_retries"),
("google_anthropic_vertex", "max_retries"),
("google_genai", "max_retries"),
("google_vertexai", "max_retries"),
("groq", "max_retries"),
Expand Down Expand Up @@ -5546,6 +5547,79 @@ def test_opus_5_profile_sets_max_tokens_and_preserves_override(

assert result.model.model_dump()["max_tokens"] == expected

@pytest.mark.filterwarnings(
"ignore:Core Pydantic V1 functionality isn't compatible with Python 3.14"
)
def test_google_anthropic_vertex_passes_env_project_and_location(
self, monkeypatch: pytest.MonkeyPatch
) -> None:
"""Claude on Vertex resolves to `ChatAnthropicVertex` with env config."""
pytest.importorskip("langchain_google_vertexai")
import anthropic

import deepagents_code.config as config_module

monkeypatch.setenv("GOOGLE_CLOUD_PROJECT", "test-project")
monkeypatch.setenv("GOOGLE_CLOUD_LOCATION", "us-east5")
runtime_settings = Settings.from_environment()
monkeypatch.setattr(config_module, "_get_settings", lambda: runtime_settings)
sync_client = Mock()
async_client = Mock()
monkeypatch.setattr(anthropic, "AnthropicVertex", sync_client)
monkeypatch.setattr(anthropic, "AsyncAnthropicVertex", async_client)

result = create_model("google_anthropic_vertex:claude-sonnet-4-6")

assert result.provider == "google_anthropic_vertex"
assert result.model.__class__.__name__ == "ChatAnthropicVertex"
model_dump = result.model.model_dump()
assert model_dump["project"] == "test-project"
assert model_dump["location"] == "us-east5"
assert sync_client.call_args.kwargs["project_id"] == "test-project"
assert sync_client.call_args.kwargs["region"] == "us-east5"
assert async_client.call_args.kwargs["region"] == "us-east5"

@patch("langchain.chat_models.init_chat_model")
def test_google_anthropic_vertex_model_params_override_env(
self, mock_init_chat_model: Mock
) -> None:
"""Explicit model params outrank Google Cloud environment defaults."""
mock_init_chat_model.return_value = _make_init_chat_model_mock()
with (
patch.object(settings, "google_cloud_project", "env-project"),
patch.object(settings, "google_cloud_location", "us-east5"),
):
create_model(
"google_anthropic_vertex:claude-sonnet-4-6",
extra_kwargs={"project": "param-project", "location": "europe-west1"},
)

assert mock_init_chat_model.call_args.kwargs["project"] == "param-project"
assert mock_init_chat_model.call_args.kwargs["location"] == "europe-west1"

def test_google_anthropic_vertex_requires_location(self) -> None:
"""Missing Claude-on-Vertex location produces an actionable error."""
with (
patch.object(settings, "google_cloud_project", "test-project"),
patch.object(settings, "google_cloud_location", None),
pytest.raises(
ModelConfigError,
match=r"GOOGLE_CLOUD_LOCATION.*DEEPAGENTS_CODE_GOOGLE_CLOUD_LOCATION",
),
):
create_model("google_anthropic_vertex:claude-sonnet-4-6")

def test_google_vertexai_rejects_claude_models(self) -> None:
"""Claude model IDs fail fast on the incompatible Google transport."""
with pytest.raises(
ModelConfigError,
match=(
r"google_anthropic_vertex:claude-sonnet-4-6.*instead of "
r"'google_vertexai:claude-sonnet-4-6'"
),
):
create_model("google_vertexai:claude-sonnet-4-6")

@patch("langchain.chat_models.init_chat_model")
def test_none_extra_kwargs_is_noop(self, mock_init_chat_model: Mock) -> None:
"""extra_kwargs=None does not affect behavior."""
Expand Down Expand Up @@ -5756,6 +5830,28 @@ def test_missing_vertexai_package_uses_declared_extra(
assert exc_info.value.provider == "google_vertexai"
assert exc_info.value.package == "langchain-google-vertexai"

@patch("langchain.chat_models.init_chat_model")
def test_missing_anthropic_vertex_package_uses_declared_extra(
self, mock_init: Mock
) -> None:
"""Anthropic Vertex shares the `vertex` integration extra."""
from deepagents_code.model_config import MissingProviderPackageError

mock_init.side_effect = ImportError(
"No module named 'langchain_google_vertexai'"
)
with (
patch("importlib.util.find_spec", return_value=None),
patch(
"deepagents_code.extras_info.extra_for_package",
return_value="vertex",
),
pytest.raises(MissingProviderPackageError) as exc_info,
):
_create_model_via_init("claude-sonnet-4-6", "google_anthropic_vertex", {})

assert exc_info.value.package == "langchain-google-vertexai"

@patch("langchain.chat_models.init_chat_model")
def test_installed_but_broken_import(self, mock_init: Mock) -> None:
"""Shows real error when package is installed but import fails internally."""
Expand Down Expand Up @@ -5994,12 +6090,12 @@ def test_detect_known_patterns(self, model_name: str, expected: str | None) -> N
settings.google_api_key = None

def test_claude_falls_back_to_vertex_when_no_anthropic(self) -> None:
"""Claude models route to google_vertexai when only Vertex AI is configured."""
"""Claude models route to Anthropic Vertex when only Vertex is configured."""
settings.anthropic_api_key = None
settings.google_cloud_project = "my-project"
settings.google_api_key = None
try:
assert detect_provider("claude-sonnet-4-5") == "google_vertexai"
assert detect_provider("claude-sonnet-4-5") == "google_anthropic_vertex"
finally:
settings.google_cloud_project = None

Expand Down
19 changes: 15 additions & 4 deletions libs/code/tests/unit_tests/test_config_manifest.py
Original file line number Diff line number Diff line change
Expand Up @@ -923,7 +923,11 @@ def test_provider_package_name_differs_from_extra() -> None:
`provider_package_name` feeds a `pypi.org/project/...` link, so confusing
it with `provider_install_extra` would link an unrelated real project.
"""
assert provider_install_extra("google_anthropic_vertex") == "vertex"
assert provider_install_extra("google_vertexai") == "vertex"
assert (
provider_package_name("google_anthropic_vertex") == "langchain-google-vertexai"
)
assert provider_package_name("google_vertexai") == "langchain-google-vertexai"


Expand Down Expand Up @@ -974,11 +978,18 @@ def test_api_key_credentials_are_secret() -> None:
)


def test_google_cloud_project_is_not_secret() -> None:
"""The Vertex project identifier is not secret material and shows its value."""
opt = get_option("credentials.google_vertexai")
@pytest.mark.parametrize(
("key", "env_var"),
[
("credentials.google_cloud_location", "GOOGLE_CLOUD_LOCATION"),
("credentials.google_anthropic_vertex", "GOOGLE_CLOUD_PROJECT"),
],
)
def test_google_cloud_configuration_is_not_secret(key: str, env_var: str) -> None:
"""Google Cloud project and location identifiers are visible configuration."""
opt = get_option(key)
assert opt is not None
assert opt.env_var == "GOOGLE_CLOUD_PROJECT"
assert opt.env_var == env_var
assert opt.redacted is False


Expand Down
10 changes: 6 additions & 4 deletions libs/code/tests/unit_tests/test_model_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -1735,6 +1735,7 @@ def test_contains_major_providers(self):
assert PROVIDER_API_KEY_ENV["cohere"] == "COHERE_API_KEY"
assert PROVIDER_API_KEY_ENV["deepseek"] == "DEEPSEEK_API_KEY"
assert PROVIDER_API_KEY_ENV["fireworks"] == "FIREWORKS_API_KEY"
assert PROVIDER_API_KEY_ENV["google_anthropic_vertex"] == "GOOGLE_CLOUD_PROJECT"
assert PROVIDER_API_KEY_ENV["google_genai"] == "GOOGLE_API_KEY"
assert PROVIDER_API_KEY_ENV["google_vertexai"] == "GOOGLE_CLOUD_PROJECT"
assert PROVIDER_API_KEY_ENV["groq"] == "GROQ_API_KEY"
Expand Down Expand Up @@ -4739,11 +4740,12 @@ def test_ollama_optional_api_key_is_configured(self, tmp_path):
assert status.env_var == "OLLAMA_API_KEY"
assert legacy is True

def test_google_vertexai_missing_project_uses_implicit_auth(self):
"""Vertex AI should not fail just because GOOGLE_CLOUD_PROJECT is unset."""
@pytest.mark.parametrize("provider", ["google_anthropic_vertex", "google_vertexai"])
def test_vertex_missing_project_uses_implicit_auth(self, provider: str):
"""Vertex providers should allow ADC when project env vars are unset."""
with patch.dict("os.environ", {}, clear=True):
status = get_provider_auth_status("google_vertexai")
legacy = has_provider_credentials("google_vertexai")
status = get_provider_auth_status(provider)
legacy = has_provider_credentials(provider)

assert status.state is ProviderAuthState.IMPLICIT
assert legacy is True
Expand Down
Loading