Skip to content
Closed
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
18 changes: 14 additions & 4 deletions agent/auxiliary_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -1855,9 +1855,9 @@ def _resolve_task_provider_model(
resolved_model = model or env_model or cfg_model

if base_url:
return "custom", resolved_model, base_url, api_key
return "custom", resolved_model, base_url, api_key or cfg_api_key
if provider:
return provider, resolved_model, base_url, api_key
return provider, resolved_model, base_url, api_key or cfg_api_key

if task:
env_base_url = _get_auxiliary_env_override(task, "BASE_URL")
Expand All @@ -1867,12 +1867,22 @@ def _resolve_task_provider_model(

env_provider = _get_auxiliary_provider(task)
if env_provider != "auto":
return env_provider, resolved_model, None, None
return env_provider, resolved_model, None, cfg_api_key

if cfg_base_url:
# OAuth providers ("nous", "openai-codex") read tokens from
# auth.json via their _try_*() handlers. Routing them through
# the "custom" handler bypasses that auth lookup and causes 401
# errors. For all other providers, preserve the existing
# behaviour of treating an explicit base_url as a custom
# endpoint (this keeps proxy setups working for OpenRouter,
# Anthropic, etc.).
_OAUTH_PROVIDERS = {"nous", "openai-codex", "codex"}
if cfg_provider in _OAUTH_PROVIDERS:
return cfg_provider, resolved_model, cfg_base_url, cfg_api_key
return "custom", resolved_model, cfg_base_url, cfg_api_key
Comment on lines 1872 to 1883

Copilot AI Apr 10, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new cfg_base_url handling changes the previously documented/advertised precedence where a configured base_url forces the request through the "custom" handler. With this change, any explicitly configured provider (including "main" and API-key providers) will now win even when base_url is set, and the base_url value will be effectively ignored by resolve_provider_client for non-"custom" providers. If the intent is only to avoid bypassing OAuth for specific providers (e.g., "nous" / "openai-codex"), consider narrowing this exception to just those providers (and keeping base_url→custom semantics for others), and update the function docstring/comments accordingly so config behavior matches docs.

Copilot uses AI. Check for mistakes.
if cfg_provider and cfg_provider != "auto":
return cfg_provider, resolved_model, None, None
return cfg_provider, resolved_model, None, cfg_api_key
return "auto", resolved_model, None, None

return "auto", resolved_model, None, None
Expand Down
152 changes: 152 additions & 0 deletions tests/agent/test_auxiliary_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -1305,3 +1305,155 @@ def test_402_with_no_fallback_reraises(self, monkeypatch):
task="compression",
messages=[{"role": "user", "content": "hello"}],
)


class TestCompressionOAuthProviderRouting:
"""Regression tests for OAuth provider routing in compression task resolution.

Covers the bug where summary_provider=nous + summary_base_url caused
the code to route through the 'custom' handler instead of _try_nous(),
bypassing OAuth auth from auth.json and causing 401 errors.
"""

def test_nous_with_summary_base_url_routes_to_nous_handler(
self, monkeypatch, tmp_path
):
"""compression.summary_provider=nous + summary_base_url must NOT
route to 'custom' handler — it must use the 'nous' handler which
reads OAuth tokens from auth.json."""
from agent.auxiliary_client import _resolve_task_provider_model

hermes_home = tmp_path / "hermes"
hermes_home.mkdir()
(hermes_home / "config.yaml").write_text(
"""compression:
summary_provider: nous
summary_model: xiaomi/mimo-v2-pro
summary_base_url: https://inference-api.nousresearch.com/v1
"""
)
monkeypatch.setenv("HERMES_HOME", str(hermes_home))

provider, model, base_url, api_key = _resolve_task_provider_model(
task="compression"
)
assert provider == "nous", (
f"Expected 'nous' handler (reads auth.json OAuth tokens), "
f"got '{provider}' which bypasses OAuth auth"
)
assert model == "xiaomi/mimo-v2-pro"

def test_openrouter_with_base_url_still_uses_custom(self, monkeypatch, tmp_path):
"""Non-OAuth providers (openrouter) with base_url must still route
through 'custom' handler — only OAuth providers get the exception."""
from agent.auxiliary_client import _resolve_task_provider_model

hermes_home = tmp_path / "hermes"
hermes_home.mkdir()
(hermes_home / "config.yaml").write_text(
"""compression:
summary_provider: openrouter
summary_model: google/gemini-3-flash-preview
summary_base_url: https://openrouter.ai/api/v1
"""
)
monkeypatch.setenv("HERMES_HOME", str(hermes_home))

provider, model, base_url, api_key = _resolve_task_provider_model(
task="compression"
)
assert provider == "custom", (
f"Non-OAuth provider with base_url should use 'custom' handler, "
f"got '{provider}'"
)

def test_codex_with_base_url_routes_to_codex_handler(
self, monkeypatch, tmp_path
):
"""openai-codex provider with base_url must route through the
codex handler (OAuth), not 'custom'."""
from agent.auxiliary_client import _resolve_task_provider_model

hermes_home = tmp_path / "hermes"
hermes_home.mkdir()
(hermes_home / "config.yaml").write_text(
"""compression:
summary_provider: openai-codex
summary_base_url: https://chatgpt.com/backend-api/codex
"""
)
monkeypatch.setenv("HERMES_HOME", str(hermes_home))

provider, model, base_url, api_key = _resolve_task_provider_model(
task="compression"
)
assert provider == "openai-codex"

def test_nous_without_base_url_still_works(self, monkeypatch, tmp_path):
"""nous provider without summary_base_url should still resolve
correctly (no backwards-compat block needed)."""
from agent.auxiliary_client import _resolve_task_provider_model

hermes_home = tmp_path / "hermes"
hermes_home.mkdir()
(hermes_home / "config.yaml").write_text(
"""auxiliary:
compression:
provider: nous
"""
)
monkeypatch.setenv("HERMES_HOME", str(hermes_home))

provider, model, base_url, api_key = _resolve_task_provider_model(
task="compression"
)
assert provider == "nous"
assert base_url is None

def test_cfg_api_key_passed_through_with_explicit_provider(
self, monkeypatch, tmp_path
):
"""When auxiliary.compression has provider + api_key set, the key
must be passed through (was previously dropped)."""
from agent.auxiliary_client import _resolve_task_provider_model

hermes_home = tmp_path / "hermes"
hermes_home.mkdir()
(hermes_home / "config.yaml").write_text(
"""auxiliary:
compression:
provider: openrouter
api_key: test-or-key-12345
"""
)
monkeypatch.setenv("HERMES_HOME", str(hermes_home))

provider, model, base_url, api_key = _resolve_task_provider_model(
task="compression"
)
assert provider == "openrouter"
assert api_key == "test-or-key-12345", (
"cfg_api_key was dropped — explicit provider path must pass it through"
)

def test_custom_provider_with_base_url_unchanged(self, monkeypatch, tmp_path):
"""provider=custom with base_url must behave exactly as before."""
from agent.auxiliary_client import _resolve_task_provider_model

hermes_home = tmp_path / "hermes"
hermes_home.mkdir()
(hermes_home / "config.yaml").write_text(
"""compression:
summary_provider: custom
summary_model: my-local-model
summary_base_url: http://localhost:8080/v1
"""
)
monkeypatch.setenv("HERMES_HOME", str(hermes_home))

provider, model, base_url, api_key = _resolve_task_provider_model(
task="compression"
)
assert provider == "custom"
assert base_url == "http://localhost:8080/v1"
assert model == "my-local-model"