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
|
|
@ -4940,9 +4940,35 @@ def resolve_vision_provider_client(
|
|||
main_provider,
|
||||
)
|
||||
else:
|
||||
# Custom endpoints (``custom`` / ``custom:<name>``) carry no
|
||||
# built-in base_url/api_key — resolve_provider_client("custom")
|
||||
# would return None ("no endpoint credentials found") and the
|
||||
# whole chain would fall through to the aggregators, breaking
|
||||
# vision for every user on a custom provider that has no
|
||||
# separate ``auxiliary.vision`` block. Recover the live main
|
||||
# endpoint that ``set_runtime_main()`` recorded for this turn so
|
||||
# Step 1 can build a working client.
|
||||
rpc_base_url = None
|
||||
rpc_api_key = None
|
||||
rpc_api_mode = resolved_api_mode
|
||||
if main_provider == "custom" or main_provider.startswith("custom:"):
|
||||
if _RUNTIME_MAIN_BASE_URL:
|
||||
rpc_base_url = _RUNTIME_MAIN_BASE_URL
|
||||
rpc_api_key = _RUNTIME_MAIN_API_KEY or None
|
||||
rpc_api_mode = resolved_api_mode or _RUNTIME_MAIN_API_MODE or None
|
||||
else:
|
||||
# No live runtime recorded (non-gateway caller): fall
|
||||
# back to resolving the configured custom endpoint.
|
||||
custom_base, custom_key, custom_mode = _resolve_custom_runtime()
|
||||
if custom_base:
|
||||
rpc_base_url = custom_base
|
||||
rpc_api_key = custom_key
|
||||
rpc_api_mode = resolved_api_mode or custom_mode or None
|
||||
rpc_client, rpc_model = resolve_provider_client(
|
||||
main_provider, vision_model,
|
||||
api_mode=resolved_api_mode,
|
||||
api_mode=rpc_api_mode,
|
||||
explicit_base_url=rpc_base_url,
|
||||
explicit_api_key=rpc_api_key,
|
||||
is_vision=True)
|
||||
if rpc_client is not None:
|
||||
logger.info(
|
||||
|
|
|
|||
|
|
@ -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