Commit Graph

383 Commits

Author SHA1 Message Date
liuhao1024
d3b2602516 fix(agent): skip /v1/models/{model} probe for unrecognised server types
When detect_local_server_type() returns None (e.g. LiteLLM proxies),
the unconditional GET /v1/models/{model} probe hits an admin-gated
endpoint that logs ERROR on the server side, even though the 401 falls
through gracefully.

Guard the probe behind server_type in (ollama, lm-studio, vllm, llamacpp)
so unrecognised servers skip straight to the universally-supported
/v1/models list endpoint.

Fixes #25848
2026-10-02 20:29:34 -05:00
JoaoMarcos44
6403dd325c fix(anthropic): simplify recovery and make accounting explicit 2026-10-02 00:18:00 +05:30
kshitijk4poor
4d3555e5ca fix(models): cap -900k on custom codex_responses routes by the route's catalog
A custom endpoint with api_mode codex_responses resolved an opted-in -900k
alias from the static table only (#116329), so it got 900K even when the
gateway's own catalog publishes a lower Codex-shaped max_context_window.
For an opted-in variant the route now asks its own catalog, at its own URL
with its own key, for that cap. The proxy's context_window is still never
trusted, plain slugs make no request, and a gateway without a Codex catalog
is probed once per negative TTL rather than on every resolution.

Reported by andrexibiza on #128288.
2026-10-01 23:39:32 +05:30
kshitijk4poor
e22905a696 fix(models): remember a Codex catalog probe that found nothing for 5 minutes
The Codex catalog fetch only cached a non-empty result, so a dead endpoint,
a non-200 answer or a body without a Codex catalog was re-probed on every
context resolution (agent init, /status, aux routing, @-reference turns),
each miss costing up to two GETs with a (5, 10)s timeout. A miss is now
cached for _CODEX_OAUTH_CONTEXT_NEGATIVE_TTL (300s): the entry is stamped
as if cached TTL - NEGATIVE_TTL ago, so the existing freshness check expires
it and the cache tuple keeps its shape. A fresh catalog stored by a
concurrent probe is never overwritten, and the max_context_window map is
left untouched so an observed cap still survives the outage (#130686). A
new token or endpoint is a new fingerprint and probes immediately. An empty
200 now reports fresh=False like the other misses (nothing to confirm).
2026-10-01 23:39:32 +05:30
kshitijk4poor
03487e9ed9 fix(models): keep an observed Codex catalog cap when a refresh fails
After the 1h context TTL, a failed catalog refresh sent resolution to the
static fallback, which applied the -900k bump without the max_context_window
the same token and endpoint had already published. A known 600K cap became
900K with no newer evidence. The fallback branch now passes the cached cap.
Introduced with the cap itself in #115872.

Reported by andrexibiza on #128288.
2026-10-01 22:17:57 +05:30
kshitijk4poor
e0a8749ca0 fix(models): publish the Codex catalog cap before its context entry
A reader that saw a fresh _codex_oauth_context_cache entry could find no
matching max_context_window yet (the max map was written second) and resolve
an opted-in -900k alias to the uncapped 900K instead of the catalog's ceiling.
Writing the max first means any reader that sees the context entry also sees
its cap. Reachable only with true thread interleaving (free-threaded builds);
the store order has been this way since #115872.

Reported by andrexibiza on #128288.
2026-10-01 22:17:57 +05:30
kshitijk4poor
bb734b45ac test(models): pin gpt-6.1-sol-900k's catalog cap through the public resolver
Review follow-up on #128288 / #129056 (andrexibiza):

- The 900K entry is only the static fallback. The existing catalog-cap test
  for get_model_context_length is now parametrized over gpt-6.1-sol as well,
  so the alias is proven to resolve to the account catalog's
  max_context_window (872K) when it is lower, and to stay 900K when the field
  is absent or higher. Removing the clamp fails both slugs. The picker test
  also pins that no 6.1 entry is synthesized when discovery does not list the
  base.
- The source comment no longer attributes "922,000 max input" to the OpenAI
  model page: the page lists 1.05M context and 128K max output; 922K is
  their difference.

Co-authored-by: yuzilongleif-collab <235949691+yuzilongleif-collab@users.noreply.github.com>
2026-10-01 22:17:57 +05:30
GodsBoy
5bb6127c5b feat(models): add the gpt-6.1-sol-900k Codex OAuth picker variant
#128248 registered gpt-6.1-sol but left it out of the -900k opt-in because
nothing was verified above 272K on Codex. Live probe on the ChatGPT OAuth
route (2026-09-29, streaming /responses, server-reported input_tokens, effort
low): gpt-6.1-sol completed at 684,566, 898,681 and 918,137 input tokens with
all three needles recalled, and was rejected with context_length_exceeded at
about 931K and above, in line with the model page's 922,000 max input. The
live catalog advertises 272K with an 872K ceiling, which still caps the bump.

Eligibility is by exact slug, not the gpt-6-sol family prefix, so the dotted
slug is its own verified line and no -pro variant is synthesized. The variant
is billed as the base model.

(cherry picked from commit 4772e9db5d7b0fe22da80dea4f3c662672ea5302)
2026-10-01 16:40:58 +05:30
kshitijk4poor
bf87ae620e feat(models): add gpt-6.1-sol (OpenRouter + direct OpenAI API)
OpenAI released GPT-6.1 Sol on 2026-09-29. It is live on OpenRouter
(openai/gpt-6.1-sol, -pro) and the direct API; not yet on Nous Portal
or on Codex OAuth for our test account.

- Catalog: OPENROUTER_MODELS + openai-api picker; kept off the nous
  list via _OPENROUTER_ONLY until the Portal serves it. Manifest regenerated.
- Context: 1.05M direct API, 272K Codex fallback; 272K compaction
  auto-raise on the Codex route.
- Pricing: $2/$10, cached input $0.10 (5%, not 6 Sol's 10%), cache write
  $2.50, 2x/1.5x above 272K; -pro aliased to the base row.
- Reasoning: none/minimal return 400 on 6.1 Sol (unlike 6 Sol), so it uses
  Astra's low..max ladder without Astra's account gating. OpenRouter's
  catalog wrongly lists 'none' for it, so the OpenRouter profile omits a
  disable when the OpenAI ladder has no 'none'.
2026-09-29 23:25:26 +05:30
kshitijk4poor
2be9fa5f9d refactor(sessions): simplify-code pass over the identity helpers
- without_persistence_fields() replaces the two hand-written strips (context-engine selection,
  token-estimate fingerprint).
- mint_uid() is the one uid minter; uid_list() the one uid-list normalizer (message_metadata's
  _absorbed_uids and the codec's _uid_list share it).
- _live_or_column() replaces four copies of 'live key first, column name second'; the absorbed
  list writer takes the dict, not a (live_key, column) pair its one caller always passed the same.
- _restore_row_identity() is the one stored-identity-onto-live-dict step, shared by restore and
  the row-addressed rewrite (transcript_repair had a hand copy). Restore skips the decoders for
  NULL columns (the common row).
- index_tool_call_uids() returns the variants it named, so the flush walk stops computing them
  twice; tool_call_uid_from_history drops guards its one caller can't trip.
- rewind returns the replacement dict (the insert already stamps _row_id and message_uid)
  instead of re-querying last_insert_rowid.
- _remember_absorbed_row's folded is required, so every call site states the witness rule.
- The backfill reads/deletes state_meta through _meta_row/_delete_meta; chat_completions strips
  MESSAGE_UID by constant.
No behaviour change.
2026-09-29 04:15:21 +05:30
kshitijk4poor
30050851f7 perf(sessions): pair tool-call uids in O(K) on flush and only when needed on restore
- The flush walk memoizes each assistant's named ids and uid index across one collect,
  so K parallel results no longer recompute K assistants' variants (O(K^2) -> O(K)).
- Restore indexes assistant maps lazily: only a result without a stored uid (an older
  build's) needs the index; rows this build wrote never pay for it. Same order, same
  shadowing. One decoder (_restore_identity_columns) for both restore paths.
- The token-estimate memo key skips persistence-only fields the estimate never reads.
- Context-engine selection strip: isdisjoint instead of per-key scans.
2026-09-29 04:15:21 +05:30
kshitijk4poor
e87f673faa fix(codex): send catalog/image credentials only to their own route
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.
2026-09-25 21:27:06 +05:30
liuhao1024
602aa9a55b fix(agent): keep custom-Codex-base credentials off chatgpt.com
Behind a custom Codex base URL (HERMES_CODEX_BASE_URL / model.base_url
gateway) three paths still hit the hard-coded chatgpt.com host with the
gateway's credential-pool key (#121486):

- the OAuth context-length probe (agent/model_metadata.py) and the
  /model picker's live discovery (hermes_cli/codex_models.py) both GET
  https://chatgpt.com/backend-api/codex/models with
  Authorization: Bearer <gateway key> whenever model.context_length is
  not pinned — the key is sent to a service it does not belong to and
  cannot answer for;
- the openai-codex image_gen plugin posts to the same hard-coded base.

Fix, mirroring the quota probe's existing gate in auth_codex:

- both catalog sites now decline to probe non-JWT credentials (real
  Codex access tokens are JWTs; a gateway key is not one) and fall
  back to the static table / offline sources — same outcome as the
  doomed request today, minus the credential leak;
- a JWT reached through a custom base now probes that base's own
  /models instead of chatgpt.com (catalog URLs are built from the
  resolved base; the per-token cache key includes the base);
- the image plugin resolves its base from HERMES_CODEX_BASE_URL the
  same way the text client does.

Fast-mode host gating in the /fast picker is intentionally left
untouched: lifting it needs an explicit opt-in design decision, not a
bug fix.

(cherry picked from commit 5d76ec525674d7b103ab53955ba5279605457ca1)
[salvage: plugins/image_gen/openai-codex/__init__.py hunk dropped in favour of #121497 (first submitter, profile-scoped override + base-aware Cloudflare headers)]
2026-09-25 21:27:06 +05:30
ethernet
16652eea18 Merge remote-tracking branch 'origin/main' into ethie/pm-clean
# Conflicts:
#	gateway/config.py
#	gateway/config_loader.py
#	gateway/readiness.py
#	hermes_cli/managed_scope.py
#	hermes_cli/plugin_python_deps.py
#	hermes_cli/plugins_cmd.py
#	hermes_cli/update_cmd_maint.py
#	plugin-catalog/hindsight.yaml
#	plugins/plugin_loader.py
#	providers/__init__.py
#	scripts/run_tests.sh
#	tests/gateway/test_control_socket_windows_live.py
#	tests/gateway/test_gateway_streaming_nested_config.py
#	tests/hermes_cli/test_doctor.py
#	tests/hermes_cli/test_plan_reconciliation_windows_live.py
#	tests/hermes_cli/test_update_apply_shallow_count.py
#	tests/hermes_cli/test_update_concurrent_quarantine.py
#	tests/hermes_cli/test_update_shim_self_lock.py
#	tests/hermes_cli/test_verify_console_scripts.py
#	tests/tools/test_lazy_deps.py
#	tests/tui_gateway/test_subprocess_encoding.py
#	tools/lazy_deps.py
2026-09-23 15:26:34 -04:00
emozilla
78a50c28fc fix(anthropic): send fast mode only to the Opus models the docs list
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.
2026-09-23 13:40:44 -04:00
ethernet
339229490c Merge remote-tracking branch 'origin/main' into ethie/pm-clean
# Conflicts:
#	.github/workflows/tests-os.yml
#	.github/workflows/tests.yml
#	Dockerfile
#	hermes_cli/memory_setup.py
#	hermes_cli/update_cmd.py
#	hermes_cli/web_server_memory.py
#	plugins/memory/hindsight/README.md
#	plugins/memory/hindsight/__init__.py
#	plugins/memory/hindsight/embedded.py
#	plugins/memory/hindsight/plugin.yaml
#	plugins/memory/hindsight/settings.py
#	plugins/memory/hindsight/setup.py
#	tests/plugins/memory/test_bom_tolerant_config_reads.py
#	tests/plugins/memory/test_hindsight_env_perms.py
#	tests/plugins/memory/test_hindsight_provider.py
#	tests/tools/test_lazy_deps.py
#	tools/lazy_deps.py
#	uv.lock
#	website/docs/user-guide/docker.md
#	website/docs/user-guide/features/memory-providers.md
2026-09-23 05:45:02 -04:00
teknium1
38c289c014 fix(models): drop gpt-6-terra, a tier OpenAI never published
#119410 registered gpt-6-terra alongside sol and luna from the request text, and the Codex
forward-compat synthesis put it (and -900k) in the live /model picker on every surface.
Nothing serves it: not the Codex account catalog (astra, sol, luna), not OpenRouter (same
three, plus -pro), and OpenAI's model page 404s. Forward-compat is for published tiers the
account catalog has not listed yet, not for guessed names. Removed from the Codex fallback
list and template chain, the context/900k tables, the effort ladder prefixes, the aux-client
family list, and the Nous/OpenRouter static catalogs; catalog JSON regenerated. A contract
test pins the synthesized GPT-6 set to the published tiers.
2026-09-23 00:44:33 -07:00
ethernet
1a68859e8f test(codex): reconcile catalog HTTP seam 2026-09-22 17:18:15 -04:00
ethernet
803165526b Merge remote-tracking branch 'origin/main' into ethie/pm-clean 2026-09-22 17:16:46 -04:00
ethernet
4b803147d3 Merge remote-tracking branch 'origin/main' into ethie/pm-clean
# Conflicts:
#	apps/desktop/src/app/contrib/onboarding-kickoff.ts
#	hermes_cli/profiles.py
2026-09-22 17:16:06 -04:00
teknium1
1d10cef836 fix(codex): ask the models catalog as the newest client so GPT-6 Sol/Luna show up (#119412)
The ChatGPT Codex models endpoint gates entries on client_version against each
model's minimal_client_version. Hermes sent the "0.0.0" sentinel, which used to
return the whole account catalog; since the GPT-6 Sol/Luna rollout it returns a
frozen legacy list (astra + the 5.6 trio) while any version at or above the newest
minimal_client_version (0.155.0, 1.0.0, 99.0.0 alike, live 2026-09-22) returns
everything the account is entitled to. So the picker and the context-window probe
hid gpt-6-sol / gpt-6-luna that the vendor's own client showed for the same account.

Both request sites now go through fetch_codex_catalog_entries(): try the
newest-client URL first and fall back to the "0.0.0" sentinel only when that
answer is non-200 or empty (the backend used to reject out-of-sequence versions
that way). No local ~/.codex/models_cache.json is consulted: a missing or stale cache (this
host records 0.147.0, which hides gpt-6-sol at 0.155.0) must not decide what the
account can see.

Report by @Stone441 (#119412); supersedes #119420 by @KoNit-K, whose fix read the
compatibility version from the local `~/.codex` cache.
2026-09-22 13:07:45 -07:00
teknium1
79ec1f2a34 feat(models): GPT-6 Sol/Terra/Luna replace the 5.6 tiers in Nous/OpenRouter catalogs, with Codex -900k variants
OpenAI shipped gpt-6-sol / gpt-6-terra / gpt-6-luna as the successors of the
gpt-5.6 tier line (Sol and Luna live on OpenRouter + the Nous Portal today).
The curated aggregator catalogs (OPENROUTER_MODELS and the derived nous list,
plus the published website model-catalog.json) now carry the gpt-6 tiers and
their -pro variants instead of the 5.6 ones; the openai-api curated fallback
lists them ahead of 5.6.

Codex OAuth support mirrors the 5.6 + Astra contract for every gpt-6 tier:
curated fallback + forward-compat synthesis (from the 5.6 twin or 5.5),
272K advertised fallback, the opt-in -900k picker variants with the
live-verified 900K bump (still capped by the catalog's max_context_window),
dated-snapshot eligibility, wire-suffix stripping, the compaction auto-raise
on the base slug, and the gpt-5.6 effort ladder (max allowed, minimal
rejected). Pricing rows for gpt-6-sol / gpt-6-luna come from OpenAI's model
pages (272K whole-request tier like Astra); Terra has no published page yet
so it deliberately has none.

/model gpt keeps resolving to the flagship: "astra" joins the rank-0 suffix
set so gpt-6-astra sorts above gpt-6-sol.
2026-09-22 11:50:39 -07:00
ethernet
05609d2092 merge: origin/main (19 commits) into ethie/pm-clean
Conflicts, all inside the local-runtime / local-models subsystem:

- hermes_cli/local_runtime/binaries.py, hermes_cli/web_routers/local_models.py:
  main's own transfer machinery (urllib ranged download, shutil.move publish) is
  retired here — pm/downloader owns every fetch. Took ours.
- tests/hermes_cli/test_local_runtime_downloads.py: deleted here (its subject,
  binaries._download, is gone); kept deleted.
- tests/hermes_cli/test_local_models_routes.py: ours (the pause/resume suite) plus
  main's held-finished-file contract, re-seamed onto the pm downloader.

main's fix 515412f415 ("wait out a held finished download instead of copying it and
failing") is re-homed so it fits the PM model: the Windows rename that fails while an
antivirus or indexing scan still holds a just-closed multi-gigabyte file is now handled
once, in pm/downloader.replace_when_released, which is the single publish path for PM
archives and model files alike. A cleanup that cannot remove its leftover can no longer
mask the error that left it.

Tests: helper retries a transient refusal and lands the file; gives up with the
plain-language message chained to the OS error; a download publishes through two refused
renames with nothing left behind; a stuck hold reports the rename error, not the failed
cleanup. Route-level: the job lands the file and reports done.
2026-09-21 17:24:14 -04:00
rob-maron
0f9fc3434e add xiaomi mimo v2.6 2026-09-21 13:28:28 -07:00
ethernet
9f2ba1b74d merge origin/main (779 commits) into ethie/pm-clean
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).
2026-09-21 00:58:39 -04:00
teknium1
13fe9c7171 feat(providers): external-process provider support for standalone model-provider plugins (from #105863)
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>
2026-09-20 14:29:39 -07:00
teknium1
2b3bab0a5c fix(anthropic): key_cmd Claude Code OAuth identity survives on custom api.anthropic.com routes (main + aux)
A named custom provider at api.anthropic.com (api_mode anthropic_messages) whose token comes from a
key_cmd callable lost the Claude Code OAuth identity: the aux custom routes hard-coded
is_oauth=False, agent_init/agent_runtime_helpers/client_lifecycle gated OAuth on provider=="anthropic"
and isinstance(key, str), and the callable-token client builder never added the OAuth betas or the
claude-code user-agent. Anthropic answers such a bare Bearer with 429 rate_limit_error "Error"
(#114967). One resolver, anthropic_credentials.anthropic_route_is_oauth(base_url, credential,
provider=), decides at every site: the route qualifies for the anthropic provider (unchanged) or an
exact api.anthropic.com host, the credential is a string or a callable materialized once
(CommandTokenSource caches), third-party hosts never qualify. model_metadata
_query_anthropic_context_length skips a callable credential instead of crashing agent init
(AttributeError on the same key_cmd route on current main).

Slimmer redo of #115007 by @liuhao1024 (same direction; one shared helper instead of per-site copies).

Co-authored-by: liuhao1024 <sunsky.lau@gmail.com>
2026-09-20 12:49:40 -07:00
teknium1
8d153b26aa feat: add z-ai/glm-5.3-flashx to the OpenRouter and Nous Portal catalogs
Both routes serve the slug (tools supported, 1,048,576 context, $0.37/$1.25 per M).
The Nous list is derived from OPENROUTER_MODELS so one tuple edit covers both; the docs
manifest is regenerated in the same commit.

DEFAULT_CONTEXT_LENGTHS gets its own key: substring matching would otherwise land the
slug on the glm-5.3-flash entry (1,310,720) and overstate the window by 25%.
2026-09-20 12:16:06 -07:00
ethernet
9f837d298b Merge remote-tracking branch 'origin/main' into ethie/pm-clean
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.
2026-09-20 10:07:50 -04:00
kshitijk4poor
b729bc43bc refactor(config_providers): keep main's self-resolve, read the config through the read-only loader
Gate review: the salvaged config_providers.py hunk was behaviour-neutral churn (the helper
already self-resolved a None route list on main) that also dropped the raw-config fallback.
Restore main's shape; since step 0c now runs for every route with a base_url, resolve via
load_config_readonly() so the per-call deepcopy of load_config() is not paid on every
context-length lookup. Comment trimmed to the WHY; the invariant test moves next to its
siblings in test_custom_provider_context_length.py.
2026-09-20 15:17:49 +05:30
Stephen Cuppett
f4ffd48274 fix(model_metadata): honor per-model context_length when the caller doesn't pass custom_providers
get_custom_provider_context_length only found the user's
custom_providers[].models.<id>.context_length override when the caller
had already loaded the route list and passed it in. Several real call
paths never do — auxiliary fallback screening (auxiliary_client),
vision auto-detect, the CLI/TUI @-context-reference estimators, and
gateway /status all pass custom_providers=None — and step 0c of
get_model_context_length was additionally gated on that list being
truthy. Those paths skipped the override and fell to the 256K/272K
probe-down defaults, while the startup path honored the same setting,
so the same config 'took' in some places and not in others.

Fix: get_custom_provider_context_length self-resolves the route from
config when neither custom_providers nor config was supplied (same
pattern as _resolve_moa_context_length; load_config() is cached on the
config file signature), and the step-0c gate now checks only
(base_url AND model).

Regression test asserts the no-args call shape returns the override
(proven red on base).
2026-09-20 15:17:49 +05:30
ethernet
ce3d042f0c Merge remote-tracking branch 'origin/main' into ethie/pm-clean 2026-09-19 23:08:38 -04:00
ethernet
e1576d06a6 Merge remote-tracking branch 'origin/main' into ethie/pm-clean
Resolved toward the branch: PM provisions uv/python (main's install.ps1 uv-shim
salvage + its test and workflow steps dropped), the shim re-exec stays retired,
package.json carries no electron-builder block (afterExtract identity stamp wired
into electron-builder.config.cjs instead; after-pack.mjs keeps signing only),
Desktop workspace-deps helpers stay retired. Main's scratch-dir bootstrap
(export_scratch_tmp_env) is taken and re-run after profile resolution.
2026-09-19 22:57:07 -04:00
teknium1
21ef1b97f9 fix(context): proxied Codex routes resolve the Codex OAuth window, not the direct-API catalog
A Codex OAuth model (gpt-6-astra, gpt-5.6-sol/terra/luna, gpt-5.5, ...) served
through a proxy — a custom provider with api_mode: codex_responses at
http://127.0.0.1:8317/v1, or the openai-codex provider behind
HERMES_CODEX_BASE_URL / model.base_url — resolved 1,050,000 from
DEFAULT_CONTEXT_LENGTHS instead of the 272,000 Codex enforces. The compressor
then set its trigger at 525,000 tokens, ~2x past the Codex limit and its 272K
billing tier, and a day of Astra sessions burned two Pro weekly windows.

Root cause: step 2 of get_model_context_length treated any non-known hostname
as a generic custom endpoint and never looked at the route's transport, so the
Codex table two steps away was skipped. The decision key is now the route:
_is_codex_route() is true for provider openai-codex on any URL and for a custom
entry whose canonical api_mode is codex_responses. A custom Codex route answers
from the Codex OAuth table first (with the opted-in -900k bump; unknown slugs
still take the endpoint probes), the native provider skips the custom-endpoint
step and keeps its live catalog probe in step 5, and both bypass the persistent
cache so a stale 1.05M entry cannot outlive the fix. Explicit
providers.<name>.models[].context_length / context_length / model.context_length
overrides still win (step 0 runs first); chat_completions proxies are unchanged.

Reported with the exact resolver path by @0xble (#116199); route-keyed lookup
per @JoaoMarcos44 (#116262).

Fixes #116191
2026-09-19 12:17:05 -07:00
teknium1
1d5620ad87 Merge remote-tracking branch 'origin/main' into HEAD
# Conflicts:
#	agent/model_metadata.py
#	tests/agent/test_output_cap_parsing.py
2026-09-19 11:22:29 -07:00
Teknium
7feb1af028 Merge pull request #115872 from NousResearch/fix/boa-res-R4-routing-catalog-azure-codex-900k-cap
fix(codex): opted-in -900k aliases cap at the live catalog max_context_window (#105443, salvage #105445)
2026-09-19 11:12:31 -07:00
teknium1
811d3959b2 chore: merge origin/main (resolve tests/agent/test_output_cap_parsing.py) 2026-09-19 10:52:15 -07:00
teknium1
7aff381943 chore: merge origin/main (resolve tests/agent/test_output_cap_parsing.py, tests/hermes_cli/test_runtime_provider_resolution.py) 2026-09-19 10:52:01 -07:00
teknium1
a516db44e7 Merge remote-tracking branch 'origin/main' into HEAD 2026-09-19 10:46:59 -07:00
teknium1
35f69a9101 fix: drop the dead is_local_endpoint clause from the hosted-host probe guard
No _URL_TO_PROVIDER entry is a local address, so the clause could never
change the guard's verdict; the comment already states the invariant.
2026-09-19 10:11:01 -07:00
liuhao1024
5d0a39a133 fix(agent): stop probing hosted provider hosts for local server types (#61421)
detect_local_server_type ran the Ollama/LM Studio/llama.cpp/vLLM discovery
waterfall (/api/v1/models, /api/tags, /v1/props, /props, /version) against
whatever base URL it was given, including https://api.openai.com/v1 via
image_routing._should_probe_ollama_vision. Every leg 404s on a hosted
provider and shows up in egress/mesh logs as Hermes hitting /version on
api.openai.com.

Skip the waterfall when the URL is not a local endpoint and its host maps
to a known hosted provider in _URL_TO_PROVIDER (the same table
_infer_provider_from_url already uses; substring host match covers
trailing-dot FQDNs and explicit ports). ollama.com is the one hosted host
that genuinely speaks Ollama's /api/tags, so it keeps the probe. Local and
unknown custom hosts are unchanged.
2026-09-19 10:11:01 -07:00
Konstantin Khlopkov
162c05831a fix(agent): price Responses function_call_output images flat, not as base64 text 2026-09-19 09:46:27 -07:00
Dhruv Modi
624ffd3b71 fix(model_metadata): recognise the OpenAI-style completion split as an output-cap error
OpenAI's original overflow wording — "(36865 in the messages, 65536 in the
completion)" and the legacy "(771 in your prompt; 4000 for the completion)" —
is copied by vLLM and llama-cpp-python. It names neither max_tokens nor
"output tokens" and ends with "reduce the length", so both
parse_available_output_tokens_from_error and is_output_cap_error rejected it
and the 400 went to the compressor, which re-sent the same oversized
max_tokens until "cannot compress further".

The first figure is the prompt the server measured, so window - prompt is a
real output budget: return it when >= 1 and route the turn to the max_tokens
clamp. When the measured prompt alone fills the window the parser keeps
returning None and a genuine input overflow still compresses.

Slim port of #90612 (regex and semantics by @Dhruv7201); the three-helper
shape was folded into one budget helper and the tests trimmed to two
invariants. Fixes #90607.
2026-09-19 09:45:54 -07:00
teknium1
3e74037cf9 fix(codex): send reasoning.effort none explicitly; no reasoning field for chat-era OpenAI models
Two silent wire mistakes on the Responses transport and its auxiliary
adapter shared one root cause: the transport had no notion of what a
model can be told about reasoning, so it always projected the default
`medium` and always omitted a disable.

- `reasoning_effort: none` was dropped from the request. On a reasoning
  model that defaults to medium (gpt-5.6) the model kept thinking: 76
  reasoning tokens against 0 with an explicit `reasoning.effort: none`.
  The disable now goes on the wire as `{"effort": "none"}` wherever the
  route's vocabulary has `none`; an unset effort is the only state that
  omits the field. A route that rejects `none` already trips the
  reasoning-mandatory recovery (warn, drop the disable, retry).
- Chat-era OpenAI models on api.openai.com (gpt-4o, gpt-4.1, -mini,
  fine-tunes) 400 on any `reasoning` key, so every `openai-api` request
  to them failed. `_codex_efforts_for_route` now returns `()` for them
  (new `model_metadata.openai_model_rejects_reasoning`, a denylist so an
  unknown future model keeps its dial) and both the main transport and
  `_CodexCompletionsAdapter` send no reasoning field. Only the exact
  OpenAI origin is judged: a relay serving those ids may translate.

Fixes #75227
Fixes #76255
2026-09-19 09:39:50 -07:00
teknium1
0837f549be fix: drop base64 imports left unused by the shared codex_account_headers helper
ruff --select F401 flagged both after the per-module _extract_chatgpt_account_id
copies were removed.
2026-09-19 09:28:13 -07:00
teknium1
7471586ad0 fix: send the Codex residency header on every JWT-derived Codex request
The Codex models catalog probe (agent/model_metadata.py), the picker catalog
fetch (hermes_cli/codex_models.py), the quota-restored probe
(hermes_cli/auth_codex.py) and the /usage dashboard call
(agent/account_usage.py) each re-decoded the OAuth JWT for
ChatGPT-Account-ID and would 401 on residency-enforced workspaces exactly
like the chat client did (#23896). They now share
agent.codex_headers.codex_account_headers, which emits the account and
x-openai-internal-codex-residency headers from one decode; the three
duplicate `_extract_chatgpt_account_id` decoders are gone.

account_usage keeps auth.json's account_id as the winner over the JWT claim
(pool-only credentials still omit it); header casing unifies on the
codex-rs canonical `ChatGPT-Account-ID` (HTTP header names are
case-insensitive on the wire; the two tests asserting the old casing follow).
2026-09-19 09:28:13 -07:00
teknium1
5eb31233b4 fix: parse the "limited to N" output-cap figure so the budget steps down
is_output_cap_error already classified "max_completion_tokens is limited to
16384 for glm-5.2" as an output cap, but parse_available_output_tokens_from_error
returned None, so _recover_context_length failed fast instead of clamping.
Add the ("limited to",) signal and the direct-cap regex; the Scaleway string
now yields 16384. Also document model.key_env in the custom provider block.
2026-09-19 01:15:42 -07:00
chelsealong
6c28f1f703 fix: parse SGLang output-cap rejection wording (#83521)
SGLang states the window, the input and the completion figures explicitly ("maximum
context length of W tokens ... I tokens from the input messages and C tokens for the
completion") without ever naming max_tokens, so the 400 fell into compression instead
of a retry with a smaller completion budget. Available output = W - I; when I >= W the
wording describes a genuine input overflow and keeps routing to compression.

Slimmed salvage of #83558.
2026-09-19 00:57:56 -07:00
David Metcalfe
2641d416c8 fix: parse Azure output-cap rejection wording (#78405)
Azure OpenAI rejects an over-cap request with "max_tokens is too large: N. This model
supports at most M completion tokens." Neither output-cap gate recognized it, so the
400 was routed into context compression, which cannot help (the input already fits)
and ended a fresh session with "cannot be shrunk further". Both gates now match the
phrasing and the advertised ceiling M becomes the retry budget.

Slimmed salvage of #81289.
2026-09-19 00:57:56 -07:00
teknium1
a585ad83b2 fix: keep the Codex catalog fetch shape while capping -900k at max_context_window
Store the catalog's max_context_window in a parallel per-token dict populated
by the same fetch instead of widening the fetch's return tuple and versioning
the cache key ("v2:"), so the one external caller and every existing cache
reset keep working. The cap moves into _apply_verified_bump as
min(900K, live max): the bump still fires only for an opted-in -900k alias
whose advertised window is exactly 272K, 900K stays the offline/absent
fallback and a catalog max above 900K does not raise it.

Trim the salvaged tests to two invariants (parametrized alias cap incl. the
absent and above-cap controls; base slug never inherits the max) and document
the live-catalog cap next to the -900k opt-in.
2026-09-19 00:40:24 -07:00