feat(custom): prompt and persist explicit api_mode for custom providers

Adds an explicit API compatibility mode prompt to the `hermes model -> custom`
flow so Codex-compatible third-party endpoints (and any other non-default
backend whose URL doesn't match the existing heuristics in
`_detect_api_mode_for_url`) can be selected explicitly instead of silently
falling back to chat_completions.

Choices: Auto-detect / chat_completions / codex_responses / anthropic_messages.

Persists `api_mode` to:
  - `model.api_mode` (active session config)
  - the matching `custom_providers[*]` entry (so re-activating the named
    provider next time replays the same transport)

Salvaged from PR #6125 onto current main: kept the new prompt and the
`_save_custom_provider(api_mode=...)` plumbing; the named-custom flow
already extracts and applies `api_mode` from the saved entry on current
main so those changes are preserved as-is. Test fixtures updated for the
new prompt and the existing display-name prompt.

Co-authored-by: littlewwwhite <1095245867@qq.com>
This commit is contained in:
littlewwwhite 2026-05-13 08:46:01 -07:00 committed by Teknium
parent 1979ef5802
commit 6f2d1c88b7
4 changed files with 200 additions and 7 deletions

View file

@ -177,6 +177,40 @@ class TestProviderPersistsAfterModelSave:
assert model.get("api_mode") == "codex_responses"
assert config["agent"]["reasoning_effort"] == "high"
def test_named_custom_provider_preserves_explicit_api_mode(self, config_home):
"""Named custom providers should re-activate with their saved api_mode."""
import yaml
from hermes_cli.main import _model_flow_named_custom
provider_info = {
"name": "Packy",
"base_url": "https://packy.example.com/v1",
"api_key": "sk-test",
"model": "gpt-5.4",
"api_mode": "codex_responses",
}
# Patch fetch_api_models so the named custom flow returns one model;
# patch simple_term_menu to force the input() fallback; patch input to
# auto-select the first model from the fallback prompt.
from unittest.mock import MagicMock
fake_menu_module = MagicMock()
fake_menu_module.TerminalMenu.side_effect = OSError("no tty in test")
with patch("hermes_cli.auth._save_model_choice"), \
patch("hermes_cli.auth.deactivate_provider"), \
patch("hermes_cli.models.fetch_api_models", return_value=["gpt-5.4"]), \
patch.dict("sys.modules", {"simple_term_menu": fake_menu_module}), \
patch("builtins.input", return_value="1"):
_model_flow_named_custom({}, provider_info)
config = yaml.safe_load((config_home / "config.yaml").read_text()) or {}
model = config.get("model")
assert isinstance(model, dict)
assert model.get("provider") == "custom"
assert model.get("base_url") == "https://packy.example.com/v1"
assert model.get("api_mode") == "codex_responses"
def test_copilot_acp_provider_saved_when_selected(self, config_home):
"""_model_flow_copilot_acp should persist provider/base_url/model together."""
from hermes_cli.main import _model_flow_copilot_acp