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
17 changes: 13 additions & 4 deletions agent/auxiliary_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -5251,12 +5251,18 @@ def _normalize_vision_provider(provider: Optional[str]) -> str:
def _resolve_strict_vision_backend(
provider: str,
model: Optional[str] = None,
explicit_api_key: Optional[str] = None,
) -> Tuple[Optional[Any], Optional[str]]:
provider = _normalize_vision_provider(provider)
if provider == "copilot":
return resolve_provider_client("copilot", model, is_vision=True)
if provider == "openrouter":
return _try_openrouter(model=model)
if explicit_api_key:
logger.debug(
"Strict vision: forwarding resolved key to OpenRouter (len=%d)",
len(explicit_api_key),
)
return _try_openrouter(model=model, explicit_api_key=explicit_api_key)
if provider == "nous":
return _try_nous(vision=True)
if provider == "openai-codex":
Expand All @@ -5283,6 +5289,7 @@ def _resolve_strict_vision_backend(
return resolve_provider_client("deepinfra", vision_model, is_vision=True)
if provider == "custom":
return _try_custom_endpoint()
logger.debug("Strict vision: no backend for provider %r", provider)
return None, None


Expand Down Expand Up @@ -5391,7 +5398,7 @@ def _finalize(resolved_provider: str, sync_client: Any, default_model: Optional[
vision_model = _resolve_provider_vision_default(main_provider) or main_model
if main_provider == "nous":
sync_client, default_model = _resolve_strict_vision_backend(
main_provider, vision_model
main_provider, vision_model, explicit_api_key=resolved_api_key
)
if sync_client is not None:
logger.info(
Expand Down Expand Up @@ -5473,7 +5480,9 @@ def _finalize(resolved_provider: str, sync_client: Any, default_model: Optional[
for candidate in _VISION_AUTO_PROVIDER_ORDER:
if candidate == main_provider:
continue # already tried above
sync_client, default_model = _resolve_strict_vision_backend(candidate)
sync_client, default_model = _resolve_strict_vision_backend(
candidate, explicit_api_key=resolved_api_key
)
if sync_client is not None:
return _finalize(candidate, sync_client, default_model)

Expand All @@ -5482,7 +5491,7 @@ def _finalize(resolved_provider: str, sync_client: Any, default_model: Optional[

if requested in _VISION_AUTO_PROVIDER_ORDER:
sync_client, default_model = _resolve_strict_vision_backend(
requested, resolved_model
requested, resolved_model, explicit_api_key=resolved_api_key
)
return _finalize(requested, sync_client, default_model)

Expand Down
112 changes: 110 additions & 2 deletions tests/agent/test_auxiliary_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -4168,7 +4168,7 @@ def test_kimi_coding_skipped_falls_through_to_openrouter(self, monkeypatch):
"agent.auxiliary_client.resolve_provider_client", rpc_mock,
)

def fake_strict(provider, model=None):
def fake_strict(provider, model=None, explicit_api_key=None):
Comment thread
LifeJiggy marked this conversation as resolved.
if provider == "openrouter":
return fake_or_client, "google/gemini-3-flash-preview"
if provider == "nous":
Expand Down Expand Up @@ -4204,7 +4204,7 @@ def test_kimi_coding_cn_skipped_too(self, monkeypatch):
)
monkeypatch.setattr(
"agent.auxiliary_client._resolve_strict_vision_backend",
lambda p, m=None: (fake_or_client, "gemini")
lambda p, m=None, explicit_api_key=None: (fake_or_client, "gemini")
if p == "openrouter"
else (None, None),
)
Expand Down Expand Up @@ -5653,3 +5653,111 @@ def _capture_create(**kwargs):
)

assert captured.get("api_key") == "no-key-required"


# ---------------------------------------------------------------------------
# Strict-vision key forwarding regression
# ---------------------------------------------------------------------------


class TestStrictVisionKeyForwarding:
"""Verify that a resolved vision API key is forwarded to OpenRouter
through _resolve_strict_vision_backend, rather than falling back to
the OPENROUTER_API_KEY environment variable.

Regression: _resolve_strict_vision_backend accepted explicit_api_key
but did not pass it to _try_openrouter, so the resolved key was silently
dropped and the env-var fallback was used instead.
"""

def test_explicit_key_forwarded_to_openrouter(self, monkeypatch):
"""A non-null explicit_api_key must reach _try_openrouter."""
from agent.auxiliary_client import _resolve_strict_vision_backend

captured_kwargs = {}

def fake_try_openrouter(**kwargs):
captured_kwargs.update(kwargs)
return MagicMock(name="openrouter_client"), "google/gemini-2.0-flash"

monkeypatch.setattr(
"agent.auxiliary_client._try_openrouter", fake_try_openrouter,
)
monkeypatch.delenv("OPENROUTER_API_KEY", raising=False)

result_client, result_model = _resolve_strict_vision_backend(
"openrouter", model="google/gemini-2.0-flash",
explicit_api_key="sk-or-v1-resolved-key-abc",
)

assert result_client is not None
assert captured_kwargs.get("explicit_api_key") == "sk-or-v1-resolved-key-abc"

def test_null_key_falls_back_to_env(self, monkeypatch):
"""When explicit_api_key is None, _try_openrouter uses env var."""
from agent.auxiliary_client import _resolve_strict_vision_backend

captured_kwargs = {}

def fake_try_openrouter(**kwargs):
captured_kwargs.update(kwargs)
return MagicMock(name="openrouter_client"), "model"

monkeypatch.setattr(
"agent.auxiliary_client._try_openrouter", fake_try_openrouter,
)
monkeypatch.setenv("OPENROUTER_API_KEY", "sk-or-v1-env-key")

result_client, _ = _resolve_strict_vision_backend(
"openrouter", explicit_api_key=None,
)

assert result_client is not None
assert captured_kwargs.get("explicit_api_key") is None

def test_empty_string_key_not_forwarded(self, monkeypatch):
"""An empty-string explicit_api_key should not mask the env var."""
from agent.auxiliary_client import _resolve_strict_vision_backend

captured_kwargs = {}

def fake_try_openrouter(**kwargs):
captured_kwargs.update(kwargs)
return MagicMock(name="openrouter_client"), "model"

monkeypatch.setattr(
"agent.auxiliary_client._try_openrouter", fake_try_openrouter,
)
monkeypatch.setenv("OPENROUTER_API_KEY", "sk-or-v1-env-key")

result_client, _ = _resolve_strict_vision_backend(
"openrouter", explicit_api_key="",
)

assert result_client is not None
assert captured_kwargs.get("explicit_api_key") == ""

def test_full_flow_resolve_to_openrouter_with_key(self, monkeypatch):
"""End-to-end: resolve_vision_provider_client with api_key='auto'
resolves a key and forwards it through _resolve_strict_vision_backend
to _try_openrouter."""
from agent.auxiliary_client import resolve_vision_provider_client

fake_client = MagicMock(name="openrouter_client")

monkeypatch.setattr(
"agent.auxiliary_client._resolve_task_provider_model",
lambda *a, **kw: ("openrouter", "google/gemini-2.0-flash", None, "sk-or-v1-resolved-xyz", None),
)
captured = {}
def fake_try_openrouter(**kwargs):
captured.update(kwargs)
return fake_client, "google/gemini-2.0-flash"
monkeypatch.setattr(
"agent.auxiliary_client._try_openrouter", fake_try_openrouter,
)

provider, client, model = resolve_vision_provider_client()
assert provider == "openrouter"
assert client is fake_client
assert captured.get("explicit_api_key") == "sk-or-v1-resolved-xyz"
2 changes: 1 addition & 1 deletion tests/agent/test_auxiliary_main_first.py
Original file line number Diff line number Diff line change
Expand Up @@ -568,7 +568,7 @@ def test_explicit_provider_override_still_wins(self):

# Explicit "nous" override → uses strict backend, NOT main model path
assert provider == "nous"
mock_strict.assert_called_once_with("nous", None)
mock_strict.assert_called_once_with("nous", None, explicit_api_key=None)


# ── Vision — custom provider endpoint credential passthrough ────────────────
Expand Down
Loading