Skip to content
Open
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
20 changes: 20 additions & 0 deletions agent/auxiliary_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
45 changes: 45 additions & 0 deletions tests/agent/test_aux_resolver_api_mode.py
Original file line number Diff line number Diff line change
@@ -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