Download hermes_cli/model_switch.py from lenson78/hermes: direct link, hf CLI and curl.
- Browser
- Download file 7.77 kB
-
https://huggingface.co/spaces/lenson78/hermes/resolve/main/hermes_cli/model_switch.py
- Command line
-
hf download hf://spaces/lenson78/hermes/hermes_cli/model_switch.py
-
curl -L -o model_switch.py https://huggingface.co/spaces/lenson78/hermes/resolve/main/hermes_cli/model_switch.py
7.77 kB
| """Shared model-switching logic for CLI and gateway /model commands. | |
| Both the CLI (cli.py) and gateway (gateway/run.py) /model handlers | |
| share the same core pipeline: | |
| parse_model_input β is_custom detection β auto-detect provider | |
| β credential resolution β validate model β return result | |
| This module extracts that shared pipeline into pure functions that | |
| return result objects. The callers handle all platform-specific | |
| concerns: state mutation, config persistence, output formatting. | |
| """ | |
| from __future__ import annotations | |
| import os | |
| from dataclasses import dataclass, field | |
| from typing import Optional | |
| class ModelSwitchResult: | |
| """Result of a model switch attempt.""" | |
| success: bool | |
| new_model: str = "" | |
| target_provider: str = "" | |
| provider_changed: bool = False | |
| api_key: str = "" | |
| base_url: str = "" | |
| persist: bool = False | |
| error_message: str = "" | |
| warning_message: str = "" | |
| is_custom_target: bool = False | |
| provider_label: str = "" | |
| class CustomAutoResult: | |
| """Result of switching to bare 'custom' provider with auto-detect.""" | |
| success: bool | |
| model: str = "" | |
| base_url: str = "" | |
| api_key: str = "" | |
| error_message: str = "" | |
| def switch_model( | |
| raw_input: str, | |
| current_provider: str, | |
| current_base_url: str = "", | |
| current_api_key: str = "", | |
| ) -> ModelSwitchResult: | |
| """Core model-switching pipeline shared between CLI and gateway. | |
| Handles parsing, provider detection, credential resolution, and | |
| model validation. Does NOT handle config persistence, state | |
| mutation, or output formatting β those are caller responsibilities. | |
| Args: | |
| raw_input: The user's model input (e.g. "claude-sonnet-4", | |
| "zai:glm-5", "custom:local:qwen"). | |
| current_provider: The currently active provider. | |
| current_base_url: The currently active base URL (used for | |
| is_custom detection). | |
| current_api_key: The currently active API key. | |
| Returns: | |
| ModelSwitchResult with all information the caller needs to | |
| apply the switch and format output. | |
| """ | |
| from hermes_cli.models import ( | |
| parse_model_input, | |
| detect_provider_for_model, | |
| validate_requested_model, | |
| _PROVIDER_LABELS, | |
| ) | |
| from hermes_cli.runtime_provider import resolve_runtime_provider | |
| # Step 1: Parse provider:model syntax | |
| target_provider, new_model = parse_model_input(raw_input, current_provider) | |
| # Step 2: Detect if we're currently on a custom endpoint | |
| _base = current_base_url or "" | |
| is_custom = current_provider == "custom" or ( | |
| "localhost" in _base or "127.0.0.1" in _base | |
| ) | |
| # Step 3: Auto-detect provider when no explicit provider:model syntax | |
| # was used. Skip for custom providers β the model name might | |
| # coincidentally match a known provider's catalog. | |
| if target_provider == current_provider and not is_custom: | |
| detected = detect_provider_for_model(new_model, current_provider) | |
| if detected: | |
| target_provider, new_model = detected | |
| provider_changed = target_provider != current_provider | |
| # Step 4: Resolve credentials for target provider | |
| api_key = current_api_key | |
| base_url = current_base_url | |
| if provider_changed: | |
| try: | |
| runtime = resolve_runtime_provider(requested=target_provider) | |
| api_key = runtime.get("api_key", "") | |
| base_url = runtime.get("base_url", "") | |
| except Exception as e: | |
| provider_label = _PROVIDER_LABELS.get(target_provider, target_provider) | |
| if target_provider == "custom": | |
| return ModelSwitchResult( | |
| success=False, | |
| target_provider=target_provider, | |
| error_message=( | |
| "No custom endpoint configured. Set model.base_url " | |
| "in config.yaml, or set OPENAI_BASE_URL in .env, " | |
| "or run: hermes setup β Custom OpenAI-compatible endpoint" | |
| ), | |
| ) | |
| return ModelSwitchResult( | |
| success=False, | |
| target_provider=target_provider, | |
| error_message=( | |
| f"Could not resolve credentials for provider " | |
| f"'{provider_label}': {e}" | |
| ), | |
| ) | |
| else: | |
| # Gateway also resolves for unchanged provider to get accurate | |
| # base_url for validation probing. | |
| try: | |
| runtime = resolve_runtime_provider(requested=current_provider) | |
| api_key = runtime.get("api_key", "") | |
| base_url = runtime.get("base_url", "") | |
| except Exception: | |
| pass | |
| # Step 5: Validate the model | |
| try: | |
| validation = validate_requested_model( | |
| new_model, | |
| target_provider, | |
| api_key=api_key, | |
| base_url=base_url, | |
| ) | |
| except Exception: | |
| validation = { | |
| "accepted": True, | |
| "persist": True, | |
| "recognized": False, | |
| "message": None, | |
| } | |
| if not validation.get("accepted"): | |
| msg = validation.get("message", "Invalid model") | |
| return ModelSwitchResult( | |
| success=False, | |
| new_model=new_model, | |
| target_provider=target_provider, | |
| error_message=msg, | |
| ) | |
| # Step 6: Build result | |
| provider_label = _PROVIDER_LABELS.get(target_provider, target_provider) | |
| is_custom_target = target_provider == "custom" or ( | |
| base_url | |
| and "openrouter.ai" not in (base_url or "") | |
| and ("localhost" in (base_url or "") or "127.0.0.1" in (base_url or "")) | |
| ) | |
| return ModelSwitchResult( | |
| success=True, | |
| new_model=new_model, | |
| target_provider=target_provider, | |
| provider_changed=provider_changed, | |
| api_key=api_key, | |
| base_url=base_url, | |
| persist=bool(validation.get("persist")), | |
| warning_message=validation.get("message") or "", | |
| is_custom_target=is_custom_target, | |
| provider_label=provider_label, | |
| ) | |
| def switch_to_custom_provider() -> CustomAutoResult: | |
| """Handle bare '/model custom' β resolve endpoint and auto-detect model. | |
| Returns a result object; the caller handles persistence and output. | |
| """ | |
| from hermes_cli.runtime_provider import ( | |
| resolve_runtime_provider, | |
| _auto_detect_local_model, | |
| ) | |
| try: | |
| runtime = resolve_runtime_provider(requested="custom") | |
| except Exception as e: | |
| return CustomAutoResult( | |
| success=False, | |
| error_message=f"Could not resolve custom endpoint: {e}", | |
| ) | |
| cust_base = runtime.get("base_url", "") | |
| cust_key = runtime.get("api_key", "") | |
| if not cust_base or "openrouter.ai" in cust_base: | |
| return CustomAutoResult( | |
| success=False, | |
| error_message=( | |
| "No custom endpoint configured. " | |
| "Set model.base_url in config.yaml, or set OPENAI_BASE_URL " | |
| "in .env, or run: hermes setup β Custom OpenAI-compatible endpoint" | |
| ), | |
| ) | |
| detected_model = _auto_detect_local_model(cust_base) | |
| if not detected_model: | |
| return CustomAutoResult( | |
| success=False, | |
| base_url=cust_base, | |
| api_key=cust_key, | |
| error_message=( | |
| f"Custom endpoint at {cust_base} is reachable but no single " | |
| f"model was auto-detected. Specify the model explicitly: " | |
| f"/model custom:<model-name>" | |
| ), | |
| ) | |
| return CustomAutoResult( | |
| success=True, | |
| model=detected_model, | |
| base_url=cust_base, | |
| api_key=cust_key, | |
| ) | |