hermes_cli/models_local.py::_api_key_from_provider_config and
hermes_cli/model_setup_flows_custom.py::_model_flow_named_custom still read
the variable named by key_env with raw os.getenv / os.environ.get. Under the
multiplexed gateway one process serves many profiles from one os.environ, so
the picker's endpoint probe and the native Ollama catalog headers could carry
a stale or another profile's value, while a key present only in the profile's
.env was not found (#67935). Every other credential read on the custom
provider path (runtime_provider_custom, runtime_provider_backends) already
goes through agent.secret_scope.get_secret_str, which falls back to
os.environ for single-profile processes, so the CLI behaviour is unchanged.
`model.key_env` is not custom-only: the Desktop settings UI stores REGISTRY
provider keys there (e.g. HERMES_CUSTOM_LMSTUDIO_API_KEY with provider
lmstudio, #106336) and auth._model_level_key_env honours it. The previous
predicate (`not custom or route_changed`) therefore wiped that pointer on a
same-provider same-base_url model re-pick and silently broke the user's
credential. The pointer now clears ONLY when provider or base_url changed;
the inline api_key/api rule is unchanged.
Custom-endpoint activation (model_setup_flows_custom) popped base_url /
api_key but never key_env, so a stale pointer from a previous endpoint
outranked the credential it had just written — pop it alongside.
Tests: the same-route re-pick case now covers a registry provider (red on
the old predicate), and the two clear_model_endpoint_credentials tests are
folded into one invariant.
Leaving the context-length prompt blank in the custom-endpoint wizard said
"will auto-detect" and then went silent, so users could not tell whether their
endpoint runs on a detected window or the runtime's default fallback (which
shapes compression and prompt-cache behaviour). After the save prompt, run the
same resolver the runtime uses (with the endpoint's URL and key) and print
either "auto-detected N tokens" or "not detected — using the default N tokens".
Feedback only: the probe result is not persisted, and a failing probe never
blocks the save.
Fixes#2513. Approach from PR #2522 (@ygd58) and PR #85499 (@Luna161), both
written against the pre-decomposition wizard module.
Co-authored-by: Luna161 <268031236+Luna161@users.noreply.github.com>
`key_cmd` (#86891) authenticates a provider with a SHORT-LIVED bearer minted
by a command — SSO/OIDC brokers, cloud IAM, internal auth proxies. The
request path has honoured it since it landed, but the picker resolved probe
credentials from `api_key`/`key_env` ONLY, so a key_cmd provider probed
`/v1/models` with an EMPTY key.
Against an authenticated endpoint the probe 401s, discovery returns nothing,
and the provider falls back to its single configured default model. The
picker shows ONE model, indistinguishable from an endpoint that genuinely
serves one — while inference keeps working, because that path mints
correctly. Reproduced against a LiteLLM gateway behind Entra OIDC: 0 models
discovered with an empty key, 26 with the minted token.
Both picker probe sites already funnel through `_entry_credentials()`, so
the fix lands in one place: it now reports a `cmd:<key_cmd>` identity, and
each site falls back to `resolve_probe_token()` after api_key/key_env. An
explicit static key still wins, so existing configs are unaffected.
The identity is keyed on the COMMAND, never the minted token: the token
rotates on every refresh, so keying on its value would change the group
fingerprint constantly and force a re-probe on every open. Two entries on
one URL with different helpers still get distinct rows.
`resolve_probe_token()` lives in agent.command_token_source, which already
owns key_cmd minting, and shares the CommandTokenSource cache with the
request path — a cache read, not a fresh sign-in. Fail-closed: a helper
needing an interactive sign-in degrades to today's empty-key behaviour
rather than taking down every other provider's row.
`_model_flow_named_custom` (the `hermes model` setup flow) is the sibling
path — it builds its own `Authorization: Bearer` from the same incomplete
resolution — and is fixed the same way, with one ordering constraint: the
value persisted to config.yaml is computed BEFORE the mint, so a short-lived
bearer can never be written back to shadow the key_cmd meant to re-mint it.
Tests drive the real code paths and assert on the credential each probe
receives rather than on function source, so a semantics-preserving refactor
does not fail them. Verified they fail with the fix reverted.
No runtime consumer read the proxy (terminal_tool/environments call is_interrupted()/set_interrupt()
directly); its only users were tests patching tools.interrupt._interrupt_event, which had no effect on
the code under test. tools/terminal_tool.py's own re-export of the name is owned by another worker.
For each issue anchor present in BASE 63279301bc non-test .py and absent on HEAD, the BASE comment/docstring block was re-attached at the HEAD location of the code it explained (matched by the distinctive code line / enclosing def). Sentences already covered by an existing HEAD comment were deduped; the issue number always survives. Insert-only: no code lines changed.