Review follow-up on the TokenHub relay fix. The comment claimed the registry
endpoint counts only "when the profile leaves it empty"; the code (correctly)
counts both, and no provider has two different non-empty URLs. The set is now
built from two named values instead of an inline hanging comprehension.
Considered and not taken: reusing agent_init._provider_default_routes. It also
counts the other Moonshot region and Gemini's OpenAI-compat path as canonical,
which would turn a kimi-coding user pinned to the other region's host from a
relay probe into native discovery. That is a behaviour change for a separate PR.
#134320 gave tencent-tokenhub a profile whose base_url is empty (its auth registry row owns
the endpoint). _configured_relay_base_url decides "relay or canonical" from the profile's
base_url only, so a model.base_url that `hermes model` saved as TokenHub's normal endpoint
read as a relay: the picker probed {endpoint}/models and a successful reply replaced the
curated Hy list, dropping curated previews the live list lacks. Before #134320 the profile
lookup failed and the curated list stood.
The canonical endpoint is now the profile's OR the registry row's inference_base_url, both
through normalize_route_base_url. A different path on the same host is still a relay.
Reported by the review bot on #134320.
ProviderProfile gains `hidden` and `listed()`. A hidden profile is left
out of every surface that offers providers (the hermes model / setup
lists, /model and the TUI inventory, the provider catalog behind the
dashboard accounts tab, the hermes auth add picker) until the user signs
in by name and its credential pool holds a row. Resolution by name is
never gated, so --provider, model.provider and `hermes auth add <name>`
keep working.
This replaces the PR's global _HIDDEN_GATES registry plus a per-provider
enable function, which returned True unconditionally and showed the
provider to every user.
Per-user-quota OAuth inference for Hermes Agent, hidden by default until
public launch is cleared. Auth (OAuth2 + PKCE with the user's account) is
brokered by nous-account-service, which holds the confidential client secret;
inference goes direct to the per-user-quota endpoint.
Built fresh on current main; commit history carries only the codename.
Provider surface:
- plugins/model-providers/solstice/ — profile + hidden enable gate
- hermes_cli/solstice.py — PKCE loopback login, NAS-brokered config/exchange/
refresh, token store, quarantine; callback path unchanged
- agent/solstice_adapter.py — per-user-quota transport over the native
adapter's OpenAI-compatible facade
- providers/: _HIDDEN_GATES + register_hidden_provider_gate + a hidden filter
in list_providers (kept out of the picker/registry/doctor until enabled)
- wired into main.py (_PROVIDER_MODEL_FLOWS), auth_commands.py (_PROVIDER_ALIASES,
_OAUTH_ADD_SPECS, _is_known_provider allowlist), models.py (catalog fetcher),
runtime_provider.py (resolution rung), model_setup_flows.py, agent_runtime_helpers.py
Kept verbatim (the live wire contract, until the broker is renamed): the
inference host, the OAuth scope, the deployed broker routes, and the
registered redirect callback path.
The enable gate is unchanged (open item: gate on stored credentials).
Verified: 226 focused tests pass; runtime resolution, client construction
and auth-add all exercise the real paths.
The sign-in / re-auth picker (_pick_nous_model_after_login) built its own
paid list and skipped union_with_nous_on_sale_models, so a discounted model
the curated list and paidRecommendedModels omit never showed there. Apply
it after the paid recommendation union and before policy narrowing, as the
other two pickers do.
Also keep image/video generation rows out of the on-sale union: the Nous
pricing fetch flags them with the chat-catalog rule from 296ec08dcf.
A Nous sale exists only in the live /v1/models pricing.original. The paid
picker unions the curated manifest with the Portal's paidRecommendedModels,
so a model missing from both never appears even while it is the deepest
discount on the gateway (inception/mercury-2.5 at -80%); sale chrome only
badged rows that were already listed.
Paid-tier Nous pickers (hermes model and the GUI/in-chat picker) now append
every discounted, tool-capable, non-free row from the cached catalog,
deepest discount first, before org policy narrows the list. Free tier is
unchanged (freeRecommendedModels). The Nous pricing fetch marks rows whose
supported_parameters omit tools so the union can skip them.
Rebase onto current main and rework the recommended-default fix per
maintainer feedback on #51493 (teknium1 + DavidMetcalfe).
Premise of the original Opus-only filter rotted: current Nous curated
list leads with anthropic/claude-fable-5, then Opus. An Opus-only filter
would leave Fable selected and fail to establish cost-safe onboarding.
Current main already routes get_recommended_default_model through
pick_silent_default_model for both nous and non-nous providers, but the
helper still fell through to model_ids[0] when the catalog-labeled
preferred default wasn't present in the (Portal-augmented) list. That is
the remaining billing hole.
Changes:
- Add _is_anthropic_frontier_tier covering Opus + Fable, anchored on the
claude- prefix so community/distill slugs like qwopus3.6-27b-coder are
rejected and future releases (claude-opus-5-0, claude-fable-6) keep
matching without code changes.
- Harden pick_silent_default_model fallback:
preferred catalog label -> first non-frontier entry -> model_ids[0] -> empty.
Single shared policy used by both nous and openrouter callsites.
- Replace brittle Opus-first endpoint assumptions with:
parametrized opus_first / fable_first_main / override_beats_ordering,
falls_back_when_all_frontier, empty-list edge case, and unit coverage
for the frontier predicate.
web_server.py needs no further change -- it already consumes
pick_silent_default_model. Hardening the helper covers every consumer.
Fixes#51491
Maintainer decision on the FASTTIER review: drop the "Daybreak/cyber aliases
offer no speed tiers" rule this stack introduced. It flipped
model_supports_fast_mode to False for gpt-daybreak-* and gpt-5.6-cyber, which
also hid /fast and the Desktop Fast option for models that accepted Priority
before. Restore the pre-stack predicate verbatim, make the kept invariant
assert Daybreak is fast-but-not-ultrafast (the proxy route stays the no-speed
case) and remove the Daybreak sentences from the speed docs.
methods_session.py is already past 2000 lines, so the exact-tier parse that
#130990 grew in place moves with the rest of the composer overrides into
methods_session_model_guard.py (the session.create param gate), and the
strict "unknown word is an error, normal words pin ''" rule becomes
agent.fast_mode.parse_exact_service_tier next to the tier word table it
reads. The Daybreak/cyber exclusion folds into _is_openai_fast_model's
single return so hermes_cli/models.py does not grow.
Drops the PR's unused contextlib import in prompt_turn.py (no use was
added with it).
Co-authored-by: FroRaut <79215537+Froraut@users.noreply.github.com>
Ultrafast collapsed into ordinary Fast across the stack: the catalog only
reported `fast`, presets and drafts stored a boolean, session.create took
only `fast`, and Settings had a single Fast switch, so an Ultrafast default
read as Fast and a new chat could lose it.
- Inventory reports `ultrafast` per model from the same route-aware
resolver that builds the request; Daybreak aliases offer no accelerated
speed (their priority request is ignored).
- session.create accepts an exact `service_tier` that wins over the legacy
`fast` flag; unknown tiers are rejected.
- Desktop keeps the exact tier through presets, draft creation, session
options and labels; an unsupported remembered tier falls back to
Standard, and failed edits reconcile to the confirmed state.
- Settings → Model → Main model offers one Speed choice (Standard / Fast /
Ultrafast) on models that support Ultrafast; others keep the Fast switch.
Split out of #129251 (which keeps Daybreak).
(cherry picked from commit 4e332fb850c989ef9f34bd70aaca174042beecc6)
Follow-up to #131892. The live+static merge landed in curated_models_for_provider,
which has no production callers on main: every real picker surface (CLI/Desktop/
Telegram /model, hermes model) resolves through provider_model_ids, whose stepfun
fetcher (_api_key_provider_live) returned the live Step Plan list unmerged — so a
working Step Plan API still hid step-3.7-flash (#41147).
Apply the merge where the stepfun live catalog is fetched, following
_anthropic_catalog's _merge_unique pattern: a new _stepfun_catalog fetcher merges
live rows first with the curated floor (deduped via _model_dedup_key), and a declined
probe (no creds / failure) still falls through to the generic profile path whose
merge_profile_catalog already serves the curated placeholder. The setup wizard's
_model_flow_stepfun now offers the same merged list via provider_model_ids. The
redundant dead-path merge in curated_models_for_provider is removed; its stepfun
test keeps passing because the merge now flows through provider_model_ids.
Fixes#41147
The StepFun Step Plan API only returns 2 models (step-3.5-flash,
step-3.5-flash-2603), but step-3.7-flash is available via the Standard
API. When the configured base_url points to Step Plan, the live fetch
overwrites the static catalog and step-3.7-flash disappears from the
/model picker.
Fix: add step-3.7-flash to _PROVIDER_MODELS["stepfun"] and merge live
API results with the static catalog in curated_models_for_provider()
so all known models appear regardless of which API endpoint is used.
Fixes#41147
get_codex_model_ids returns CuratedFallbackModels directly instead of a
second codex-only marker that _codex_catalog re-wrapped; the class moves to
the static catalog leaf so codex_models can use it. The catalog identity
normalizes its route with normalize_route_base_url like every other route
key.
_anthropic_catalog() returned the curated list as a plain list when the live
/v1/models fetch failed, so a transient outage on a proxy base_url was cached
with the full 1h TTL as if it were the account's/proxy's real catalog, and
neither a same-credentials restart nor Refresh Models could shake it before the
TTL lapsed. Wrap the fallback return in CuratedFallbackModels, the placeholder
type #107391 introduced for the same class of bug.
Salvaged from #121248 (the Anthropic half; the Codex half lands via the
typed-fallback commit before this one).
A failed ChatGPT Codex catalog fetch returned the static offline hints as if
they were the account's catalog, so the picker cached them and dropped
account-gated rows such as GPT-6 Astra until the cache expired. The hints are
now typed (CodexFallbackModels) and cached as short-lived curated fallback
rows that never replace a verified account fetch, and the catalog cache key
includes the resolved route, not only the ChatGPT principal.
Treat openai-api as the first-party api.openai.com route in _fast_mode_route_supported. This closes the route gate introduced by c7e2e0b779 while preserving the existing host check, so proxies and aggregators remain excluded. The shared service_tier parser already accepts operator literals, including ultrafast, and the existing static and bounded-mode paths thread the selected literal through request_overrides.
OpenAI's Ultrafast service tier (service_tier="ultrafast") is broadly
available for GPT-6 Astra in the API and Codex (Pro 500 / Enterprise),
billed at 6x Standard. GPT-6.1 Sol Ultrafast is announced as coming soon.
- One tier table: agent.fast_mode.parse_service_tier / STATIC_TIERS now
back the CLI, gateway and TUI config loaders (three hand-copied parsers).
- resolve_fast_mode_overrides(tier="ultrafast") sends the tier only for
Ultrafast models on api.openai.com / chatgpt.com; anything else gets
nothing rather than a silent swap to another paid tier.
- /fast ultrafast on CLI, gateway (typed + picker when supported) and TUI;
static-tier attach sites pass the tier through.
- Pricing: the response's SERVED service_tier is folded into usage (Codex
stream assembler keeps it); served ultrafast prices from a published
6x row (272K whole-request tier included). A request asking for
Ultrafast but served at default bills at Standard.
- i18n: 4 new keys in all 17 locales; option lists mention ultrafast.
* refactor(plugins): remove the Sep 2026 decomposition compat layer on schedule
The PLUGIN-COMPAT layer (2776813df3 + d63e380324 + 0a5164cebe) kept pre-#102117 import paths
alive for external plugins until 2026-09-14. That window closed two weeks ago; since then the loader
has already been skipping plugins that use the old paths. This removes the layer itself:
- 328 appended `PLUGIN-COMPAT` blocks (lazy `__getattr__` pointer tables, re-exported third-party
names, restored dead definitions) and the three re-export stub modules
(gateway/startup_watchdog, hermes_cli/observability/relay_runtime, tools/environments/modal_utils)
- COMPAT_MANIFEST.md, compat_manifest.json, scripts/check_compat_pointers.py and its lint step
- the reporting surfaces: CLI banner notice, `hermes plugins compat`, the `hermes doctor` section,
the post-update notice, the Desktop one-time dialog, the loader's pre-import skip and the
`plugins.allow_deprecated_imports` escape hatch
An external plugin that still imports an old path now fails to load with its ImportError as the
reason in `hermes plugins list`, the same path as any broken plugin.
hermes_cli/plugin_compat.py stays as three inert stubs (compat_report, removal_in_effect,
summary_lines): an already-running pre-removal `hermes update` lazy-imports them after the checkout
swap (tests/compat/old_updater_surface.json).
In-tree fallout, both already dead: hermes_cli/setup.py::_check_espeak_ng (no callers; its
`shutil` came from a compat block) and gateway/config.py::SessionResetPolicy ("retained solely for
the scheduled plugin-compat window"). Two test_run_agent patches targeted the removed
`run_agent.handle_function_call` pointer; they now patch `model_tools.handle_function_call`, the
seam production reads, like every sibling test in that file.
* chore: retrigger CI (zero-job startup_failure phantom)
* test: drop resolution allowlist rows for the two deleted which() sites
hermes_cli/setup.py::_check_espeak_ng (dead) and tools/skillevaluator_scan.py::scanner_available
(a restored definition inside a PLUGIN-COMPAT block) no longer exist; the stale-row gate requires
their allowlist entries go with them.
Follow-up to the #103857 salvage. The Copilot provider profile and the
main-agent GitHub reasoning path each carried the same clamp-then-medium
fallback. Both now call hermes_cli.models.clamp_github_reasoning_effort.
The offline Astra tests move next to the other Copilot effort tests, along
with a check that a structured catalog entry still wins. The main-agent
clamp now has a test pinning max/ultra -> high on the GPT-5 ladder. The
transport test no longer writes config.yaml. The two effort comments now
say the same thing.
Follow-up to the #103857 salvage. The Copilot pattern fallback now uses
is_astra_model (the shared exact set) instead of a gpt-6-astra prefix, so
speed-tier or unknown suffixes such as gpt-6-astra-pro stay off the Astra
ladder, matching every other Astra gate. Also drops the stale
'"max" is gpt-5.6-only' comment in the auxiliary Responses builder.
Follow-up to the #123592 salvage. The canonical endpoint now comes from the
provider profile, which also covers OpenRouter (absent from PROVIDER_REGISTRY),
so pinning https://openrouter.ai/api/v1 keeps the native catalog too. Both
sides go through normalize_route_base_url, the helper the rest of the route
comparisons use, so scheme/host case and default ports no longer read as a
relay. The provider is normalized once.
Chat catalogs and the session switch treated image and video generation
models as chat. Exclude them by the capability type and name shape the
catalog already publishes, reject selecting one as the session model, and
do not restore a primary already known to be non-chat. Desktop shows the
fallback switch in the transcript.
Addresses two P1 review findings on #121614.
1. `provider_model_ids`: a failed or empty relay probe fell through to the
canonical per-provider fetcher, sending the provider credential to exactly
the vendor host the user routed away from — recreating #121387 on the
failure path. A configured `model.base_url` relay is now TERMINAL for live
catalog egress and degrades to the local curated list instead. The curated
tail is extracted as `_static_catalog` and shared by both paths.
Fetchers that already resolve `model.base_url` themselves and degrade
locally (`_anthropic_catalog`, `_custom_catalog`, `_openai_catalog`, the
simple api-key fetchers) are excluded from interception via
`_RELAY_AWARE_CATALOG_FETCHERS` — they already satisfy the invariant and
produce a better-merged catalog.
2. `_try_anthropic`: an `explicit_base_url` that failed
`_is_anthropic_compatible_host` was silently dropped, leaving `base_url` at
the ambient/canonical host and continuing with the explicit credential —
a silent retarget of authority, not the refusal the PR body claimed. It now
returns unavailable before client construction.
Regressions: an egress-sentinel test pins that no vendor fetcher, profile
catalog or models.dev merge is reached after a relay 404/hang; the Anthropic
test now asserts no client and zero SDK builder calls.
Follow-up to the two contributor commits for #121486. The picker, the
image plugin and the auxiliary Codex client still composed a pooled
gateway key with a base re-read from ambient state (HERMES_CODEX_BASE_URL
or the chatgpt.com default), so a model.base_url-only gateway (env unset)
still sent its key to chatgpt.com.
- auth_codex: resolve_codex_runtime_credentials reports the host a pooled
credential actually routes to (runtime_provider._pool_entry_mode_and_url:
env > model.base_url while the row is canonical > row URL) instead of the
ambient default; get_codex_auth_status carries the same bound base_url.
- picker: get_codex_model_ids(access_token, base_url=) now receives the base
resolved with the token from hermes_cli/models.py, the CLI default-model
swap (self.base_url) and the `hermes model` Codex flow.
- aux/image: _resolve_codex_credential_and_base() returns (token, base) from
one pool selection; the image plugin, _build_codex_client and the raw
Codex client use it (profile-scoped override from #121497 still wins).
- model_metadata: the non-JWT refusal now applies only when the target is
chatgpt.com; a gateway key may probe its own gateway's /models.
Adversarial regressions: model.base_url with env unset, env/route mismatch,
opaque + JWT gateway keys, pool-selected credential, pool row with its own
gateway URL, direct-ChatGPT positive control.
Addresses @andrexibiza's review on #121508.
provider_model_ids() probed the provider's canonical host even when
model.base_url pointed the configured provider at a relay or proxy, so
the picker listed the vendor's models and sent the listing request to a
host the user never chose. Discovery now probes the same endpoint
inference uses, and any probe failure falls through to the canonical
fetchers unchanged.
Fixes#121387
The fast-mode docs list Claude Opus 4.8, Opus 5 and Opus 5.5. The old
"opus-5" substring matched any future Opus 5.x, and the API answers
`speed` on a model it doesn't list with an error.
One exact list in agent/model_metadata.py now backs both the wire gate
(anthropic_adapter) and the /fast toggle (hermes_cli/models.py). It
accepts vendor-prefixed, dotted and dated ids.
_disk_serve_tier owns the ollama clamp now, and get_model_capabilities /
get_model_info already treat config=None as load-it-yourself.
Six config=config pass-through ternaries in agent/models_dev.py whose callees
have no test double (_configured_catalog_provider, _models_dev_id,
_get_provider_models) collapse to a single call; the _cfg_get and
_load_model_overrides ternaries stay because tests replace those functions.
The parallel prefetch classified any cache entry past
_PROVIDER_MODELS_CACHE_TTL as needing a fetch and blocked the picker on
the thread pool until every one returned. But cached_provider_model_ids()
has two non-blocking tiers, not one: past the TTL and inside
_PROVIDER_MODELS_STALE_SERVE_MAX it still returns the cached list
immediately and revalidates off-thread. Prefetching those slugs traded a
non-blocking serial call for a blocking parallel one.
Since _PROVIDER_MODELS_STALE_SERVE_MAX is far longer than
_PROVIDER_MODELS_CACHE_TTL, every picker open more than a TTL after the
previous one paid for it: locally, 26 providers with a cache 60s past TTL
took 3.1-4.3s, bounded only by the slowest provider's round-trip. Gating
on usability instead of freshness brings that to 0.45s.
The gate now keys on the row the serial call actually reads via
_normalized_cache_slug: a bare "ollama" stays its own cache key rather
than folding into "custom", because the local native catalog has its own
TTL and its own empty-is-authoritative rule. And an empty catalog counts
as servable only for ollama inside _OLLAMA_LOCAL_MODELS_CACHE_TTL —
cached_provider_model_ids returns it with no round-trip, so prefetching
it is redundant work the picker waits on. Past that TTL an empty row gets
no stale-serve window and really does block, so it stays in the prefetch.
Entries the serial path genuinely cannot serve — missing, fingerprint
mismatched, or past the stale-serve window — still prefetch in parallel,
so a cold cache is unaffected. refresh=True already skips the prefetch
entirely, so explicit refresh still forces every provider.
python -m pytest tests/hermes_cli/test_model_cache_parallel_prefetch.py -q
19 passed
Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
(cherry picked from commit fb8954a37816002bc5db9a592ebc2d8f1af87673)
Runtime identity resolved through hermes_cli.__version__ (a static 0.0.0
on source installs, rewritten by release stamping) leaked v0.0.0 into
About, /api/health, User-Agents, and plugin compat, and source updates
showed "couldn't reach update server" because identity and channel
authority disagreed with the checkout.
Now: get_version_info() resolves install stamp -> live git -> unknown,
never pyproject metadata, never a package constant. Source checkouts
derive identity from their reachable release tag; the completion tail of
every successful install/update/historical takeover atomically rewrites
install-stamp.json with that identity; a stale source stamp whose commit
no longer matches HEAD defers to live git. ACP/TUI use derived_version
for display and base_version for protocol fields; all ~44 runtime
__version__ consumers migrated; hermes_cli.__version__ and generated
_version.py are gone; release stamping only touches the native manifests
external builders consume (nix/tauri/cargo) and passes release identity
straight into write_install_stamp.py; pyproject.toml stays inert 0.0.0.
Desktop no longer synthesizes a competing install-stamp.json: the
checkout owns its stamp, and desktop-bootstrap classification keys on
the bootstrap-complete marker. verify-bootstrap-version-stamp.py now
cross-checks the checkout's stamp (baseVersion + commit == HEAD).
Validation: 31-file focused suite green (version identity, stamping,
adoption, providers, gateway, acp/tui runtime identity, api server via
extras env, release graph); desktop tsc + 25 vitest green; real-repo
probe: base=unknown derived=git.0635606.dirty source=git on this
checkout; clean-env imports resolve entirely from this tree; windows
footgun + compat-pointer scans clean.
When a provider's live catalog fetch failed, provider_model_ids() degraded to the
curated static list and cached_provider_model_ids() wrote that list to
provider_models_cache.json with a fresh 1h TTL, exactly as if it were the
account's real catalog. The "only non-empty results are cached" guard never
fired because the fallback is non-empty. A transient Copilot outage therefore
replaced an account's 10 enabled models with the 17-model static list on every
picker surface until the TTL lapsed, and a same-credentials restart, re-auth or
Refresh Models could not shake it (#107391).
Mark the curated list as CuratedFallbackModels at the sites that serve it for a
missing live catalog (the Copilot fetcher, the generic profile merge, and the
static tail when a live fetcher declined). The cache layer then treats it as a
placeholder: it never replaces a same-credentials live row (the account's real
catalog is served instead), it is stored flagged with a 60s TTL when there is
nothing better, and it is never served through the stale-while-revalidate
window. A provider with no live source at all is unaffected: its static list is
its catalog and caches as before.
Branch semantics kept where main and PM disagree: update_cmd_deps.py,
constraints-termux.txt, the Electron update-api-check module and the
post-swap hand-off test stay deleted; the pending-fleet-restart catch-up
and the local_runtime tag/download ladder stay retired (PM owns engines).
Ported from main onto the branch's shape: profile_scoped_chore for the
auto-archive and plugin-update housekeeping chores, the local-runtime
cross-process boot lock and residency cap, the checkpoint tmp_pack sweep,
the cua daemon-liveness status probe, the remote-served Desktop update
flag (posix.sh / windows.ps1), sign-in for env-pinned remote gateways
(urlDisabled on RemoteSetupFields), the uvloop extra split (uvicorn
without [standard]), and the umask-scoping spawn test.
uv.lock regenerated with pm.build_env --lock-only; new utf-8 reads from
main switched to utf-8-sig (check-windows-footguns).
Native `deepseek` is curated-only: `deepseek-flash` / `deepseek-v4-pro`, in that
order. models.dev still indexes the retired `deepseek-v4-flash*` ids, and with
`deepseek` in `_MODELS_DEV_PREFERRED` the registry union re-added them
registry-first whenever the live /models fetch was unavailable. The Desktop
label for `deepseek-flash` now reads "DeepSeek V4.1 Flash".
Trim of the contributor diff: the deepseek-specific branch inside
`_merge_with_models_dev` was unreachable once deepseek left the preferred set,
so it is dropped; the test drives `provider_model_ids("deepseek")` instead of
the helper.
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>
The offline (no key) path serves the curated floor merged with models.dev, and both still list
x-preview-f-free; the setup-flow path goes through merge_profile_catalog with an empty live list.
One helper, _drop_delisted_opencode_models, runs on the FINAL rows of merge_profile_catalog and of
the static/models.dev return in provider_model_ids, so no path can offer a slug the relay 401s.
Adds the offline invariant test.
The Zen relay retired x-preview-f-free (the picker-facing id for Ox Alpha),
but the exclusion set named the wrong slug (ox-alpha-free) and the live-first
merge only filtered the live half, so the curated floor resurrected the
delisted id and the picker kept offering a model that 401s.
- add x-preview-f-free to _OPENCODE_FREE_EXCLUDED_MODELS
- filter the MERGED result (curated floor is merged back in as secondary half)
Closes#115496
Conflicts resolved toward the PM model: main's lazy_deps/update_cmd_deps/npm
stamp machinery stays deleted (PM + scripts/build/node-deps.mjs own it), the
systemd ExecStop stop-mark rides the installation launcher, legacy
linux_only/macos_only/windows_only markers are rewritten to platforms(), and
finalize_update_receipt carries pending manual-serve obligations forward
again (lost when the ContextVar receipt rewrite crossed c0aa3ce354).
Test harness: the real-home I/O guard exempts /proc/<pid>/fd metadata reads
(deleted-WAL holder scans) and run_tests.sh drops ~/.hermes PATH entries so
shutil.which() cannot trip the tripwire.