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
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.
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).
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.
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.
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>
#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)
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'.
- 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.
- 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.
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.
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)]
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.
#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.
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.
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.
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.
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).
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>
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>
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%.
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.
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.
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).
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.
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
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.
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.
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#75227Fixes#76255
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).
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.
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.
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.
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.