Merge branch 'simp/r3-21-B' into simp/integration3
This commit is contained in:
+46
-75
@@ -356,10 +356,7 @@ class StartupModelRoute(NamedTuple):
|
||||
|
||||
|
||||
def resolve_startup_model_route(
|
||||
raw_model: str,
|
||||
*,
|
||||
explicit_provider: str = "",
|
||||
current_provider: str = "",
|
||||
raw_model: str, *, explicit_provider: str = "", current_provider: str = "",
|
||||
user_providers: Optional[dict] = None,
|
||||
custom_providers: Optional[list] = None) -> Optional[StartupModelRoute]:
|
||||
"""Resolve aliases and configured ``provider/model`` input at startup.
|
||||
@@ -477,18 +474,15 @@ def parse_model_flags_detailed(raw_args: str) -> ModelFlagParseResult:
|
||||
# require shell quoting.
|
||||
flags = dict.fromkeys(_BOOL_FLAGS.values(), False)
|
||||
explicit_provider = ""
|
||||
parts = raw_args.split()
|
||||
i = 0
|
||||
filtered: list[str] = []
|
||||
while i < len(parts):
|
||||
if parts[i] in _BOOL_FLAGS:
|
||||
flags[_BOOL_FLAGS[parts[i]]] = True
|
||||
elif parts[i] == "--provider" and i + 1 < len(parts):
|
||||
explicit_provider = parts[i + 1]
|
||||
i += 1
|
||||
tokens = iter(raw_args.split())
|
||||
for tok in tokens:
|
||||
if tok in _BOOL_FLAGS:
|
||||
flags[_BOOL_FLAGS[tok]] = True
|
||||
elif tok == "--provider" and (value := next(tokens, None)) is not None:
|
||||
explicit_provider = value
|
||||
else:
|
||||
filtered.append(parts[i])
|
||||
i += 1
|
||||
filtered.append(tok) # a trailing bare ``--provider`` stays part of the model text
|
||||
return ModelFlagParseResult(model_input=" ".join(filtered).strip(), explicit_provider=explicit_provider, **flags)
|
||||
|
||||
|
||||
@@ -796,14 +790,9 @@ def _resolve_alias_fallback(
|
||||
|
||||
|
||||
def resolve_display_context_length(
|
||||
model: str,
|
||||
provider: str,
|
||||
base_url: str = "",
|
||||
api_key: str = "",
|
||||
model_info: Optional[ModelInfo] = None,
|
||||
custom_providers: list | None = None,
|
||||
config_context_length: int | None = None,
|
||||
configured_model: str | None = None,
|
||||
model: str, provider: str, base_url: str = "", api_key: str = "",
|
||||
model_info: Optional[ModelInfo] = None, custom_providers: list | None = None,
|
||||
config_context_length: int | None = None, configured_model: str | None = None,
|
||||
configured_provider: str | None = None,
|
||||
configured_base_url: str | None = None) -> Optional[int]:
|
||||
"""Context length to show in /model output.
|
||||
@@ -867,12 +856,10 @@ def _configured_provider_matches(
|
||||
|
||||
matches: dict[str, str] = {}
|
||||
for slug, cfg in candidates:
|
||||
if slug in matches:
|
||||
continue
|
||||
hit = next((mid for key in ("models", "model", "default_model")
|
||||
for mid in _declared_model_ids(cfg.get(key)) if mid.lower() == target), None)
|
||||
if hit:
|
||||
matches[slug] = hit
|
||||
matches.setdefault(slug, hit) # first declaration wins
|
||||
return matches
|
||||
|
||||
|
||||
@@ -901,14 +888,6 @@ def _resolve_named_custom_model_id(model_name: str, target_provider: str, custom
|
||||
|
||||
# --- Core model-switching pipeline
|
||||
|
||||
def _runtime_creds(fallback_headers: dict, **kwargs) -> tuple[str, str, str, dict]:
|
||||
"""``resolve_runtime_provider`` unpacked as ``(api_key, base_url, api_mode, extra_headers)``;
|
||||
``extra_headers`` falls back to *fallback_headers*."""
|
||||
from hermes_cli.runtime_provider import resolve_runtime_provider
|
||||
rt = resolve_runtime_provider(**kwargs)
|
||||
return rt.get("api_key", ""), rt.get("base_url", ""), rt.get("api_mode", ""), rt.get("extra_headers") or fallback_headers
|
||||
|
||||
|
||||
def _entry_configured_key(cfg: dict, read_env) -> str:
|
||||
"""Inline ``api_key`` (a ``${VAR}`` template resolves via *read_env*), else
|
||||
``key_env``/``api_key_env`` via *read_env*."""
|
||||
@@ -1001,59 +980,53 @@ def _config_declares_model(
|
||||
return False
|
||||
|
||||
|
||||
def _apply_direct_alias_endpoint(
|
||||
da: DirectAlias, target_provider: str, new_model: str, api_key: str, base_url: str,
|
||||
) -> tuple[str, str, dict | None, bool]:
|
||||
"""Route a direct alias to its own base_url and decide its credential.
|
||||
def _apply_direct_alias_endpoint(st: "_Switch", da: DirectAlias) -> None:
|
||||
"""Route a direct alias to its own base_url and decide its credential (mutates ``st``).
|
||||
|
||||
Returns ``(api_key, base_url, validation_headers_override, suppress_ollama_headers)``; a
|
||||
``None`` headers override means "leave as is". Credentials were resolved against the DEFAULT
|
||||
provider; carrying that key onto the alias endpoint both 401s and ships the default provider's
|
||||
secret to an unrelated host. The alias's own endpoint decides: its declared key; else the
|
||||
session key only for the SAME ORIGIN; else a fresh resolution against the alias base_url
|
||||
(env-key fallbacks are host-gated: OLLAMA_API_KEY resolves for ollama.com, OPENROUTER_API_KEY
|
||||
never reaches an unrelated host).
|
||||
Credentials were resolved against the DEFAULT provider; carrying that key onto the alias
|
||||
endpoint both 401s and ships the default provider's secret to an unrelated host. The alias's
|
||||
own endpoint decides: its declared key; else the session key only for the SAME ORIGIN; else a
|
||||
fresh resolution against the alias base_url (env-key fallbacks are host-gated: OLLAMA_API_KEY
|
||||
resolves for ollama.com, OPENROUTER_API_KEY never reaches an unrelated host).
|
||||
"""
|
||||
from hermes_cli.models import _same_ollama_native_root
|
||||
from hermes_cli.runtime_provider import resolve_runtime_provider
|
||||
alias_key = direct_alias_api_key(da)
|
||||
same_host = _may_reuse_session_credential(st.base_url, da.base_url)
|
||||
if alias_key:
|
||||
base_url, api_key = da.base_url, alias_key
|
||||
elif api_key and api_key != "no-key-required" and _may_reuse_session_credential(base_url, da.base_url):
|
||||
st.base_url, st.api_key = da.base_url, alias_key
|
||||
elif st.api_key and st.api_key != "no-key-required" and same_host:
|
||||
# Same origin: the key is host-appropriate and re-resolving would only repeat the work.
|
||||
base_url = da.base_url
|
||||
st.base_url = da.base_url
|
||||
else:
|
||||
try:
|
||||
req, explicit = direct_alias_runtime_request(da)
|
||||
alias_runtime = resolve_runtime_provider(
|
||||
requested=req, explicit_api_key=explicit, explicit_base_url=da.base_url, target_model=new_model,
|
||||
)
|
||||
requested=req, explicit_api_key=explicit, explicit_base_url=da.base_url, target_model=st.new_model)
|
||||
except Exception:
|
||||
alias_runtime = {}
|
||||
same_host = _may_reuse_session_credential(base_url, da.base_url)
|
||||
base_url = alias_runtime.get("base_url", "") or da.base_url
|
||||
st.base_url = alias_runtime.get("base_url", "") or da.base_url
|
||||
# The resolver reports "no key found" as the `no-key-required` placeholder; normalise so
|
||||
# a same-host credential still outranks it.
|
||||
resolved_key = alias_runtime.get("api_key", "")
|
||||
if resolved_key == "no-key-required":
|
||||
resolved_key = ""
|
||||
api_key = resolved_key or (api_key if same_host else "") or "no-key-required"
|
||||
st.api_key = resolved_key or (st.api_key if same_host else "") or "no-key-required"
|
||||
|
||||
headers_override = None
|
||||
suppress = False
|
||||
# providers.ollama refinement: pick up the configured key only for the configured native
|
||||
# root; drop key and provider-level headers for any other origin. Skipped when the alias
|
||||
# declared its own credential (explicit api_key/key_env outranks a provider-level config key).
|
||||
if not alias_key and target_provider.strip().lower() == "ollama":
|
||||
if not alias_key and st.target_provider.strip().lower() == "ollama":
|
||||
ollama_cfg, ollama_cfg_base = _ollama_configured_base()
|
||||
if ollama_cfg_base and _same_ollama_native_root(base_url, ollama_cfg_base):
|
||||
if ollama_cfg_base and _same_ollama_native_root(st.base_url, ollama_cfg_base):
|
||||
configured_key = _entry_configured_key(ollama_cfg, lambda n: os.environ.get(n, "").strip())
|
||||
if configured_key:
|
||||
api_key = configured_key
|
||||
st.api_key = configured_key
|
||||
else:
|
||||
# Different origin, or no configured root to safely associate the headers with.
|
||||
headers_override, suppress, api_key = {}, True, "no-key-required"
|
||||
return api_key or "no-key-required", base_url, headers_override, suppress
|
||||
st.validation_headers, st.suppress_ollama_headers, st.api_key = {}, True, "no-key-required"
|
||||
st.api_key = st.api_key or "no-key-required"
|
||||
st.api_mode = "" # clear so determine_api_mode re-detects from URL
|
||||
|
||||
|
||||
def _moa_default_preset() -> str:
|
||||
@@ -1105,6 +1078,14 @@ class _Switch:
|
||||
def provider_changed(self) -> bool:
|
||||
return self.target_provider != self.current_provider
|
||||
|
||||
def resolve_runtime(self, **kwargs) -> None:
|
||||
"""Fill api_key / base_url / api_mode / validation_headers from ``resolve_runtime_provider``
|
||||
for ``new_model``; headers keep their current value when the resolver returns none."""
|
||||
from hermes_cli.runtime_provider import resolve_runtime_provider
|
||||
rt = resolve_runtime_provider(target_model=self.new_model, **kwargs)
|
||||
self.api_key, self.base_url, self.api_mode = rt.get("api_key", ""), rt.get("base_url", ""), rt.get("api_mode", "")
|
||||
self.validation_headers = rt.get("extra_headers") or self.validation_headers
|
||||
|
||||
|
||||
def _route_explicit_provider(st: _Switch) -> Optional[ModelSwitchResult]:
|
||||
"""PATH A (``--provider`` given): resolve the provider, auto-detect a model from a local
|
||||
@@ -1299,11 +1280,9 @@ def _creds_for_switched_provider(st: _Switch) -> Optional[ModelSwitchResult]:
|
||||
ukey = _entry_configured_key(ucfg, _scoped_key_env)
|
||||
st.validation_headers = _extra_headers_from_config(ucfg)
|
||||
try:
|
||||
api_key, base_url, st.api_mode, st.validation_headers = _runtime_creds(
|
||||
st.validation_headers, requested=st.target_provider, explicit_api_key=ukey or None,
|
||||
explicit_base_url=user_pdef.base_url, target_model=st.new_model)
|
||||
st.api_key = api_key or ukey
|
||||
st.base_url = base_url or user_pdef.base_url
|
||||
st.resolve_runtime(
|
||||
requested=st.target_provider, explicit_api_key=ukey or None, explicit_base_url=user_pdef.base_url)
|
||||
st.api_key, st.base_url = st.api_key or ukey, st.base_url or user_pdef.base_url
|
||||
except Exception:
|
||||
st.api_key, st.base_url, st.api_mode = ukey, user_pdef.base_url, ""
|
||||
elif st.target_provider == "custom" and st.current_base_url:
|
||||
@@ -1311,8 +1290,7 @@ def _creds_for_switched_provider(st: _Switch) -> Optional[ModelSwitchResult]:
|
||||
st.api_mode = determine_api_mode(st.target_provider, st.base_url)
|
||||
else:
|
||||
try:
|
||||
st.api_key, st.base_url, st.api_mode, st.validation_headers = _runtime_creds(
|
||||
st.validation_headers, requested=st.target_provider, target_model=st.new_model)
|
||||
st.resolve_runtime(requested=st.target_provider)
|
||||
except Exception as e:
|
||||
return st.fail_on_target(f"Could not resolve credentials for provider '{st.provider_label}': {e}")
|
||||
return None
|
||||
@@ -1346,8 +1324,7 @@ def _creds_for_current_provider(st: _Switch) -> None:
|
||||
st.validation_headers = ollama_headers
|
||||
else:
|
||||
try:
|
||||
st.api_key, st.base_url, st.api_mode, st.validation_headers = _runtime_creds(
|
||||
st.validation_headers, requested=st.current_provider, target_model=st.new_model)
|
||||
st.resolve_runtime(requested=st.current_provider)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
@@ -1369,13 +1346,7 @@ def _resolve_switch_credentials(st: _Switch) -> Optional[ModelSwitchResult]:
|
||||
_ensure_direct_aliases()
|
||||
da = DIRECT_ALIASES.get(st.resolved_alias)
|
||||
if da is not None and da.base_url:
|
||||
st.api_key, st.base_url, headers_override, suppress = _apply_direct_alias_endpoint(
|
||||
da, st.target_provider, st.new_model, st.api_key, st.base_url)
|
||||
st.api_mode = "" # clear so determine_api_mode re-detects from URL
|
||||
if headers_override is not None:
|
||||
st.validation_headers = headers_override
|
||||
if suppress:
|
||||
st.suppress_ollama_headers = True
|
||||
_apply_direct_alias_endpoint(st, da)
|
||||
|
||||
# Fills an empty mode (alias cleared it) and overrides a STALE mode carried from previous
|
||||
# session state when the host mandates one wire protocol (e.g. gpt-5.x on api.openai.com
|
||||
|
||||
@@ -93,12 +93,8 @@ class _NativePickerModelList(list[str]):
|
||||
|
||||
|
||||
def _fetch_picker_live_models(
|
||||
api_key: str,
|
||||
api_url: str,
|
||||
native_catalog_provider: str,
|
||||
preserve_native_models: bool,
|
||||
headers: dict[str, str] | None = None,
|
||||
timeout: float = 5.0,
|
||||
api_key: str, api_url: str, native_catalog_provider: str, preserve_native_models: bool,
|
||||
headers: dict[str, str] | None = None, timeout: float = 5.0,
|
||||
api_mode: str | None = None) -> list[str] | None:
|
||||
"""Fetch picker models with native Ollama and cached generic discovery."""
|
||||
from hermes_cli.models import (
|
||||
@@ -602,12 +598,14 @@ class _PickerBuild:
|
||||
builtin_endpoints: set = field(default_factory=set)
|
||||
# (display_name, base_url) pairs from section 3 so section 4 skips overlapping rows.
|
||||
section3_pairs: set = field(default_factory=set)
|
||||
current_provider_norm: str = field(init=False)
|
||||
current_base_url_norm: str = field(init=False)
|
||||
|
||||
def __post_init__(self):
|
||||
self.current_provider_norm = self.current_provider.lower()
|
||||
self.current_base_url_norm = self.current_base_url.rstrip("/").lower()
|
||||
@property
|
||||
def current_provider_norm(self) -> str:
|
||||
return self.current_provider.lower()
|
||||
|
||||
@property
|
||||
def current_base_url_norm(self) -> str:
|
||||
return self.current_base_url.rstrip("/").lower()
|
||||
|
||||
def can_probe_custom(self, *, row_is_current: bool) -> bool:
|
||||
return bool(self.probe_custom_providers or (self.probe_current_custom_provider and row_is_current))
|
||||
|
||||
Reference in New Issue
Block a user