diff --git a/agent/auxiliary_client.py b/agent/auxiliary_client.py index 84ab7741982bc..b86f1fa5561c3 100644 --- a/agent/auxiliary_client.py +++ b/agent/auxiliary_client.py @@ -4465,6 +4465,26 @@ def _resolve_task_provider_model( resolved_model = model or cfg_model resolved_api_mode = cfg_api_mode + # When an explicit provider is given and no task-config api_mode override, + # fall back to the provider profile's declared api_mode. Without this, + # plugin providers that declare api_mode="anthropic_messages" (i.e. their + # upstream speaks the Anthropic Messages API, not OpenAI Chat Completions) + # silently land on the OpenAI-wire transport and 404 from + # `chat/completions` calls. The URL-suffix detection in + # `_endpoint_speaks_anthropic_messages` only catches /anthropic-suffixed + # gateways; it misses plain-base-URL Anthropic-API endpoints (e.g. local + # proxies on 127.0.0.1:PORT). Reading the profile here closes the gap so + # any provider plugin can declare api_mode and have it honored regardless + # of upstream URL shape. + if provider and not resolved_api_mode: + try: + from providers import get_provider_profile as _gpf_resolve + _profile = _gpf_resolve(provider) + if _profile and getattr(_profile, "api_mode", None): + resolved_api_mode = _profile.api_mode + except Exception: + pass + # Convenience aliases for direct API-key endpoints that aren't first-class # providers (e.g. ``provider: openai`` → custom + api.openai.com/v1). # Applied to both explicit args and config-derived values. When the user diff --git a/tests/agent/test_aux_resolver_api_mode.py b/tests/agent/test_aux_resolver_api_mode.py new file mode 100644 index 0000000000000..fb23ec96c613d --- /dev/null +++ b/tests/agent/test_aux_resolver_api_mode.py @@ -0,0 +1,45 @@ +"""Regression test for _resolve_task_provider_model consulting ProviderProfile.api_mode. + +When an explicit provider is given without a task-level api_mode override, the +resolver must fall back to the provider profile's declared api_mode so plugin +providers whose upstream speaks the Anthropic Messages API are wrapped with the +correct transport regardless of base-URL shape. Task config still wins. +""" + +from types import SimpleNamespace +from unittest.mock import patch + +import agent.auxiliary_client as ac + + +def test_resolver_falls_back_to_profile_api_mode(): + prof = SimpleNamespace(api_mode="anthropic_messages") + with patch("providers.get_provider_profile", return_value=prof): + provider, model, base_url, api_key, api_mode = ac._resolve_task_provider_model( + provider="myplugin" + ) + assert provider == "myplugin" + assert api_mode == "anthropic_messages" + + +def test_task_config_api_mode_still_wins_over_profile(): + prof = SimpleNamespace(api_mode="anthropic_messages") + with patch("providers.get_provider_profile", return_value=prof), \ + patch.object(ac, "_get_auxiliary_task_config", + return_value={"provider": "myplugin", "api_mode": "chat_completions"}): + # Explicit provider arg with explicit api_mode override would be cfg-driven; + # here we pass provider via task config so cfg_api_mode is set. + provider, model, base_url, api_key, api_mode = ac._resolve_task_provider_model( + task="compression" + ) + # cfg_api_mode set -> resolver must NOT overwrite it with the profile value. + assert api_mode == "chat_completions" + + +def test_resolver_no_profile_api_mode_leaves_none(): + prof = SimpleNamespace(api_mode=None) + with patch("providers.get_provider_profile", return_value=prof): + provider, model, base_url, api_key, api_mode = ac._resolve_task_provider_model( + provider="plainplugin" + ) + assert api_mode is None