The provider-agnostic half of PR #105863, so a CLI-driven subscription provider can ship as a standalone `kind: model-provider` plugin instead of a bundled one: - ProviderProfile: `native_reasoning_details_type`, `model_aliases`, `get_model_context_length`, `get_usage_cost`, `setup_status`, `discover_models` hooks (all default None / no-op). - Chat Completions transport: provider-native `reasoning_details` carriers follow only their declaring profile; standard records still replay on OpenRouter-style routes, strict routes drop the field wholesale (#70233). Relay/stream accumulate `delta.reasoning_details` verbatim. - `hermes model`: the generic plugin flow gates an external-process row on the CLI's own login status (inline `login_command` on a TTY), offers `discover_models()` rows with per-row notes, and never writes config when the executable is missing. - `/model` and the pickers: process providers list their live catalog merged with the pinned one, declared aliases/ids resolve inside the provider, and validation accepts a listed id without probing `process://`. - Delegation keeps the selected external-process provider and protocol for the child. - Model metadata / usage pricing consult the profile's bound and cost hooks first. - Desktop: `[1m]` renders as a "1M" tag and hyphenated Anthropic versions read "Haiku 4.5". The bespoke `_model_flow_external_process` and hard-coded `hermes_cli/main.py` paths from the PR were dropped in favour of main's `_model_flow_plugin_provider`. Co-authored-by: unsupportedpastels <unsupportedpastels@users.noreply.github.com>
54 lines
3.4 KiB
Python
54 lines
3.4 KiB
Python
"""`/model <alias>` stays on an external-process provider that declares the alias.
|
|
|
|
Process providers are not in models.dev, so ``list_provider_models`` returns ``[]`` and
|
|
the alias fallback used to route ``haiku`` to whichever authenticated HTTP provider was
|
|
handy (live repro: Copilot, then HTTP 400 on the next turn).
|
|
"""
|
|
from unittest.mock import patch
|
|
|
|
from hermes_cli.model_switch import switch_model
|
|
from providers.base import ProviderProfile
|
|
|
|
_ACCEPTED = {"accepted": True, "persist": True, "recognized": True, "message": None}
|
|
|
|
|
|
def _switch(raw_input, profile):
|
|
with patch("hermes_cli.model_switch.list_provider_models", return_value=[]), \
|
|
patch("providers.get_provider_profile", return_value=profile), \
|
|
patch("hermes_cli.model_switch.get_authenticated_provider_slugs", return_value=["copilot", "anthropic"]), \
|
|
patch("hermes_cli.models_validate.validate_requested_model", return_value=_ACCEPTED), \
|
|
patch("hermes_cli.models.detect_provider_for_model", return_value=("copilot", "claude-haiku-4.5")), \
|
|
patch("hermes_cli.model_switch.get_model_info", return_value=None), \
|
|
patch("hermes_cli.model_switch.get_model_capabilities", return_value=None), \
|
|
patch("hermes_cli.runtime_provider.resolve_runtime_provider",
|
|
return_value={"api_key": "external-process", "base_url": profile.base_url, "api_mode": "chat_completions"}):
|
|
return switch_model(raw_input=raw_input, current_provider=profile.name,
|
|
current_model="claude-sonnet-5[1m]", current_base_url=profile.base_url,
|
|
user_providers={}, custom_providers=[])
|
|
|
|
|
|
def test_alias_and_model_id_stay_on_external_process_provider():
|
|
profile = ProviderProfile(
|
|
name="proc-provider", auth_type="external_process", base_url="process://proc-provider",
|
|
fallback_models=("claude-sonnet-5[1m]", "claude-haiku-4-5-20251001", "claude-opus-5[1m]", "claude-opus-4-8[1m]"),
|
|
model_aliases={"opus": "claude-opus-5[1m]", "fable": "claude-fable-5-1[1m]"})
|
|
for typed, expected in (("haiku", "claude-haiku-4-5-20251001"), ("claude-opus-5[1m]", "claude-opus-5[1m]"),
|
|
("Claude-Opus-5", "claude-opus-5[1m]"), ("opus", "claude-opus-5[1m]"),
|
|
("fable", "claude-fable-5-1[1m]")):
|
|
result = _switch(typed, profile)
|
|
assert result.success, result.error_message
|
|
assert (result.target_provider, result.new_model) == (profile.name, expected), typed
|
|
|
|
|
|
def test_declared_process_model_validates_without_endpoint_warning():
|
|
from hermes_cli.models_validate import validate_requested_model
|
|
profile = ProviderProfile(
|
|
name="proc-provider", auth_type="external_process", base_url="process://proc-provider",
|
|
fallback_models=("claude-sonnet-5[1m]", "claude-haiku-4-5-20251001"))
|
|
with patch("providers.get_provider_profile", return_value=profile), \
|
|
patch("hermes_cli.models.fetch_api_models", side_effect=AssertionError("no HTTP probe for process://")):
|
|
verdict = validate_requested_model("claude-haiku-4-5-20251001", profile.name, base_url=profile.base_url)
|
|
assert verdict["accepted"] and verdict["recognized"] and not verdict.get("message")
|
|
unknown = validate_requested_model("claude-haiku-9", profile.name, base_url=profile.base_url)
|
|
assert "unreachable" not in (unknown.get("message") or "")
|