mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-24 16:54:43 +00:00
fix(moa): route Copilot slots by target model
This commit is contained in:
parent
4cb85fb7fc
commit
02f1dd0857
4 changed files with 86 additions and 16 deletions
|
|
@ -3649,19 +3649,11 @@ def copilot_model_api_mode(
|
|||
if _should_use_copilot_responses_api(normalized):
|
||||
return "codex_responses"
|
||||
|
||||
# Secondary: check catalog for non-GPT-5 models (Claude via /v1/messages, etc.)
|
||||
if catalog:
|
||||
catalog_entry = next((item for item in catalog if item.get("id") == normalized), None)
|
||||
if isinstance(catalog_entry, dict):
|
||||
supported_endpoints = {
|
||||
str(endpoint).strip()
|
||||
for endpoint in (catalog_entry.get("supported_endpoints") or [])
|
||||
if str(endpoint).strip()
|
||||
}
|
||||
# For non-GPT-5 models, check if they only support messages API
|
||||
if "/v1/messages" in supported_endpoints and "/chat/completions" not in supported_endpoints:
|
||||
return "anthropic_messages"
|
||||
|
||||
# Copilot's Claude models are exposed through its OpenAI-compatible chat
|
||||
# endpoint, not through Hermes' native Anthropic adapter. The live catalog may
|
||||
# advertise /v1/messages, but the Copilot token/header scheme is handled by
|
||||
# the OpenAI client path; selecting anthropic_messages would send the wrong
|
||||
# auth/wire shape. Keep non-GPT Copilot slots on chat_completions.
|
||||
return "chat_completions"
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -317,13 +317,24 @@ def _provider_supports_explicit_api_mode(provider: Optional[str], configured_pro
|
|||
return normalized_configured == normalized_provider
|
||||
|
||||
|
||||
def _copilot_runtime_api_mode(model_cfg: Dict[str, Any], api_key: str) -> str:
|
||||
def _copilot_runtime_api_mode(
|
||||
model_cfg: Dict[str, Any],
|
||||
api_key: str,
|
||||
*,
|
||||
target_model: Optional[str] = None,
|
||||
) -> str:
|
||||
configured_provider = str(model_cfg.get("provider") or "").strip().lower()
|
||||
configured_mode = _parse_api_mode(model_cfg.get("api_mode"))
|
||||
if configured_mode and _provider_supports_explicit_api_mode("copilot", configured_provider):
|
||||
return configured_mode
|
||||
|
||||
model_name = str(model_cfg.get("default") or "").strip()
|
||||
# Use the model being resolved for this runtime, not the persisted global
|
||||
# default. MoA slots, fallback models, and mid-session model switches all
|
||||
# resolve credentials for a target model that can differ from config.yaml's
|
||||
# model.default. If we derive Copilot api_mode from the stale default, a
|
||||
# Claude/Gemini MoA slot can inherit codex_responses from a GPT-5 default and
|
||||
# fail with "model ... does not support Responses API".
|
||||
model_name = str(target_model or model_cfg.get("default") or "").strip()
|
||||
if not model_name:
|
||||
return "chat_completions"
|
||||
|
||||
|
|
@ -449,7 +460,11 @@ def _resolve_runtime_from_pool_entry(
|
|||
api_mode = "chat_completions"
|
||||
base_url = _nous_inference_base_url_override() or base_url
|
||||
elif provider == "copilot":
|
||||
api_mode = _copilot_runtime_api_mode(model_cfg, getattr(entry, "runtime_api_key", ""))
|
||||
api_mode = _copilot_runtime_api_mode(
|
||||
model_cfg,
|
||||
getattr(entry, "runtime_api_key", ""),
|
||||
target_model=effective_model,
|
||||
)
|
||||
base_url = base_url or PROVIDER_REGISTRY["copilot"].inference_base_url
|
||||
elif provider == "azure-foundry":
|
||||
# Azure Foundry: read api_mode and base_url from config
|
||||
|
|
|
|||
23
tests/hermes_cli/test_copilot_model_api_mode.py
Normal file
23
tests/hermes_cli/test_copilot_model_api_mode.py
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
"""Tests for Copilot model API-mode routing."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
|
||||
def test_copilot_claude_stays_on_chat_completions_even_if_catalog_lists_messages():
|
||||
from hermes_cli.models import copilot_model_api_mode
|
||||
|
||||
catalog = [
|
||||
{
|
||||
"id": "claude-opus-4.8",
|
||||
"supported_endpoints": ["/v1/messages"],
|
||||
}
|
||||
]
|
||||
|
||||
assert copilot_model_api_mode("claude-opus-4.8", catalog=catalog) == "chat_completions"
|
||||
|
||||
|
||||
def test_copilot_gpt5_still_uses_responses_api():
|
||||
from hermes_cli.models import copilot_model_api_mode
|
||||
|
||||
assert copilot_model_api_mode("gpt-5.5", catalog=[]) == "codex_responses"
|
||||
assert copilot_model_api_mode("gpt-5-mini", catalog=[]) == "chat_completions"
|
||||
40
tests/hermes_cli/test_copilot_runtime_api_mode.py
Normal file
40
tests/hermes_cli/test_copilot_runtime_api_mode.py
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
"""Regression tests for Copilot runtime api_mode resolution."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
|
||||
def test_copilot_runtime_api_mode_uses_target_model_over_stale_config_default(monkeypatch):
|
||||
"""MoA/fallback slots must derive Copilot api_mode from the slot model.
|
||||
|
||||
Repro: user's main/default Copilot model is GPT-5.5 (Responses API), but a
|
||||
MoA reference slot is Copilot Claude Opus (Chat Completions). Runtime
|
||||
resolution previously looked only at model.default and returned
|
||||
codex_responses for the Claude slot, causing Copilot to reject it with
|
||||
"model ... does not support Responses API".
|
||||
"""
|
||||
from hermes_cli import runtime_provider as rp
|
||||
|
||||
monkeypatch.setattr(
|
||||
"hermes_cli.models.copilot_model_api_mode",
|
||||
lambda model, api_key=None: "codex_responses" if str(model).startswith("gpt-5") else "chat_completions",
|
||||
)
|
||||
|
||||
assert rp._copilot_runtime_api_mode(
|
||||
{"provider": "copilot", "default": "gpt-5.5"},
|
||||
"token",
|
||||
target_model="claude-opus-4.8",
|
||||
) == "chat_completions"
|
||||
|
||||
|
||||
def test_copilot_runtime_api_mode_still_uses_default_without_target(monkeypatch):
|
||||
from hermes_cli import runtime_provider as rp
|
||||
|
||||
monkeypatch.setattr(
|
||||
"hermes_cli.models.copilot_model_api_mode",
|
||||
lambda model, api_key=None: "codex_responses" if str(model).startswith("gpt-5") else "chat_completions",
|
||||
)
|
||||
|
||||
assert rp._copilot_runtime_api_mode(
|
||||
{"provider": "copilot", "default": "gpt-5.5"},
|
||||
"token",
|
||||
) == "codex_responses"
|
||||
Loading…
Add table
Add a link
Reference in a new issue