From dbb434982d7d7b8fa1cd8a0e42628a664298e2c2 Mon Sep 17 00:00:00 2001 From: wqz666 Date: Mon, 1 Jun 2026 19:39:12 +0800 Subject: [PATCH] fix(auxiliary): use post-override base_url for endpoint detection resolve_provider_client() in agent/auxiliary_client.py used the raw provider-registry base_url when deciding whether to wrap the client as Anthropic-compatible. When a user overrode the base_url to an OpenAI- compatible endpoint (e.g. via custom_providers or a non-Anthropic proxy), the detector still saw the registry's hard-coded '/anthropic' suffix and forced the request through the Anthropic SDK, which then hit the real Anthropic endpoint with the user's key and returned 401. Fix: use the post-override base_url for endpoint-speak detection so the wrapping decision follows the URL the request will actually hit. api_mode='anthropic_messages' remains an explicit opt-in. Regression test: tests/agent/test_auxiliary_url_override_anthropic_detect.py covers three scenarios: 1. user-override to non-Anthropic URL keeps OpenAI client (the bug) 2. explicit api_mode=anthropic_messages still routes to Anthropic 3. OpenAI path normalises '/anthropic' suffix to '/v1' Reproducer: MiniMax auxiliary tasks (title generation, memory writer) returned 'HTTP 401: invalid api key (2049)' because the registry's anthropic-suffixed URL was preferred over the user's override. --- agent/auxiliary_client.py | 11 ++- ...auxiliary_url_override_anthropic_detect.py | 93 +++++++++++++++++++ 2 files changed, 103 insertions(+), 1 deletion(-) create mode 100644 tests/agent/test_auxiliary_url_override_anthropic_detect.py diff --git a/agent/auxiliary_client.py b/agent/auxiliary_client.py index 4c88772327f2..d62b44e5396d 100644 --- a/agent/auxiliary_client.py +++ b/agent/auxiliary_client.py @@ -3728,7 +3728,16 @@ def _wrap_if_needed(client_obj, final_model_str: str, base_url_str: str = "", # Anthropic-wire endpoints (Kimi Coding Plan api.kimi.com/coding, # /anthropic-suffixed gateways) so named providers like kimi-coding # land on the right transport without needing per-provider branches. - client = _wrap_if_needed(client, final_model, raw_base_url, api_key) + # Pass ``base_url`` (post-override) rather than ``raw_base_url`` so + # the Anthropic-detect heuristic respects the user's explicit + # endpoint. Otherwise a user who routes the ``minimax`` provider + # through their own OpenAI-compatible gateway (e.g. + # api.minimax.chat/v1) gets falsely detected as Anthropic-wire + # and wrapped in AnthropicAuxiliaryClient → 401 invalid_api_key + # (their key is not valid on the registry's hardcoded + # api.minimax.io/anthropic). Same trap for any provider with a + # hardcoded /anthropic inference URL. + client = _wrap_if_needed(client, final_model, base_url, api_key) logger.debug("resolve_provider_client: %s (%s)", provider, final_model) return (_to_async_client(client, final_model, is_vision=is_vision) if async_mode diff --git a/tests/agent/test_auxiliary_url_override_anthropic_detect.py b/tests/agent/test_auxiliary_url_override_anthropic_detect.py new file mode 100644 index 000000000000..11ba52f70fbb --- /dev/null +++ b/tests/agent/test_auxiliary_url_override_anthropic_detect.py @@ -0,0 +1,93 @@ +"""Regression: auxiliary client Anthropic-detect must respect user base_url override. + +The bug: ``resolve_provider_client`` built the OpenAI client with the user's +``explicit_base_url`` (e.g. ``https://api.minimax.chat/v1``) but then passed +the *raw* registry URL (e.g. ``https://api.minimax.io/anthropic``) to +``_wrap_if_needed``. ``_endpoint_speaks_anthropic_messages`` saw the +``/anthropic`` suffix and wrapped the client in ``AnthropicAuxiliaryClient``, +which then hit a real Anthropic endpoint with the user's key and 401'd. + +The fix: pass the post-override ``base_url`` to ``_wrap_if_needed`` so the +Anthropic heuristic sees what the user actually configured. + +If you re-introduce this regression, a user who routes the ``minimax`` +provider through their own OpenAI-compatible gateway will get +``HTTP 401: invalid api key`` from the auxiliary tasks. +""" + +import sys +import os + +sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "..")) + +from agent.auxiliary_client import resolve_provider_client + + +def test_user_base_url_override_skips_anthropic_wrap(): + """A user-supplied OpenAI-compatible base_url must NOT trigger the + Anthropic wire wrap, even when the provider registry's raw URL is the + /anthropic endpoint (minimax, minimax-cn, etc.).""" + client, resolved = resolve_provider_client( + provider="minimax", + model="MiniMax-M3", + explicit_base_url="https://api.minimax.chat/v1", + explicit_api_key="dummy-key-for-shape-test", + api_mode=None, # the original buggy path; # type: ignore[arg-type] + ) + assert client is not None + assert resolved == "MiniMax-M3" + assert type(client).__name__ == "OpenAI", ( + f"Expected plain OpenAI client (user base_url is OpenAI-compatible), " + f"got {type(client).__name__} — the Anthropic detect heuristic " + f"leaked the registry's hardcoded /anthropic URL into the wrap " + f"decision (regression of url-override-anthropic-detect bug)." + ) + # The actual base_url on the constructed client should be the user's URL, + # not the registry's hardcoded /anthropic URL. + actual = str(getattr(client, "base_url", "") or "") + assert actual.startswith("https://api.minimax.chat"), ( + f"client.base_url leaked: got {actual!r}, expected to start with " + f"'https://api.minimax.chat'" + ) + + +def test_user_explicit_anthropic_url_still_wraps_with_explicit_mode(): + """If the user genuinely wants the Anthropic wire transport, they + declare it with ``api_mode='anthropic_messages'`` and the wrap MUST + happen. The fix should not break this path.""" + client, resolved = resolve_provider_client( + provider="minimax", + model="MiniMax-M3", + explicit_base_url="https://api.minimax.io/anthropic", + explicit_api_key="dummy-key-for-shape-test", + api_mode="anthropic_messages", + ) + assert client is not None + # Should be wrapped in AnthropicAuxiliaryClient (the wire adapter) + assert type(client).__name__ in ("AnthropicAuxiliaryClient",), ( + f"Expected AnthropicAuxiliaryClient for api_mode=anthropic_messages, " + f"got {type(client).__name__}" + ) + + +def test_explicit_anthropic_url_without_mode_uses_openai_shape(): + """A URL ending in ``/anthropic`` gets normalized to ``/v1`` by + ``_to_openai_base_url`` (the OpenAI SDK needs ``/v1``). With no + ``api_mode`` override, the client is plain OpenAI — to actually + use the Anthropic wire, the user must set ``api_mode=anthropic_messages``. + This documents the contract so a future refactor doesn't silently + re-introduce the wrapping heuristic on /anthropic URLs.""" + client, resolved = resolve_provider_client( + provider="minimax", + model="MiniMax-M3", + explicit_base_url="https://api.minimax.io/anthropic", + explicit_api_key="dummy-key-for-shape-test", + api_mode=None, # type: ignore[arg-type] + ) + assert client is not None + assert type(client).__name__ == "OpenAI" + actual = str(getattr(client, "base_url", "") or "") + assert actual.endswith("/v1/") or actual.endswith("/v1"), ( + f"_to_openai_base_url should have normalised /anthropic -> /v1, " + f"got {actual!r}" + )