fix(cli): hermes model treats local Ollama as keyless

The generic API-key flow's first step is the key prompt, so selecting
Ollama (local) dead-ended at 'No Ollama (local) API key configured.' —
a local server's credential is reachability, not a key.

- Skip the API-key prompt for ollama; the runtime substitutes the
  local-only placeholder bearer. A key for a reverse-proxied server
  still works via model.api_key.
- List the server's installed models live from /api/tags (like the
  LM Studio branch), with a start-the-server / pull-a-model hint when
  none are found.
- Honor a typed base URL override for providers without a base-url env
  var by carrying it into model.base_url, which the flow already writes.

The provider-catalog contract test exempts ollama from the every-
api_key-provider-exposes-an-env-var invariant for the same reason the
custom pseudo-provider is exempt: it is configured by server detection,
not a pasted credential.
This commit is contained in:
emozilla 2026-07-09 14:21:07 -04:00
parent 83f88909ef
commit cd16d9aec3
2 changed files with 32 additions and 10 deletions

View file

@ -2563,11 +2563,17 @@ def _model_flow_api_key_provider(config, provider_id, current_model=""):
if existing_key:
break
existing_key, abort = _prompt_api_key(
pconfig, existing_key, provider_id=provider_id
)
if abort:
return
if provider_id == "ollama":
# Local Ollama servers are unauthenticated — there is no key to
# prompt for. The runtime substitutes a placeholder bearer; a key
# for a reverse-proxied server can be set via model.api_key.
pass
else:
existing_key, abort = _prompt_api_key(
pconfig, existing_key, provider_id=provider_id
)
if abort:
return
# Gemini free-tier gate: free-tier daily quotas (<= 250 RPD for Flash)
# are exhausted in a handful of agent turns, so refuse to wire up the
@ -2659,14 +2665,18 @@ def _model_flow_api_key_provider(config, provider_id, current_model=""):
except (KeyboardInterrupt, EOFError):
print()
override = ""
if override and base_url_env:
if override:
if not override.startswith(("http://", "https://")):
print(
" Invalid URL — must start with http:// or https://. Keeping current value."
)
else:
elif base_url_env:
save_env_value(base_url_env, override)
effective_base = override
else:
# No env var for this provider (e.g. ollama): the override
# still takes effect via model.base_url, written below.
effective_base = override
# Model selection — resolution order:
# 1. models.dev registry (cached, filtered for agentic/tool-capable models)
@ -2690,6 +2700,15 @@ def _model_flow_api_key_provider(config, provider_id, current_model=""):
model_list = []
if model_list:
print(f" Found {len(model_list)} model(s) from LM Studio")
elif provider_id == "ollama":
from hermes_cli.models import fetch_ollama_local_models
model_list = fetch_ollama_local_models(base_url=effective_base, timeout=3.0)
if model_list:
print(f" Found {len(model_list)} installed model(s) on the Ollama server")
else:
print(f" No models found — is Ollama running at {effective_base}?")
print(" Pull one first (e.g. `ollama pull qwen3:8b`) or enter a name below.")
elif provider_id == "ollama-cloud":
from hermes_cli.models import fetch_ollama_cloud_models

View file

@ -95,11 +95,14 @@ def test_api_key_providers_expose_a_credential_env_var():
surface at least one env var to write the key into (otherwise the GUI can't
configure it).
Exemptions: ``aws_sdk`` (bedrock uses AWS_REGION/AWS_PROFILE) and the
Exemptions: ``aws_sdk`` (bedrock uses AWS_REGION/AWS_PROFILE); the
``custom`` bring-your-own-endpoint pseudo-provider, which is configured
inline via the local-endpoint flow rather than a fixed env var.
inline via the local-endpoint flow rather than a fixed env var; and
``ollama``, whose local server is unauthenticated by design the GUI
configures it via server detection, and a key for a reverse-proxied
server goes in model.api_key.
"""
exempt = {"custom"}
exempt = {"custom", "ollama"}
for d in provider_catalog():
if d.auth_type == "api_key" and d.slug not in exempt:
assert d.api_key_env_vars, f"{d.slug} is api_key but exposes no env var"