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
14 changes: 9 additions & 5 deletions agent/auxiliary_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -4211,7 +4211,7 @@ def _finalize(resolved_provider: str, sync_client: Any, default_model: Optional[

def get_auxiliary_extra_body() -> dict:
"""Return extra_body kwargs for auxiliary API calls.

Includes Nous Portal product tags when the auxiliary client is backed
by Nous Portal. Returns empty dict otherwise.
"""
Expand All @@ -4220,7 +4220,7 @@ def get_auxiliary_extra_body() -> dict:

def auxiliary_max_tokens_param(value: int) -> dict:
"""Return the correct max tokens kwarg for the auxiliary client's provider.

OpenRouter and local models use 'max_tokens'. Direct OpenAI with newer
models (gpt-4o, o-series, gpt-5+) requires 'max_completion_tokens'.
The Codex adapter translates max_tokens internally, so we use max_tokens
Expand Down Expand Up @@ -4598,9 +4598,11 @@ def _resolve_task_provider_model(
3. "auto" (full auto-detection chain)

Returns (provider, model, base_url, api_key, api_mode) where model may
be None (use provider default). When base_url is set, provider is forced
to "custom" and the task uses that direct endpoint. api_mode is one of
"chat_completions", "codex_responses", or None (auto-detect).
be None (use provider default). A bare base_url is treated as a custom
endpoint, but an explicit first-class provider plus base_url keeps the
provider identity so provider-specific auth and headers still apply.
api_mode is one of "chat_completions", "codex_responses", or None
(auto-detect).
"""
cfg_provider = None
cfg_model = None
Expand Down Expand Up @@ -4638,6 +4640,8 @@ def _expand_direct_api_alias(prov: Optional[str], existing_base: Optional[str])
if cfg_provider:
cfg_provider, cfg_base_url = _expand_direct_api_alias(cfg_provider, cfg_base_url)

if provider and provider.strip().lower() != "auto":
return provider, resolved_model, base_url, api_key, resolved_api_mode
if base_url:
return "custom", resolved_model, base_url, api_key, resolved_api_mode
if provider:
Expand Down
86 changes: 86 additions & 0 deletions tests/agent/test_auxiliary_provider_input.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
"""Auxiliary provider/model input resolution regressions."""

from unittest.mock import MagicMock, patch

from agent.auxiliary_client import _resolve_task_provider_model


def test_explicit_copilot_base_url_preserves_provider_identity():
"""Copilot is first-class even though it uses an OpenAI-compatible URL."""
provider, model, base_url, api_key, api_mode = _resolve_task_provider_model(
provider="copilot",
model="gpt-5.4-mini",
base_url="https://api.githubcopilot.com",
)

assert provider == "copilot"
assert model == "gpt-5.4-mini"
assert base_url == "https://api.githubcopilot.com"
assert api_key is None
assert api_mode is None


def test_openai_alias_with_explicit_base_url_still_routes_as_custom():
"""The direct OpenAI alias remains custom because it is not first-class."""
provider, model, base_url, api_key, api_mode = _resolve_task_provider_model(
provider="openai",
model="gpt-4o-mini",
base_url="https://proxy.example.com/v1",
api_key="sk-test",
)

assert provider == "custom"
assert model == "gpt-4o-mini"
assert base_url == "https://proxy.example.com/v1"
assert api_key == "sk-test"
assert api_mode is None


def test_auto_provider_with_explicit_base_url_routes_as_custom():
"""Explicit auto provider must not override an explicit endpoint."""
provider, model, base_url, api_key, api_mode = _resolve_task_provider_model(
provider="auto",
model="gpt-4o-mini",
base_url="https://proxy.example.com/v1",
api_key="sk-test",
)

assert provider == "custom"
assert model == "gpt-4o-mini"
assert base_url == "https://proxy.example.com/v1"
assert api_key == "sk-test"
assert api_mode is None


def test_resolved_copilot_client_uses_copilot_credentials_and_headers():
"""The preserved provider identity reaches Copilot auth, not custom auth."""
from agent.auxiliary_client import resolve_provider_client

fake_client = MagicMock()
fake_headers = {
"Authorization": "Bearer ghu_test",
"Editor-Version": "Hermes/0.0",
}

with patch(
"hermes_cli.auth.resolve_api_key_provider_credentials",
return_value={
"api_key": "ghu_test",
"base_url": "https://api.githubcopilot.com",
},
), patch(
"hermes_cli.copilot_auth.copilot_request_headers",
return_value=fake_headers,
), patch("agent.auxiliary_client.OpenAI", return_value=fake_client) as openai:
client, resolved_model = resolve_provider_client(
"copilot",
model="gpt-5.4-mini",
explicit_base_url="https://api.githubcopilot.com",
raw_codex=True,
)

assert client is fake_client
assert resolved_model == "gpt-5.4-mini"
assert openai.call_args.kwargs["api_key"] == "ghu_test"
assert openai.call_args.kwargs["base_url"] == "https://api.githubcopilot.com"
assert openai.call_args.kwargs["default_headers"] == fake_headers