fix(vision): forward custom-endpoint credentials in vision auto-detect
A custom:<name> main provider resolves at runtime to the bare provider id
"custom". In the vision auto-detect chain, the main-provider branch called
resolve_provider_client("custom", ...) WITHOUT explicit_base_url/api_key,
so it returned (None, None) ("no endpoint credentials found") and the whole
chain fell through to OpenRouter/Nous. A user on a custom endpoint with no
aggregator configured then got "No LLM provider configured for task=vision
provider=auto" on every image, even though their main model fully supports
vision.
Recover the live endpoint that set_runtime_main() records each turn
(_RUNTIME_MAIN_BASE_URL/_API_KEY/_API_MODE) and forward it to Step 1, with
a fallback to _resolve_custom_runtime() for non-gateway callers. Mirrors the
existing explicit-base_url branch directly above.
Adds TestResolveVisionCustomProvider covering custom, custom:<name>, and the
no-runtime fallback path.
This commit is contained in:
parent
8bf797f1c2
commit
25aa626cb4
2 changed files with 145 additions and 1 deletions
|
|
@ -543,6 +543,124 @@ class TestResolveVisionMainFirst:
|
|||
mock_strict.assert_called_once_with("nous", None)
|
||||
|
||||
|
||||
# ── Vision — custom provider endpoint credential passthrough ────────────────
|
||||
|
||||
|
||||
class TestResolveVisionCustomProvider:
|
||||
"""Custom-endpoint mains must forward base_url/api_key to Step 1.
|
||||
|
||||
Regression: a ``custom:<name>`` main provider resolves to the bare
|
||||
runtime provider id ``"custom"``. ``resolve_provider_client("custom")``
|
||||
has no built-in endpoint, so without forwarding the live base_url/api_key
|
||||
it returns ``(None, None)`` and vision falls through to OpenRouter / Nous,
|
||||
which an offline / aggregator-less user has never configured — breaking
|
||||
vision entirely with ``No LLM provider configured for task=vision
|
||||
provider=auto``. The fix recovers the live endpoint that
|
||||
``set_runtime_main()`` recorded for the turn.
|
||||
"""
|
||||
|
||||
def test_custom_main_forwards_runtime_endpoint(self, monkeypatch):
|
||||
"""custom main with recorded runtime endpoint → Step 1 builds a client."""
|
||||
import agent.auxiliary_client as aux
|
||||
|
||||
monkeypatch.setattr(aux, "_RUNTIME_MAIN_BASE_URL", "https://my.endpoint.example/v1")
|
||||
monkeypatch.setattr(aux, "_RUNTIME_MAIN_API_KEY", "sk-runtime-key")
|
||||
monkeypatch.setattr(aux, "_RUNTIME_MAIN_API_MODE", "anthropic_messages")
|
||||
|
||||
with patch(
|
||||
"agent.auxiliary_client._read_main_provider", return_value="custom",
|
||||
), patch(
|
||||
"agent.auxiliary_client._read_main_model", return_value="claude-opus-4-8",
|
||||
), patch(
|
||||
"agent.auxiliary_client._resolve_task_provider_model",
|
||||
return_value=("auto", None, None, None, None),
|
||||
), patch(
|
||||
"agent.auxiliary_client.resolve_provider_client"
|
||||
) as mock_resolve:
|
||||
mock_client = MagicMock()
|
||||
mock_resolve.return_value = (mock_client, "claude-opus-4-8")
|
||||
|
||||
from agent.auxiliary_client import resolve_vision_provider_client
|
||||
|
||||
provider, client, model = resolve_vision_provider_client()
|
||||
|
||||
assert provider == "custom"
|
||||
assert client is mock_client
|
||||
assert model == "claude-opus-4-8"
|
||||
# The endpoint credentials recorded for the turn MUST be forwarded,
|
||||
# otherwise resolve_provider_client("custom") returns (None, None).
|
||||
kwargs = mock_resolve.call_args.kwargs
|
||||
assert kwargs.get("explicit_base_url") == "https://my.endpoint.example/v1"
|
||||
assert kwargs.get("explicit_api_key") == "sk-runtime-key"
|
||||
assert kwargs.get("is_vision") is True
|
||||
|
||||
def test_custom_prefixed_main_forwards_runtime_endpoint(self, monkeypatch):
|
||||
"""A ``custom:<name>`` provider id also forwards the runtime endpoint."""
|
||||
import agent.auxiliary_client as aux
|
||||
|
||||
monkeypatch.setattr(aux, "_RUNTIME_MAIN_BASE_URL", "https://named.example/v1")
|
||||
monkeypatch.setattr(aux, "_RUNTIME_MAIN_API_KEY", "sk-named")
|
||||
monkeypatch.setattr(aux, "_RUNTIME_MAIN_API_MODE", "")
|
||||
|
||||
with patch(
|
||||
"agent.auxiliary_client._read_main_provider",
|
||||
return_value="custom:copilot-gateway",
|
||||
), patch(
|
||||
"agent.auxiliary_client._read_main_model", return_value="claude-opus-4-8",
|
||||
), patch(
|
||||
"agent.auxiliary_client._resolve_task_provider_model",
|
||||
return_value=("auto", None, None, None, None),
|
||||
), patch(
|
||||
"agent.auxiliary_client.resolve_provider_client"
|
||||
) as mock_resolve:
|
||||
mock_client = MagicMock()
|
||||
mock_resolve.return_value = (mock_client, "claude-opus-4-8")
|
||||
|
||||
from agent.auxiliary_client import resolve_vision_provider_client
|
||||
|
||||
provider, client, model = resolve_vision_provider_client()
|
||||
|
||||
assert provider == "custom:copilot-gateway"
|
||||
assert client is mock_client
|
||||
kwargs = mock_resolve.call_args.kwargs
|
||||
assert kwargs.get("explicit_base_url") == "https://named.example/v1"
|
||||
assert kwargs.get("explicit_api_key") == "sk-named"
|
||||
assert kwargs.get("is_vision") is True
|
||||
|
||||
def test_custom_main_no_runtime_falls_back_to_configured_endpoint(self, monkeypatch):
|
||||
"""No recorded runtime endpoint → resolve the configured custom endpoint."""
|
||||
import agent.auxiliary_client as aux
|
||||
|
||||
monkeypatch.setattr(aux, "_RUNTIME_MAIN_BASE_URL", "")
|
||||
monkeypatch.setattr(aux, "_RUNTIME_MAIN_API_KEY", "")
|
||||
monkeypatch.setattr(aux, "_RUNTIME_MAIN_API_MODE", "")
|
||||
|
||||
with patch(
|
||||
"agent.auxiliary_client._read_main_provider", return_value="custom",
|
||||
), patch(
|
||||
"agent.auxiliary_client._read_main_model", return_value="claude-opus-4-8",
|
||||
), patch(
|
||||
"agent.auxiliary_client._resolve_task_provider_model",
|
||||
return_value=("auto", None, None, None, None),
|
||||
), patch(
|
||||
"agent.auxiliary_client._resolve_custom_runtime",
|
||||
return_value=("https://configured.example/v1", "sk-configured", "chat_completions"),
|
||||
), patch(
|
||||
"agent.auxiliary_client.resolve_provider_client"
|
||||
) as mock_resolve:
|
||||
mock_client = MagicMock()
|
||||
mock_resolve.return_value = (mock_client, "claude-opus-4-8")
|
||||
|
||||
from agent.auxiliary_client import resolve_vision_provider_client
|
||||
|
||||
provider, client, model = resolve_vision_provider_client()
|
||||
|
||||
assert client is mock_client
|
||||
kwargs = mock_resolve.call_args.kwargs
|
||||
assert kwargs.get("explicit_base_url") == "https://configured.example/v1"
|
||||
assert kwargs.get("explicit_api_key") == "sk-configured"
|
||||
|
||||
|
||||
# ── Constant cleanup ────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue