2026-02-21 22:31:43 -08:00
|
|
|
|
"""Model metadata, context lengths, and token estimation utilities.
|
|
|
|
|
|
|
|
|
|
|
|
Pure utility functions with no AIAgent dependency. Used by ContextCompressor
|
|
|
|
|
|
and run_agent.py for pre-flight context checks.
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
2026-04-13 06:17:13 +02:00
|
|
|
|
import ipaddress
|
2026-06-14 04:45:46 -07:00
|
|
|
|
import json
|
2026-02-21 22:31:43 -08:00
|
|
|
|
import logging
|
2026-04-23 14:59:26 +03:00
|
|
|
|
import os
|
2026-03-05 16:09:57 -08:00
|
|
|
|
import re
|
2026-02-21 22:31:43 -08:00
|
|
|
|
import time
|
2026-03-05 16:09:57 -08:00
|
|
|
|
from pathlib import Path
|
2026-05-12 14:59:31 -04:00
|
|
|
|
from typing import Any, Dict, List, Optional, Tuple
|
2026-03-18 03:04:07 -07:00
|
|
|
|
from urllib.parse import urlparse
|
2026-02-21 22:31:43 -08:00
|
|
|
|
|
|
|
|
|
|
import requests
|
2026-03-05 16:09:57 -08:00
|
|
|
|
import yaml
|
2026-02-21 22:31:43 -08:00
|
|
|
|
|
2026-06-14 04:45:46 -07:00
|
|
|
|
from utils import atomic_json_write, base_url_host_matches, base_url_hostname
|
fix: extend hostname-match provider detection across remaining call sites
Aslaaen's fix in the original PR covered _detect_api_mode_for_url and the
two openai/xai sites in run_agent.py. This finishes the sweep: the same
substring-match false-positive class (e.g. https://api.openai.com.evil/v1,
https://proxy/api.openai.com/v1, https://api.anthropic.com.example/v1)
existed in eight more call sites, and the hostname helper was duplicated
in two modules.
- utils: add shared base_url_hostname() (single source of truth).
- hermes_cli/runtime_provider, run_agent: drop local duplicates, import
from utils. Reuse the cached AIAgent._base_url_hostname attribute
everywhere it's already populated.
- agent/auxiliary_client: switch codex-wrap auto-detect, max_completion_tokens
gate (auxiliary_max_tokens_param), and custom-endpoint max_tokens kwarg
selection to hostname equality.
- run_agent: native-anthropic check in the Claude-style model branch
and in the AIAgent init provider-auto-detect branch.
- agent/model_metadata: Anthropic /v1/models context-length lookup.
- hermes_cli/providers.determine_api_mode: anthropic / openai URL
heuristics for custom/unknown providers (the /anthropic path-suffix
convention for third-party gateways is preserved).
- tools/delegate_tool: anthropic detection for delegated subagent
runtimes.
- hermes_cli/setup, hermes_cli/tools_config: setup-wizard vision-endpoint
native-OpenAI detection (paired with deduping the repeated check into
a single is_native_openai boolean per branch).
Tests:
- tests/test_base_url_hostname.py covers the helper directly
(path-containing-host, host-suffix, trailing dot, port, case).
- tests/hermes_cli/test_determine_api_mode_hostname.py adds the same
regression class for determine_api_mode, plus a test that the
/anthropic third-party gateway convention still wins.
Also: add asslaenn5@gmail.com → Aslaaen to scripts/release.py AUTHOR_MAP.
2026-04-20 20:58:01 -07:00
|
|
|
|
|
2026-02-21 22:31:43 -08:00
|
|
|
|
from hermes_constants import OPENROUTER_MODELS_URL
|
|
|
|
|
|
|
|
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
|
2026-04-23 14:59:26 +03:00
|
|
|
|
|
|
|
|
|
|
def _resolve_requests_verify() -> bool | str:
|
|
|
|
|
|
"""Resolve SSL verify setting for `requests` calls from env vars.
|
|
|
|
|
|
|
|
|
|
|
|
The `requests` library only honours REQUESTS_CA_BUNDLE / CURL_CA_BUNDLE
|
|
|
|
|
|
by default. Hermes also honours HERMES_CA_BUNDLE (its own convention)
|
|
|
|
|
|
and SSL_CERT_FILE (used by the stdlib `ssl` module and by httpx), so
|
|
|
|
|
|
that a single env var can cover both `requests` and `httpx` callsites
|
|
|
|
|
|
inside the same process.
|
|
|
|
|
|
|
|
|
|
|
|
Returns either a filesystem path to a CA bundle, or True to defer to
|
|
|
|
|
|
the requests default (certifi).
|
|
|
|
|
|
"""
|
|
|
|
|
|
for env_var in ("HERMES_CA_BUNDLE", "REQUESTS_CA_BUNDLE", "SSL_CERT_FILE"):
|
|
|
|
|
|
val = os.getenv(env_var)
|
|
|
|
|
|
if val and os.path.isfile(val):
|
|
|
|
|
|
return val
|
|
|
|
|
|
return True
|
|
|
|
|
|
|
2026-03-20 03:19:31 -07:00
|
|
|
|
# Provider names that can appear as a "provider:" prefix before a model ID.
|
|
|
|
|
|
# Only these are stripped — Ollama-style "model:tag" colons (e.g. "qwen3.5:27b")
|
|
|
|
|
|
# are preserved so the full model name reaches cache lookups and server queries.
|
|
|
|
|
|
_PROVIDER_PREFIXES: frozenset[str] = frozenset({
|
|
|
|
|
|
"openrouter", "nous", "openai-codex", "copilot", "copilot-acp",
|
2026-04-24 14:31:58 +00:00
|
|
|
|
"gemini", "ollama-cloud", "zai", "kimi-coding", "kimi-coding-cn", "stepfun", "minimax", "minimax-oauth", "minimax-cn", "anthropic", "deepseek",
|
remove Vercel AI Gateway and Vercel Sandbox (#33067)
* remove Vercel AI Gateway provider and Vercel Sandbox terminal backend
Both Vercel-hosted integrations are removed end-to-end. Users on the AI
Gateway should switch to OpenRouter or one of the other aggregators
(Nous Portal, Kilo Code). Users on the Vercel Sandbox backend should
switch to Docker, Modal, Daytona, or SSH.
What's removed:
- `plugins/model-providers/ai-gateway/` provider plugin
- `hermes_cli/vercel_auth.py` Vercel-Sandbox auth helper
- `tools/environments/vercel_sandbox.py` terminal backend
- `ai-gateway` provider wiring across auth, doctor, setup, models,
config, status, providers, main, web_server, model_normalize, dump
- `vercel_sandbox` backend wiring across terminal_tool, file_tools,
code_execution_tool, file_operations, approval, skills_tool,
environments/local, credential_files, lazy_deps, prompt_builder,
cli, gateway/run
- `AI_GATEWAY_BASE_URL` constant, `_AI_GATEWAY_HEADERS` auxiliary-client
header set, run_agent base-URL header/reasoning special-cases
- `[vercel]` pyproject extra and `vercel`/`vercel-workers` from uv.lock
- env vars: `AI_GATEWAY_API_KEY`, `AI_GATEWAY_BASE_URL`, `VERCEL_TOKEN`,
`VERCEL_PROJECT_ID`, `VERCEL_TEAM_ID`, `VERCEL_OIDC_TOKEN`,
`TERMINAL_VERCEL_RUNTIME`
- Tests: deletes test_ai_gateway_models.py and
test_vercel_sandbox_environment.py; scrubs references across 23
surviving test files (no entire tests deleted unless they were
dedicated to AI Gateway / Sandbox)
- Docs: provider tables, env-var reference, setup guides, security
notes, tool config, terminal-backend tables — English plus zh-Hans
i18n parity
- `hermes-agent` skill: provider table entry and remote-backend list
What stays (intentional):
- `popular-web-designs/templates/vercel.md` — CSS design reference,
unrelated to Vercel-the-AI-product
- `x-vercel-id` in `stream_diag.py` headers — generic Vercel CDN
response header, useful diag signal on any Vercel-hosted endpoint
- `vercel-labs/agent-browser` URL in browser config — lightpanda
browser project, different OSS effort
- `userStories.json` historical contributor entry mentioning Vercel
Sandbox — archive, not active docs
Validation:
- 1153 tests in the 22 targeted files pass (`scripts/run_tests.sh`)
- Full repo `py_compile` clean
- Live import of every touched module + invariant check (no
`ai-gateway` in `PROVIDER_REGISTRY`, no `_AI_GATEWAY_HEADERS`, no
`vercel_sandbox` in `_REMOTE_TERMINAL_BACKENDS`)
* test: convert profile-count check from change-detector to invariant
The hardcoded "== 34" assertion broke when ai-gateway was removed.
Per AGENTS.md change-detector-test guidance, assert the relationship
(registry count >= number of plugin dirs) instead of a literal count.
Counts shift when providers are added/removed; that's expected.
2026-05-27 00:43:32 -07:00
|
|
|
|
"opencode-zen", "opencode-go", "kilocode", "alibaba", "novita",
|
feat(qwen): add Qwen OAuth provider with portal request support
Based on #6079 by @tunamitom with critical fixes and comprehensive tests.
Changes from #6079:
- Fix: sanitization overwrite bug — Qwen message prep now runs AFTER codex
field sanitization, not before (was silently discarding Qwen transforms)
- Fix: missing try/except AuthError in runtime_provider.py — stale Qwen
credentials now fall through to next provider on auto-detect
- Fix: 'qwen' alias conflict — bare 'qwen' stays mapped to 'alibaba'
(DashScope); use 'qwen-portal' or 'qwen-cli' for the OAuth provider
- Fix: hardcoded ['coder-model'] replaced with live API fetch + curated
fallback list (qwen3-coder-plus, qwen3-coder)
- Fix: extract _is_qwen_portal() helper + _qwen_portal_headers() to replace
5 inline 'portal.qwen.ai' string checks and share headers between init
and credential swap
- Fix: add Qwen branch to _apply_client_headers_for_base_url for mid-session
credential swaps
- Fix: remove suspicious TypeError catch blocks around _prompt_provider_choice
- Fix: handle bare string items in content lists (were silently dropped)
- Fix: remove redundant dict() copies after deepcopy in message prep
- Revert: unrelated ai-gateway test mock removal and model_switch.py comment deletion
New tests (30 test functions):
- _qwen_cli_auth_path, _read_qwen_cli_tokens (success + 3 error paths)
- _save_qwen_cli_tokens (roundtrip, parent creation, permissions)
- _qwen_access_token_is_expiring (5 edge cases: fresh, expired, within skew,
None, non-numeric)
- _refresh_qwen_cli_tokens (success, preserve old refresh, 4 error paths,
default expires_in, disk persistence)
- resolve_qwen_runtime_credentials (fresh, auto-refresh, force-refresh,
missing token, env override)
- get_qwen_auth_status (logged in, not logged in)
- Runtime provider resolution (direct, pool entry, alias)
- _build_api_kwargs (metadata, vl_high_resolution_images, message formatting,
max_tokens suppression)
2026-04-08 20:48:21 +05:30
|
|
|
|
"qwen-oauth",
|
feat(xiaomi): add Xiaomi MiMo as first-class provider
Cherry-picked from PR #7702 by kshitijk4poor.
Adds Xiaomi MiMo as a direct provider (XIAOMI_API_KEY) with models:
- mimo-v2-pro (1M context), mimo-v2-omni (256K, multimodal), mimo-v2-flash (256K, cheapest)
Standard OpenAI-compatible provider checklist: auth.py, config.py, models.py,
main.py, providers.py, doctor.py, model_normalize.py, model_metadata.py,
models_dev.py, auxiliary_client.py, .env.example, cli-config.yaml.example.
Follow-up: vision tasks use mimo-v2-omni (multimodal) instead of the user's
main model. Non-vision aux uses the user's selected model. Added
_PROVIDER_VISION_MODELS dict for provider-specific vision model overrides.
On failure, falls back to aggregators (gemini flash) via existing fallback chain.
Corrects pre-existing context lengths: mimo-v2-pro 1048576→1000000,
mimo-v2-omni 1048576→256000, adds mimo-v2-flash 256000.
36 tests covering registry, aliases, auto-detect, credentials, models.dev,
normalization, URL mapping, providers module, doctor, aux client, vision
model override, and agent init.
2026-04-11 10:10:31 -07:00
|
|
|
|
"xiaomi",
|
feat(providers): add Arcee AI as direct API provider
Adds Arcee AI as a standard direct provider (ARCEEAI_API_KEY) with
Trinity models: trinity-large-thinking, trinity-large-preview, trinity-mini.
Standard OpenAI-compatible provider checklist: auth.py, config.py,
models.py, main.py, providers.py, doctor.py, model_normalize.py,
model_metadata.py, setup.py, trajectory_compressor.py.
Based on PR #9274 by arthurbr11, simplified to a standard direct
provider without dual-endpoint OpenRouter routing.
2026-04-13 17:16:43 -07:00
|
|
|
|
"arcee",
|
feat(providers): add GMI Cloud as a first-class API-key provider (#11955)
Add GMI Cloud (api.gmi-serving.com) as a full first-class API-key provider
with built-in auth, aliases, model catalog, CLI entry points, auxiliary client
routing, context length resolution, doctor checks, env var tracking, and docs.
- auth.py: ProviderConfig for 'gmi' (api_key, GMI_API_KEY / GMI_BASE_URL)
- providers.py: HermesOverlay with extra_env_vars for models.dev detection
- models.py: curated slash-form model catalog; live /v1/models fetch
- main.py: 'gmi' in _named_custom_provider_map and --provider choices
- model_metadata.py: _URL_TO_PROVIDER, _PROVIDER_PREFIXES, dedicated
context-length probe block (GMI's /models has authoritative data)
- auxiliary_client.py: alias entries; _compat_model fix for slash-form
models on cached aggregator-style clients; gmi aux default model
- doctor.py: GMI in provider connectivity checks
- config.py: GMI_API_KEY / GMI_BASE_URL in OPTIONAL_ENV_VARS
- conftest.py: explicit GMI_BASE_URL clearing (not caught by _API_KEY suffix)
- docs: providers.md, environment-variables.md, fallback-providers.md,
configuration.md, quickstart.md (expands provider table)
Co-authored-by: Isaac Huang <isaachuang@Isaacs-MacBook-Pro.local>
2026-04-17 11:19:56 -07:00
|
|
|
|
"gmi",
|
feat(providers): add tencent-tokenhub provider support
Registers tencent-tokenhub (https://tokenhub.tencentmaas.com/v1) as a
new API-key provider with model tencent/hy3-preview (256K context).
- PROVIDER_REGISTRY entry + TOKENHUB_API_KEY / TOKENHUB_BASE_URL env vars
- Aliases: tencent, tokenhub, tencent-cloud, tencentmaas
- openai_chat transport with is_tokenhub branch for top-level
reasoning_effort (Hy3 is a reasoning model)
- tencent/hy3-preview:free added to OpenRouter curated list
- 60+ tests (provider registry, aliases, runtime resolution,
credentials, model catalog, URL mapping, context length)
- Docs: integrations/providers.md, environment-variables.md,
model-catalog.json
Author: simonweng <simonweng@tencent.com>
Salvaged from PR #16860 onto current main (resolved conflicts with
#16935 Azure Anthropic env-var hint tests and the --provider choices=
list removal in chat_parser).
2026-04-28 03:40:45 -07:00
|
|
|
|
"tencent-tokenhub",
|
2026-03-20 03:19:31 -07:00
|
|
|
|
"custom", "local",
|
|
|
|
|
|
# Common aliases
|
2026-04-06 10:14:01 -07:00
|
|
|
|
"google", "google-gemini", "google-ai-studio",
|
2026-03-20 03:19:31 -07:00
|
|
|
|
"glm", "z-ai", "z.ai", "zhipu", "github", "github-copilot",
|
2026-04-13 11:13:09 -07:00
|
|
|
|
"github-models", "kimi", "moonshot", "kimi-cn", "moonshot-cn", "claude", "deep-seek",
|
2026-04-15 22:32:05 -07:00
|
|
|
|
"ollama",
|
remove Vercel AI Gateway and Vercel Sandbox (#33067)
* remove Vercel AI Gateway provider and Vercel Sandbox terminal backend
Both Vercel-hosted integrations are removed end-to-end. Users on the AI
Gateway should switch to OpenRouter or one of the other aggregators
(Nous Portal, Kilo Code). Users on the Vercel Sandbox backend should
switch to Docker, Modal, Daytona, or SSH.
What's removed:
- `plugins/model-providers/ai-gateway/` provider plugin
- `hermes_cli/vercel_auth.py` Vercel-Sandbox auth helper
- `tools/environments/vercel_sandbox.py` terminal backend
- `ai-gateway` provider wiring across auth, doctor, setup, models,
config, status, providers, main, web_server, model_normalize, dump
- `vercel_sandbox` backend wiring across terminal_tool, file_tools,
code_execution_tool, file_operations, approval, skills_tool,
environments/local, credential_files, lazy_deps, prompt_builder,
cli, gateway/run
- `AI_GATEWAY_BASE_URL` constant, `_AI_GATEWAY_HEADERS` auxiliary-client
header set, run_agent base-URL header/reasoning special-cases
- `[vercel]` pyproject extra and `vercel`/`vercel-workers` from uv.lock
- env vars: `AI_GATEWAY_API_KEY`, `AI_GATEWAY_BASE_URL`, `VERCEL_TOKEN`,
`VERCEL_PROJECT_ID`, `VERCEL_TEAM_ID`, `VERCEL_OIDC_TOKEN`,
`TERMINAL_VERCEL_RUNTIME`
- Tests: deletes test_ai_gateway_models.py and
test_vercel_sandbox_environment.py; scrubs references across 23
surviving test files (no entire tests deleted unless they were
dedicated to AI Gateway / Sandbox)
- Docs: provider tables, env-var reference, setup guides, security
notes, tool config, terminal-backend tables — English plus zh-Hans
i18n parity
- `hermes-agent` skill: provider table entry and remote-backend list
What stays (intentional):
- `popular-web-designs/templates/vercel.md` — CSS design reference,
unrelated to Vercel-the-AI-product
- `x-vercel-id` in `stream_diag.py` headers — generic Vercel CDN
response header, useful diag signal on any Vercel-hosted endpoint
- `vercel-labs/agent-browser` URL in browser config — lightpanda
browser project, different OSS effort
- `userStories.json` historical contributor entry mentioning Vercel
Sandbox — archive, not active docs
Validation:
- 1153 tests in the 22 targeted files pass (`scripts/run_tests.sh`)
- Full repo `py_compile` clean
- Live import of every touched module + invariant check (no
`ai-gateway` in `PROVIDER_REGISTRY`, no `_AI_GATEWAY_HEADERS`, no
`vercel_sandbox` in `_REMOTE_TERMINAL_BACKENDS`)
* test: convert profile-count check from change-detector to invariant
The hardcoded "== 34" assertion broke when ai-gateway was removed.
Per AGENTS.md change-detector-test guidance, assert the relationship
(registry count >= number of plugin dirs) instead of a literal count.
Counts shift when providers are added/removed; that's expected.
2026-05-27 00:43:32 -07:00
|
|
|
|
"stepfun", "opencode", "zen", "go", "kilo", "dashscope", "aliyun", "qwen",
|
feat(xiaomi): add Xiaomi MiMo as first-class provider
Cherry-picked from PR #7702 by kshitijk4poor.
Adds Xiaomi MiMo as a direct provider (XIAOMI_API_KEY) with models:
- mimo-v2-pro (1M context), mimo-v2-omni (256K, multimodal), mimo-v2-flash (256K, cheapest)
Standard OpenAI-compatible provider checklist: auth.py, config.py, models.py,
main.py, providers.py, doctor.py, model_normalize.py, model_metadata.py,
models_dev.py, auxiliary_client.py, .env.example, cli-config.yaml.example.
Follow-up: vision tasks use mimo-v2-omni (multimodal) instead of the user's
main model. Non-vision aux uses the user's selected model. Added
_PROVIDER_VISION_MODELS dict for provider-specific vision model overrides.
On failure, falls back to aggregators (gemini flash) via existing fallback chain.
Corrects pre-existing context lengths: mimo-v2-pro 1048576→1000000,
mimo-v2-omni 1048576→256000, adds mimo-v2-flash 256000.
36 tests covering registry, aliases, auto-detect, credentials, models.dev,
normalization, URL mapping, providers module, doctor, aux client, vision
model override, and agent init.
2026-04-11 10:10:31 -07:00
|
|
|
|
"mimo", "xiaomi-mimo",
|
feat(providers): add tencent-tokenhub provider support
Registers tencent-tokenhub (https://tokenhub.tencentmaas.com/v1) as a
new API-key provider with model tencent/hy3-preview (256K context).
- PROVIDER_REGISTRY entry + TOKENHUB_API_KEY / TOKENHUB_BASE_URL env vars
- Aliases: tencent, tokenhub, tencent-cloud, tencentmaas
- openai_chat transport with is_tokenhub branch for top-level
reasoning_effort (Hy3 is a reasoning model)
- tencent/hy3-preview:free added to OpenRouter curated list
- 60+ tests (provider registry, aliases, runtime resolution,
credentials, model catalog, URL mapping, context length)
- Docs: integrations/providers.md, environment-variables.md,
model-catalog.json
Author: simonweng <simonweng@tencent.com>
Salvaged from PR #16860 onto current main (resolved conflicts with
#16935 Azure Anthropic env-var hint tests and the --provider choices=
list removal in chat_parser).
2026-04-28 03:40:45 -07:00
|
|
|
|
"tencent", "tokenhub", "tencent-cloud", "tencentmaas",
|
feat(providers): add Arcee AI as direct API provider
Adds Arcee AI as a standard direct provider (ARCEEAI_API_KEY) with
Trinity models: trinity-large-thinking, trinity-large-preview, trinity-mini.
Standard OpenAI-compatible provider checklist: auth.py, config.py,
models.py, main.py, providers.py, doctor.py, model_normalize.py,
model_metadata.py, setup.py, trajectory_compressor.py.
Based on PR #9274 by arthurbr11, simplified to a standard direct
provider without dual-endpoint OpenRouter routing.
2026-04-13 17:16:43 -07:00
|
|
|
|
"arcee-ai", "arceeai",
|
feat(providers): add GMI Cloud as a first-class API-key provider (#11955)
Add GMI Cloud (api.gmi-serving.com) as a full first-class API-key provider
with built-in auth, aliases, model catalog, CLI entry points, auxiliary client
routing, context length resolution, doctor checks, env var tracking, and docs.
- auth.py: ProviderConfig for 'gmi' (api_key, GMI_API_KEY / GMI_BASE_URL)
- providers.py: HermesOverlay with extra_env_vars for models.dev detection
- models.py: curated slash-form model catalog; live /v1/models fetch
- main.py: 'gmi' in _named_custom_provider_map and --provider choices
- model_metadata.py: _URL_TO_PROVIDER, _PROVIDER_PREFIXES, dedicated
context-length probe block (GMI's /models has authoritative data)
- auxiliary_client.py: alias entries; _compat_model fix for slash-form
models on cached aggregator-style clients; gmi aux default model
- doctor.py: GMI in provider connectivity checks
- config.py: GMI_API_KEY / GMI_BASE_URL in OPTIONAL_ENV_VARS
- conftest.py: explicit GMI_BASE_URL clearing (not caught by _API_KEY suffix)
- docs: providers.md, environment-variables.md, fallback-providers.md,
configuration.md, quickstart.md (expands provider table)
Co-authored-by: Isaac Huang <isaachuang@Isaacs-MacBook-Pro.local>
2026-04-17 11:19:56 -07:00
|
|
|
|
"gmi-cloud", "gmicloud",
|
2026-04-14 16:43:23 -07:00
|
|
|
|
"xai", "x-ai", "x.ai", "grok",
|
feat(providers): add native NVIDIA NIM provider
Adds NVIDIA NIM as a first-class provider: ProviderConfig in
auth.py, HermesOverlay in providers.py, curated models
(Nemotron plus other open source models hosted on
build.nvidia.com), URL mapping in model_metadata.py, aliases
(nim, nvidia-nim, build-nvidia, nemotron), and env var tests.
Docs updated: providers page, quickstart table, fallback
providers table, and README provider list.
2026-04-17 09:55:58 -07:00
|
|
|
|
"nvidia", "nim", "nvidia-nim", "nemotron",
|
feat: add NovitaAI as LLM provider
Add NovitaAI as a first-class provider with dedicated model selection
flow, live pricing, and authoritative context length resolution.
- Register provider in PROVIDER_REGISTRY, HERMES_OVERLAYS, and all
alias/label maps (ID: novita, aliases: novita-ai, novitaai)
- Add dedicated _model_flow_novita() with 3-tier model list fallback:
Novita API → models.dev → static curated list
- Fetch live pricing from /v1/models with correct unit conversion
(input_token_price_per_m is 0.0001 USD per Mtok)
- Add Novita-specific context length resolution (step 4b) in
get_model_context_length(), prioritized over models.dev/OpenRouter
- Register api.novita.ai in _URL_TO_PROVIDER to prevent early return
from the custom-endpoint code path
- Add models.dev mapping (novita → novita-ai)
- Add default auxiliary model (deepseek/deepseek-v3-0324)
- Add NOVITA_API_KEY to test isolation (conftest.py)
- Update docs: providers page, env vars reference, CLI reference,
.env.example, README, and landing page
2026-04-10 22:22:47 +08:00
|
|
|
|
"qwen-portal", "novita-ai", "novitaai",
|
2026-03-20 03:19:31 -07:00
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-03-20 08:52:37 -07:00
|
|
|
|
_OLLAMA_TAG_PATTERN = re.compile(
|
|
|
|
|
|
r"^(\d+\.?\d*b|latest|stable|q\d|fp?\d|instruct|chat|coder|vision|text)",
|
|
|
|
|
|
re.IGNORECASE,
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-04-13 06:17:13 +02:00
|
|
|
|
# Tailscale's CGNAT range (RFC 6598). `ipaddress.is_private` excludes this
|
|
|
|
|
|
# block, so without an explicit check Ollama reached over Tailscale (e.g.
|
|
|
|
|
|
# `http://100.77.243.5:11434`) wouldn't be treated as local and its stream
|
|
|
|
|
|
# read / stale timeouts wouldn't get auto-bumped. Built once at import time.
|
|
|
|
|
|
_TAILSCALE_CGNAT = ipaddress.IPv4Network("100.64.0.0/10")
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-03-20 03:19:31 -07:00
|
|
|
|
def _strip_provider_prefix(model: str) -> str:
|
|
|
|
|
|
"""Strip a recognised provider prefix from a model string.
|
|
|
|
|
|
|
|
|
|
|
|
``"local:my-model"`` → ``"my-model"``
|
|
|
|
|
|
``"qwen3.5:27b"`` → ``"qwen3.5:27b"`` (unchanged — not a provider prefix)
|
2026-03-20 08:52:37 -07:00
|
|
|
|
``"qwen:0.5b"`` → ``"qwen:0.5b"`` (unchanged — Ollama model:tag)
|
|
|
|
|
|
``"deepseek:latest"``→ ``"deepseek:latest"``(unchanged — Ollama model:tag)
|
2026-03-20 03:19:31 -07:00
|
|
|
|
"""
|
|
|
|
|
|
if ":" not in model or model.startswith("http"):
|
|
|
|
|
|
return model
|
2026-03-20 08:52:37 -07:00
|
|
|
|
prefix, suffix = model.split(":", 1)
|
|
|
|
|
|
prefix_lower = prefix.strip().lower()
|
|
|
|
|
|
if prefix_lower in _PROVIDER_PREFIXES:
|
|
|
|
|
|
# Don't strip if suffix looks like an Ollama tag (e.g. "7b", "latest", "q4_0")
|
|
|
|
|
|
if _OLLAMA_TAG_PATTERN.match(suffix.strip()):
|
|
|
|
|
|
return model
|
|
|
|
|
|
return suffix
|
2026-03-20 03:19:31 -07:00
|
|
|
|
return model
|
|
|
|
|
|
|
2026-02-21 22:31:43 -08:00
|
|
|
|
_model_metadata_cache: Dict[str, Dict[str, Any]] = {}
|
|
|
|
|
|
_model_metadata_cache_time: float = 0
|
feat: add NovitaAI as LLM provider
Add NovitaAI as a first-class provider with dedicated model selection
flow, live pricing, and authoritative context length resolution.
- Register provider in PROVIDER_REGISTRY, HERMES_OVERLAYS, and all
alias/label maps (ID: novita, aliases: novita-ai, novitaai)
- Add dedicated _model_flow_novita() with 3-tier model list fallback:
Novita API → models.dev → static curated list
- Fetch live pricing from /v1/models with correct unit conversion
(input_token_price_per_m is 0.0001 USD per Mtok)
- Add Novita-specific context length resolution (step 4b) in
get_model_context_length(), prioritized over models.dev/OpenRouter
- Register api.novita.ai in _URL_TO_PROVIDER to prevent early return
from the custom-endpoint code path
- Add models.dev mapping (novita → novita-ai)
- Add default auxiliary model (deepseek/deepseek-v3-0324)
- Add NOVITA_API_KEY to test isolation (conftest.py)
- Update docs: providers page, env vars reference, CLI reference,
.env.example, README, and landing page
2026-04-10 22:22:47 +08:00
|
|
|
|
_novita_metadata_cache: Dict[str, Dict[str, Any]] = {}
|
|
|
|
|
|
_novita_metadata_cache_time: float = 0
|
2026-02-21 22:31:43 -08:00
|
|
|
|
_MODEL_CACHE_TTL = 3600
|
2026-03-18 03:04:07 -07:00
|
|
|
|
_endpoint_model_metadata_cache: Dict[str, Dict[str, Dict[str, Any]]] = {}
|
|
|
|
|
|
_endpoint_model_metadata_cache_time: Dict[str, float] = {}
|
|
|
|
|
|
_ENDPOINT_MODEL_CACHE_TTL = 300
|
2026-02-21 22:31:43 -08:00
|
|
|
|
|
2026-06-14 04:45:46 -07:00
|
|
|
|
|
|
|
|
|
|
def _get_model_metadata_cache_path() -> Path:
|
|
|
|
|
|
"""Return path to the OpenRouter model metadata disk cache."""
|
|
|
|
|
|
from hermes_constants import get_hermes_home
|
|
|
|
|
|
return get_hermes_home() / "cache" / "openrouter_model_metadata.json"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _model_metadata_disk_cache_age_seconds() -> Optional[float]:
|
|
|
|
|
|
"""Return disk-cache age in seconds, or None if freshness is unknown."""
|
|
|
|
|
|
try:
|
|
|
|
|
|
cache_path = _get_model_metadata_cache_path()
|
|
|
|
|
|
if not cache_path.exists():
|
|
|
|
|
|
return None
|
|
|
|
|
|
age = time.time() - cache_path.stat().st_mtime
|
|
|
|
|
|
if age < 0:
|
|
|
|
|
|
return None
|
|
|
|
|
|
return age
|
|
|
|
|
|
except Exception:
|
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _load_model_metadata_disk_cache() -> Dict[str, Dict[str, Any]]:
|
|
|
|
|
|
"""Load processed OpenRouter metadata cache from disk."""
|
|
|
|
|
|
try:
|
|
|
|
|
|
cache_path = _get_model_metadata_cache_path()
|
|
|
|
|
|
with cache_path.open("r", encoding="utf-8") as f:
|
|
|
|
|
|
data = json.load(f)
|
|
|
|
|
|
if not isinstance(data, dict):
|
|
|
|
|
|
return {}
|
|
|
|
|
|
return {
|
|
|
|
|
|
str(key): value
|
|
|
|
|
|
for key, value in data.items()
|
|
|
|
|
|
if isinstance(value, dict)
|
|
|
|
|
|
}
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
logger.debug("Failed to load OpenRouter model metadata disk cache: %s", e)
|
|
|
|
|
|
return {}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _save_model_metadata_disk_cache(data: Dict[str, Dict[str, Any]]) -> None:
|
|
|
|
|
|
"""Save processed OpenRouter metadata cache to disk atomically."""
|
|
|
|
|
|
try:
|
|
|
|
|
|
atomic_json_write(
|
|
|
|
|
|
_get_model_metadata_cache_path(),
|
|
|
|
|
|
data,
|
|
|
|
|
|
indent=0,
|
|
|
|
|
|
separators=(",", ":"),
|
|
|
|
|
|
)
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
logger.debug("Failed to save OpenRouter model metadata disk cache: %s", e)
|
|
|
|
|
|
|
2026-03-05 16:09:57 -08:00
|
|
|
|
# Descending tiers for context length probing when the model is unknown.
|
fix(context): honor custom_providers context_length on /model switch + bump probe tier to 256K (#15844)
Fixes #15779. Custom-provider per-model context_length (`custom_providers[].models.<id>.context_length`) is now honored across every resolution path, not just agent startup. Also adds 256K as the top probe tier and default fallback.
## What changed
New helper `hermes_cli.config.get_custom_provider_context_length()` — single source of truth for the per-model override lookup, with trailing-slash-insensitive base-url matching.
`agent.model_metadata.get_model_context_length()` gains an optional `custom_providers=` kwarg (step 0b — runs after explicit `config_context_length` but before every other probe).
Wired through five call sites that previously either duplicated the lookup or ignored it entirely:
- `run_agent.py` startup — refactored to use the new helper (dedups legacy inline loop, keeps invalid-value warning)
- `AIAgent.switch_model()` — re-reads custom_providers from live config on every /model switch
- `hermes_cli.model_switch.resolve_display_context_length()` — new `custom_providers=` kwarg
- `gateway/run.py` /model confirmation (picker callback + text path)
- `gateway/run.py` `_format_session_info` (/info)
## Context probe tiers
`CONTEXT_PROBE_TIERS = [256_000, 128_000, 64_000, 32_000, 16_000, 8_000]` — was `[128_000, ...]`. `DEFAULT_FALLBACK_CONTEXT` follows tier[0], so unknown models now default to 256K. The stale `128000` literal in the OpenRouter metadata-miss path is replaced with `DEFAULT_FALLBACK_CONTEXT` for consistency.
## Repro (from #15779)
```yaml
custom_providers:
- name: my-custom-endpoint
base_url: https://example.invalid/v1
model: gpt-5.5
models:
gpt-5.5:
context_length: 1050000
```
`/model gpt-5.5 --provider custom:my-custom-endpoint` → previously "Context: 128,000", now "Context: 1,050,000".
## Tests
- `tests/hermes_cli/test_custom_provider_context_length.py` — new file, 19 tests covering the helper, step-0b integration, and the 256K tier invariants
- `tests/hermes_cli/test_model_switch_context_display.py` — added regression tests for #15779 through the display resolver
- `tests/gateway/test_session_info.py` — updated default-fallback assertion (128K → 256K)
- `tests/agent/test_model_metadata.py` — updated tier assertions for the new top tier
2026-04-25 18:47:53 -07:00
|
|
|
|
# We start at 256K (covers GPT-5.x, many current large-context models) and
|
|
|
|
|
|
# step down on context-length errors until one works. Tier[0] is also the
|
|
|
|
|
|
# default fallback when no detection method succeeds.
|
2026-03-05 16:09:57 -08:00
|
|
|
|
CONTEXT_PROBE_TIERS = [
|
fix(context): honor custom_providers context_length on /model switch + bump probe tier to 256K (#15844)
Fixes #15779. Custom-provider per-model context_length (`custom_providers[].models.<id>.context_length`) is now honored across every resolution path, not just agent startup. Also adds 256K as the top probe tier and default fallback.
## What changed
New helper `hermes_cli.config.get_custom_provider_context_length()` — single source of truth for the per-model override lookup, with trailing-slash-insensitive base-url matching.
`agent.model_metadata.get_model_context_length()` gains an optional `custom_providers=` kwarg (step 0b — runs after explicit `config_context_length` but before every other probe).
Wired through five call sites that previously either duplicated the lookup or ignored it entirely:
- `run_agent.py` startup — refactored to use the new helper (dedups legacy inline loop, keeps invalid-value warning)
- `AIAgent.switch_model()` — re-reads custom_providers from live config on every /model switch
- `hermes_cli.model_switch.resolve_display_context_length()` — new `custom_providers=` kwarg
- `gateway/run.py` /model confirmation (picker callback + text path)
- `gateway/run.py` `_format_session_info` (/info)
## Context probe tiers
`CONTEXT_PROBE_TIERS = [256_000, 128_000, 64_000, 32_000, 16_000, 8_000]` — was `[128_000, ...]`. `DEFAULT_FALLBACK_CONTEXT` follows tier[0], so unknown models now default to 256K. The stale `128000` literal in the OpenRouter metadata-miss path is replaced with `DEFAULT_FALLBACK_CONTEXT` for consistency.
## Repro (from #15779)
```yaml
custom_providers:
- name: my-custom-endpoint
base_url: https://example.invalid/v1
model: gpt-5.5
models:
gpt-5.5:
context_length: 1050000
```
`/model gpt-5.5 --provider custom:my-custom-endpoint` → previously "Context: 128,000", now "Context: 1,050,000".
## Tests
- `tests/hermes_cli/test_custom_provider_context_length.py` — new file, 19 tests covering the helper, step-0b integration, and the 256K tier invariants
- `tests/hermes_cli/test_model_switch_context_display.py` — added regression tests for #15779 through the display resolver
- `tests/gateway/test_session_info.py` — updated default-fallback assertion (128K → 256K)
- `tests/agent/test_model_metadata.py` — updated tier assertions for the new top tier
2026-04-25 18:47:53 -07:00
|
|
|
|
256_000,
|
2026-03-05 16:09:57 -08:00
|
|
|
|
128_000,
|
|
|
|
|
|
64_000,
|
|
|
|
|
|
32_000,
|
feat: overhaul context length detection with models.dev and provider-aware resolution (#2158)
Replace the fragile hardcoded context length system with a multi-source
resolution chain that correctly identifies context windows per provider.
Key changes:
- New agent/models_dev.py: Fetches and caches the models.dev registry
(3800+ models across 100+ providers with per-provider context windows).
In-memory cache (1hr TTL) + disk cache for cold starts.
- Rewritten get_model_context_length() resolution chain:
0. Config override (model.context_length)
1. Custom providers per-model context_length
2. Persistent disk cache
3. Endpoint /models (local servers)
4. Anthropic /v1/models API (max_input_tokens, API-key only)
5. OpenRouter live API (existing, unchanged)
6. Nous suffix-match via OpenRouter (dot/dash normalization)
7. models.dev registry lookup (provider-aware)
8. Thin hardcoded defaults (broad family patterns)
9. 128K fallback (was 2M)
- Provider-aware context: same model now correctly resolves to different
context windows per provider (e.g. claude-opus-4.6: 1M on Anthropic,
128K on GitHub Copilot). Provider name flows through ContextCompressor.
- DEFAULT_CONTEXT_LENGTHS shrunk from 80+ entries to ~16 broad patterns.
models.dev replaces the per-model hardcoding.
- CONTEXT_PROBE_TIERS changed from [2M, 1M, 512K, 200K, 128K, 64K, 32K]
to [128K, 64K, 32K, 16K, 8K]. Unknown models no longer start at 2M.
- hermes model: prompts for context_length when configuring custom
endpoints. Supports shorthand (32k, 128K). Saved to custom_providers
per-model config.
- custom_providers schema extended with optional models dict for
per-model context_length (backward compatible).
- Nous Portal: suffix-matches bare IDs (claude-opus-4-6) against
OpenRouter's prefixed IDs (anthropic/claude-opus-4.6) with dot/dash
normalization. Handles all 15 current Nous models.
- Anthropic direct: queries /v1/models for max_input_tokens. Only works
with regular API keys (sk-ant-api*), not OAuth tokens. Falls through
to models.dev for OAuth users.
Tests: 5574 passed (18 new tests for models_dev + updated probe tiers)
Docs: Updated configuration.md context length section, AGENTS.md
Co-authored-by: Test <test@test.com>
2026-03-20 06:04:33 -07:00
|
|
|
|
16_000,
|
|
|
|
|
|
8_000,
|
2026-03-05 16:09:57 -08:00
|
|
|
|
]
|
|
|
|
|
|
|
feat: overhaul context length detection with models.dev and provider-aware resolution (#2158)
Replace the fragile hardcoded context length system with a multi-source
resolution chain that correctly identifies context windows per provider.
Key changes:
- New agent/models_dev.py: Fetches and caches the models.dev registry
(3800+ models across 100+ providers with per-provider context windows).
In-memory cache (1hr TTL) + disk cache for cold starts.
- Rewritten get_model_context_length() resolution chain:
0. Config override (model.context_length)
1. Custom providers per-model context_length
2. Persistent disk cache
3. Endpoint /models (local servers)
4. Anthropic /v1/models API (max_input_tokens, API-key only)
5. OpenRouter live API (existing, unchanged)
6. Nous suffix-match via OpenRouter (dot/dash normalization)
7. models.dev registry lookup (provider-aware)
8. Thin hardcoded defaults (broad family patterns)
9. 128K fallback (was 2M)
- Provider-aware context: same model now correctly resolves to different
context windows per provider (e.g. claude-opus-4.6: 1M on Anthropic,
128K on GitHub Copilot). Provider name flows through ContextCompressor.
- DEFAULT_CONTEXT_LENGTHS shrunk from 80+ entries to ~16 broad patterns.
models.dev replaces the per-model hardcoding.
- CONTEXT_PROBE_TIERS changed from [2M, 1M, 512K, 200K, 128K, 64K, 32K]
to [128K, 64K, 32K, 16K, 8K]. Unknown models no longer start at 2M.
- hermes model: prompts for context_length when configuring custom
endpoints. Supports shorthand (32k, 128K). Saved to custom_providers
per-model config.
- custom_providers schema extended with optional models dict for
per-model context_length (backward compatible).
- Nous Portal: suffix-matches bare IDs (claude-opus-4-6) against
OpenRouter's prefixed IDs (anthropic/claude-opus-4.6) with dot/dash
normalization. Handles all 15 current Nous models.
- Anthropic direct: queries /v1/models for max_input_tokens. Only works
with regular API keys (sk-ant-api*), not OAuth tokens. Falls through
to models.dev for OAuth users.
Tests: 5574 passed (18 new tests for models_dev + updated probe tiers)
Docs: Updated configuration.md context length section, AGENTS.md
Co-authored-by: Test <test@test.com>
2026-03-20 06:04:33 -07:00
|
|
|
|
# Default context length when no detection method succeeds.
|
|
|
|
|
|
DEFAULT_FALLBACK_CONTEXT = CONTEXT_PROBE_TIERS[0]
|
|
|
|
|
|
|
fix: prevent agent from stopping mid-task — compression floor, budget overhaul, activity tracking
Three root causes of the 'agent stops mid-task' gateway bug:
1. Compression threshold floor (64K tokens minimum)
- The 50% threshold on a 100K-context model fired at 50K tokens,
causing premature compression that made models lose track of
multi-step plans. Now threshold_tokens = max(50% * context, 64K).
- Models with <64K context are rejected at startup with a clear error.
2. Budget warning removal — grace call instead
- Removed the 70%/90% iteration budget warnings entirely. These
injected '[BUDGET WARNING: Provide your final response NOW]' into
tool results, causing models to abandon complex tasks prematurely.
- Now: no warnings during normal execution. When the budget is
actually exhausted (90/90), inject a user message asking the model
to summarise, allow one grace API call, and only then fall back
to _handle_max_iterations.
3. Activity touches during long terminal execution
- _wait_for_process polls every 0.2s but never reported activity.
The gateway's inactivity timeout (default 1800s) would fire during
long-running commands that appeared 'idle.'
- Now: thread-local activity callback fires every 10s during the
poll loop, keeping the gateway's activity tracker alive.
- Agent wires _touch_activity into the callback before each tool call.
Also: docs update noting 64K minimum context requirement.
Closes #7915 (root cause was agent-loop termination, not Weixin delivery limits).
2026-04-11 16:18:57 -07:00
|
|
|
|
# Minimum context length required to run Hermes Agent. Models with fewer
|
|
|
|
|
|
# tokens cannot maintain enough working memory for tool-calling workflows.
|
|
|
|
|
|
# Sessions, model switches, and cron jobs should reject models below this.
|
|
|
|
|
|
MINIMUM_CONTEXT_LENGTH = 64_000
|
|
|
|
|
|
|
feat: overhaul context length detection with models.dev and provider-aware resolution (#2158)
Replace the fragile hardcoded context length system with a multi-source
resolution chain that correctly identifies context windows per provider.
Key changes:
- New agent/models_dev.py: Fetches and caches the models.dev registry
(3800+ models across 100+ providers with per-provider context windows).
In-memory cache (1hr TTL) + disk cache for cold starts.
- Rewritten get_model_context_length() resolution chain:
0. Config override (model.context_length)
1. Custom providers per-model context_length
2. Persistent disk cache
3. Endpoint /models (local servers)
4. Anthropic /v1/models API (max_input_tokens, API-key only)
5. OpenRouter live API (existing, unchanged)
6. Nous suffix-match via OpenRouter (dot/dash normalization)
7. models.dev registry lookup (provider-aware)
8. Thin hardcoded defaults (broad family patterns)
9. 128K fallback (was 2M)
- Provider-aware context: same model now correctly resolves to different
context windows per provider (e.g. claude-opus-4.6: 1M on Anthropic,
128K on GitHub Copilot). Provider name flows through ContextCompressor.
- DEFAULT_CONTEXT_LENGTHS shrunk from 80+ entries to ~16 broad patterns.
models.dev replaces the per-model hardcoding.
- CONTEXT_PROBE_TIERS changed from [2M, 1M, 512K, 200K, 128K, 64K, 32K]
to [128K, 64K, 32K, 16K, 8K]. Unknown models no longer start at 2M.
- hermes model: prompts for context_length when configuring custom
endpoints. Supports shorthand (32k, 128K). Saved to custom_providers
per-model config.
- custom_providers schema extended with optional models dict for
per-model context_length (backward compatible).
- Nous Portal: suffix-matches bare IDs (claude-opus-4-6) against
OpenRouter's prefixed IDs (anthropic/claude-opus-4.6) with dot/dash
normalization. Handles all 15 current Nous models.
- Anthropic direct: queries /v1/models for max_input_tokens. Only works
with regular API keys (sk-ant-api*), not OAuth tokens. Falls through
to models.dev for OAuth users.
Tests: 5574 passed (18 new tests for models_dev + updated probe tiers)
Docs: Updated configuration.md context length section, AGENTS.md
Co-authored-by: Test <test@test.com>
2026-03-20 06:04:33 -07:00
|
|
|
|
# Thin fallback defaults — only broad model family patterns.
|
|
|
|
|
|
# These fire only when provider is unknown AND models.dev/OpenRouter/Anthropic
|
|
|
|
|
|
# all miss. Replaced the previous 80+ entry dict.
|
|
|
|
|
|
# For provider-specific context lengths, models.dev is the primary source.
|
2026-02-21 22:31:43 -08:00
|
|
|
|
DEFAULT_CONTEXT_LENGTHS = {
|
feat: overhaul context length detection with models.dev and provider-aware resolution (#2158)
Replace the fragile hardcoded context length system with a multi-source
resolution chain that correctly identifies context windows per provider.
Key changes:
- New agent/models_dev.py: Fetches and caches the models.dev registry
(3800+ models across 100+ providers with per-provider context windows).
In-memory cache (1hr TTL) + disk cache for cold starts.
- Rewritten get_model_context_length() resolution chain:
0. Config override (model.context_length)
1. Custom providers per-model context_length
2. Persistent disk cache
3. Endpoint /models (local servers)
4. Anthropic /v1/models API (max_input_tokens, API-key only)
5. OpenRouter live API (existing, unchanged)
6. Nous suffix-match via OpenRouter (dot/dash normalization)
7. models.dev registry lookup (provider-aware)
8. Thin hardcoded defaults (broad family patterns)
9. 128K fallback (was 2M)
- Provider-aware context: same model now correctly resolves to different
context windows per provider (e.g. claude-opus-4.6: 1M on Anthropic,
128K on GitHub Copilot). Provider name flows through ContextCompressor.
- DEFAULT_CONTEXT_LENGTHS shrunk from 80+ entries to ~16 broad patterns.
models.dev replaces the per-model hardcoding.
- CONTEXT_PROBE_TIERS changed from [2M, 1M, 512K, 200K, 128K, 64K, 32K]
to [128K, 64K, 32K, 16K, 8K]. Unknown models no longer start at 2M.
- hermes model: prompts for context_length when configuring custom
endpoints. Supports shorthand (32k, 128K). Saved to custom_providers
per-model config.
- custom_providers schema extended with optional models dict for
per-model context_length (backward compatible).
- Nous Portal: suffix-matches bare IDs (claude-opus-4-6) against
OpenRouter's prefixed IDs (anthropic/claude-opus-4.6) with dot/dash
normalization. Handles all 15 current Nous models.
- Anthropic direct: queries /v1/models for max_input_tokens. Only works
with regular API keys (sk-ant-api*), not OAuth tokens. Falls through
to models.dev for OAuth users.
Tests: 5574 passed (18 new tests for models_dev + updated probe tiers)
Docs: Updated configuration.md context length section, AGENTS.md
Co-authored-by: Test <test@test.com>
2026-03-20 06:04:33 -07:00
|
|
|
|
# Anthropic Claude 4.6 (1M context) — bare IDs only to avoid
|
|
|
|
|
|
# fuzzy-match collisions (e.g. "anthropic/claude-sonnet-4" is a
|
|
|
|
|
|
# substring of "anthropic/claude-sonnet-4.6").
|
|
|
|
|
|
# OpenRouter-prefixed models resolve via OpenRouter live API or models.dev.
|
2026-06-09 23:37:23 +05:30
|
|
|
|
"claude-fable-5": 1000000,
|
|
|
|
|
|
"claude-fable": 1000000,
|
feat: add claude-opus-4.8 and claude-opus-4.8-fast (#34003)
Anthropic released Claude Opus 4.8 on 2026-05-27, available on
OpenRouter, Anthropic, Amazon Bedrock, and Claude Platform on AWS:
- https://openrouter.ai/anthropic/claude-opus-4.8
- https://openrouter.ai/anthropic/claude-opus-4.8-fast
The fast-mode variant is a separate model ID (anthropic/claude-opus-4.8-fast)
priced at 2x of the base model — a notable improvement over the 6x premium
on older Opus generations (4.6/4.7). It is NOT a `speed: "fast"` request
parameter like Opus 4.6; Anthropic's native fast-mode beta still only
covers Opus 4.6.
Changes:
hermes_cli/models.py
- Add anthropic/claude-opus-4.8 + anthropic/claude-opus-4.8-fast to
the OpenRouter fallback snapshot and the Nous Portal curated list
(live catalogs surface them automatically when reachable; the
fallback list matters when the manifest fetch fails).
- Add claude-opus-4-8 to the Anthropic-native picker list.
agent/model_metadata.py
- Register claude-opus-4-8 / claude-opus-4.8 in DEFAULT_CONTEXT_LENGTHS
with 1M tokens (matches 4.6/4.7).
agent/anthropic_adapter.py
- Extend _XHIGH_EFFORT_SUBSTRINGS, _ADAPTIVE_THINKING_SUBSTRINGS, and
_NO_SAMPLING_PARAMS_SUBSTRINGS with "4-8"/"4.8". 4.8 inherits the
Opus 4.7 API contract: adaptive thinking only, xhigh effort level
supported, sampling parameters (temperature/top_p/top_k) return 400.
- Add claude-opus-4-8 to _ANTHROPIC_OUTPUT_LIMITS (128k max output,
same as 4.7). Matches by substring so claude-opus-4-8-fast and
date-stamped variants resolve correctly.
agent/usage_pricing.py
- Add anthropic/claude-opus-4-8: $5/$25 per MTok input/output, $0.50
cache read, $6.25 cache write (same as 4.6/4.7).
- Add anthropic/claude-opus-4-8-fast: $10/$50 per MTok (2x), $1.00
cache read, $12.50 cache write. Per OpenRouter, the 2x premium is
the only differentiator from regular Opus 4.8.
- OpenRouter routes still pull pricing from the live /models API, so
no static OpenRouter entry is needed.
tests/agent/test_model_metadata.py
- Extend the Claude 4.6+ context-length tag list with 4.8/4-8.
website/static/api/model-catalog.json
- Regenerated via `python scripts/build_model_catalog.py` to pick up
the new entries in the OpenRouter and Nous Portal fallback lists.
E2E verification (isolated sys.path import against the worktree):
- _supports_adaptive_thinking, _supports_xhigh_effort, _forbids_sampling_params
all return True for claude-opus-4.8 and claude-opus-4.8-fast.
- _supports_fast_mode (the `speed: "fast"` request-parameter gate) stays
False for 4.8 — fast mode is a separate model ID on OpenRouter, not a
parameter Anthropic accepts on the base model.
- DEFAULT_CONTEXT_LENGTHS resolves 1M for both notations.
- resolve_billing_route + _lookup_official_docs_pricing resolve the
correct $5/$25 (regular) and $10/$50 (fast) pricing for both
dot-notation and dash-notation inputs.
- 4.7 and 4.6 regression: behavior unchanged.
Unit tests: 305 passed across tests/agent/test_usage_pricing.py,
test_model_metadata.py, tests/hermes_cli/test_model_catalog.py,
test_models.py, test_model_validation.py, test_models_dev_preferred_merge.py.
2026-05-28 10:31:59 -07:00
|
|
|
|
"claude-opus-4-8": 1000000,
|
|
|
|
|
|
"claude-opus-4.8": 1000000,
|
fix(agent): complete Claude Opus 4.7 API migration
Claude Opus 4.7 introduced several breaking API changes that the current
codebase partially handled but not completely. This patch finishes the
migration per the official migration guide at
https://platform.claude.com/docs/en/about-claude/models/migration-guide
Fixes NousResearch/hermes-agent#11137
Breaking-change coverage:
1. Adaptive thinking + output_config.effort — 4.7 is now recognized by
_supports_adaptive_thinking() (extends previous 4.6-only gate).
2. Sampling parameter stripping — 4.7 returns 400 for any non-default
temperature / top_p / top_k. build_anthropic_kwargs drops them as a
safety net; the OpenAI-protocol auxiliary path (_build_call_kwargs)
and AnthropicCompletionsAdapter.create() both early-exit before
setting temperature for 4.7+ models. This keeps flush_memories and
structured-JSON aux paths that hardcode temperature from 400ing
when the aux model is flipped to 4.7.
3. thinking.display = "summarized" — 4.7 defaults display to "omitted",
which silently hides reasoning text from Hermes's CLI activity feed
during long tool runs. Restoring "summarized" preserves 4.6 UX.
4. Effort level mapping — xhigh now maps to xhigh (was xhigh→max, which
silently over-efforted every coding/agentic request). max is now a
distinct ceiling per Anthropic's 5-level effort model.
5. New stop_reason values — refusal and model_context_window_exceeded
were silently collapsed to "stop" (end_turn) by the adapter's
stop_reason_map. Now mapped to "content_filter" and "length"
respectively, matching upstream finish-reason handling already in
bedrock_adapter.
6. Model catalogs — claude-opus-4-7 added to the Anthropic provider
list, anthropic/claude-opus-4.7 added at top of OpenRouter fallback
catalog (recommended), claude-opus-4-7 added to model_metadata
DEFAULT_CONTEXT_LENGTHS (1M, matching 4.6 per migration guide).
7. Prefill docstrings — run_agent.AIAgent and BatchRunner now document
that Anthropic Sonnet/Opus 4.6+ reject a trailing assistant-role
prefill (400).
8. Tests — 4 new tests in test_anthropic_adapter covering display
default, xhigh preservation, max on 4.7, refusal / context-overflow
stop_reason mapping, plus the sampling-param predicate. test_model_metadata
accepts 4.7 at 1M context.
Tested on macOS 15.5 (darwin). 119 tests pass in
tests/agent/test_anthropic_adapter.py, 1320 pass in tests/agent/.
2026-04-16 12:35:43 -05:00
|
|
|
|
"claude-opus-4-7": 1000000,
|
|
|
|
|
|
"claude-opus-4.7": 1000000,
|
2026-03-20 04:38:59 -07:00
|
|
|
|
"claude-opus-4-6": 1000000,
|
|
|
|
|
|
"claude-sonnet-4-6": 1000000,
|
feat: overhaul context length detection with models.dev and provider-aware resolution (#2158)
Replace the fragile hardcoded context length system with a multi-source
resolution chain that correctly identifies context windows per provider.
Key changes:
- New agent/models_dev.py: Fetches and caches the models.dev registry
(3800+ models across 100+ providers with per-provider context windows).
In-memory cache (1hr TTL) + disk cache for cold starts.
- Rewritten get_model_context_length() resolution chain:
0. Config override (model.context_length)
1. Custom providers per-model context_length
2. Persistent disk cache
3. Endpoint /models (local servers)
4. Anthropic /v1/models API (max_input_tokens, API-key only)
5. OpenRouter live API (existing, unchanged)
6. Nous suffix-match via OpenRouter (dot/dash normalization)
7. models.dev registry lookup (provider-aware)
8. Thin hardcoded defaults (broad family patterns)
9. 128K fallback (was 2M)
- Provider-aware context: same model now correctly resolves to different
context windows per provider (e.g. claude-opus-4.6: 1M on Anthropic,
128K on GitHub Copilot). Provider name flows through ContextCompressor.
- DEFAULT_CONTEXT_LENGTHS shrunk from 80+ entries to ~16 broad patterns.
models.dev replaces the per-model hardcoding.
- CONTEXT_PROBE_TIERS changed from [2M, 1M, 512K, 200K, 128K, 64K, 32K]
to [128K, 64K, 32K, 16K, 8K]. Unknown models no longer start at 2M.
- hermes model: prompts for context_length when configuring custom
endpoints. Supports shorthand (32k, 128K). Saved to custom_providers
per-model config.
- custom_providers schema extended with optional models dict for
per-model context_length (backward compatible).
- Nous Portal: suffix-matches bare IDs (claude-opus-4-6) against
OpenRouter's prefixed IDs (anthropic/claude-opus-4.6) with dot/dash
normalization. Handles all 15 current Nous models.
- Anthropic direct: queries /v1/models for max_input_tokens. Only works
with regular API keys (sk-ant-api*), not OAuth tokens. Falls through
to models.dev for OAuth users.
Tests: 5574 passed (18 new tests for models_dev + updated probe tiers)
Docs: Updated configuration.md context length section, AGENTS.md
Co-authored-by: Test <test@test.com>
2026-03-20 06:04:33 -07:00
|
|
|
|
"claude-opus-4.6": 1000000,
|
|
|
|
|
|
"claude-sonnet-4.6": 1000000,
|
|
|
|
|
|
# Catch-all for older Claude models (must sort after specific entries)
|
|
|
|
|
|
"claude": 200000,
|
fix: correct GPT-5 family context lengths in fallback defaults (#9309)
The generic 'gpt-5' fallback was set to 128,000 — which is the max
OUTPUT tokens, not the context window. GPT-5 base and most variants
(codex, mini) have 400,000 context. This caused /model to report
128k for models like gpt-5.3-codex when models.dev was unavailable.
Added specific entries for GPT-5 variants with different context sizes:
- gpt-5.4, gpt-5.4-pro: 1,050,000 (1.05M)
- gpt-5.4-mini, gpt-5.4-nano: 400,000
- gpt-5.3-codex-spark: 128,000 (reduced)
- gpt-5.1-chat: 128,000 (chat variant)
- gpt-5 (catch-all): 400,000
Sources: https://developers.openai.com/api/docs/models
2026-04-13 19:22:23 -07:00
|
|
|
|
# OpenAI — GPT-5 family (most have 400k; specific overrides first)
|
|
|
|
|
|
# Source: https://developers.openai.com/api/docs/models
|
fix(cli): /model picker honors provider-specific context caps (#16030)
`_apply_model_switch_result` (the interactive `/model` picker's
confirmation path) printed `ModelInfo.context_window` straight from
models.dev, which reports the vendor-wide value (1.05M for gpt-5.5 on
openai). ChatGPT Codex OAuth caps the same slug at 272K, so the picker
showed 1M while the runtime (compressor, gateway `/model`, typed
`/model <name>`) correctly used 272K — the classic 'sometimes 1M,
sometimes 272K' mismatch on a single model.
Both display paths now go through `resolve_display_context_length()`,
matching the fix that `_handle_model_switch` received earlier.
Also bump the stale last-resort fallback in DEFAULT_CONTEXT_LENGTHS
(`gpt-5.5: 400000 -> 1050000`) to match the real OpenAI API value; the
272K Codex cap is already enforced via the Codex-OAuth branch, so the
fallback now reflects what every non-Codex probe-miss should see.
Tests: adds `test_apply_model_switch_result_context.py` with three
scenarios (Codex cap wins, OpenRouter shows 1.05M, resolver-empty falls
back to ModelInfo). Updates the existing non-Codex fallback test to
assert 1.05M (the correct value).
## Validation
| path | before | after |
|-------------------------------|-----------|-----------|
| picker -> gpt-5.5 on Codex | 1,050,000 | 272,000 |
| picker -> gpt-5.5 on OpenAI | 1,050,000 | 1,050,000 |
| picker -> gpt-5.5 on OpenRouter | 1,050,000 | 1,050,000 |
| typed /model gpt-5.5 on Codex | 272,000 | 272,000 |
2026-04-26 05:43:31 -07:00
|
|
|
|
# GPT-5.5 (launched Apr 23 2026) is 1.05M on the direct OpenAI API and
|
|
|
|
|
|
# ChatGPT Codex OAuth caps it at 272K; both paths resolve via their own
|
|
|
|
|
|
# provider-aware branches (_resolve_codex_oauth_context_length + models.dev).
|
|
|
|
|
|
# This hardcoded value is only reached when every probe misses.
|
|
|
|
|
|
"gpt-5.5": 1050000,
|
fix: correct GPT-5 family context lengths in fallback defaults (#9309)
The generic 'gpt-5' fallback was set to 128,000 — which is the max
OUTPUT tokens, not the context window. GPT-5 base and most variants
(codex, mini) have 400,000 context. This caused /model to report
128k for models like gpt-5.3-codex when models.dev was unavailable.
Added specific entries for GPT-5 variants with different context sizes:
- gpt-5.4, gpt-5.4-pro: 1,050,000 (1.05M)
- gpt-5.4-mini, gpt-5.4-nano: 400,000
- gpt-5.3-codex-spark: 128,000 (reduced)
- gpt-5.1-chat: 128,000 (chat variant)
- gpt-5 (catch-all): 400,000
Sources: https://developers.openai.com/api/docs/models
2026-04-13 19:22:23 -07:00
|
|
|
|
"gpt-5.4-nano": 400000, # 400k (not 1.05M like full 5.4)
|
|
|
|
|
|
"gpt-5.4-mini": 400000, # 400k (not 1.05M like full 5.4)
|
|
|
|
|
|
"gpt-5.4": 1050000, # GPT-5.4, GPT-5.4 Pro (1.05M context)
|
2026-05-10 11:44:11 +05:30
|
|
|
|
# gpt-5.3-codex-spark is Codex-OAuth-only (ChatGPT Pro entitlement) and
|
|
|
|
|
|
# uses a smaller 128k window than other gpt-5.x slugs. Listed here as
|
|
|
|
|
|
# a defensive override so the longest-substring fallback doesn't match
|
|
|
|
|
|
# the generic "gpt-5" entry below (400k) and report the wrong limit if
|
|
|
|
|
|
# Spark's context ever needs to be resolved through this path. Real
|
|
|
|
|
|
# usage flows through _CODEX_OAUTH_CONTEXT_FALLBACK at line ~1113.
|
|
|
|
|
|
"gpt-5.3-codex-spark": 128000,
|
fix: correct GPT-5 family context lengths in fallback defaults (#9309)
The generic 'gpt-5' fallback was set to 128,000 — which is the max
OUTPUT tokens, not the context window. GPT-5 base and most variants
(codex, mini) have 400,000 context. This caused /model to report
128k for models like gpt-5.3-codex when models.dev was unavailable.
Added specific entries for GPT-5 variants with different context sizes:
- gpt-5.4, gpt-5.4-pro: 1,050,000 (1.05M)
- gpt-5.4-mini, gpt-5.4-nano: 400,000
- gpt-5.3-codex-spark: 128,000 (reduced)
- gpt-5.1-chat: 128,000 (chat variant)
- gpt-5 (catch-all): 400,000
Sources: https://developers.openai.com/api/docs/models
2026-04-13 19:22:23 -07:00
|
|
|
|
"gpt-5.1-chat": 128000, # Chat variant has 128k context
|
|
|
|
|
|
"gpt-5": 400000, # GPT-5.x base, mini, codex variants (400k)
|
feat: overhaul context length detection with models.dev and provider-aware resolution (#2158)
Replace the fragile hardcoded context length system with a multi-source
resolution chain that correctly identifies context windows per provider.
Key changes:
- New agent/models_dev.py: Fetches and caches the models.dev registry
(3800+ models across 100+ providers with per-provider context windows).
In-memory cache (1hr TTL) + disk cache for cold starts.
- Rewritten get_model_context_length() resolution chain:
0. Config override (model.context_length)
1. Custom providers per-model context_length
2. Persistent disk cache
3. Endpoint /models (local servers)
4. Anthropic /v1/models API (max_input_tokens, API-key only)
5. OpenRouter live API (existing, unchanged)
6. Nous suffix-match via OpenRouter (dot/dash normalization)
7. models.dev registry lookup (provider-aware)
8. Thin hardcoded defaults (broad family patterns)
9. 128K fallback (was 2M)
- Provider-aware context: same model now correctly resolves to different
context windows per provider (e.g. claude-opus-4.6: 1M on Anthropic,
128K on GitHub Copilot). Provider name flows through ContextCompressor.
- DEFAULT_CONTEXT_LENGTHS shrunk from 80+ entries to ~16 broad patterns.
models.dev replaces the per-model hardcoding.
- CONTEXT_PROBE_TIERS changed from [2M, 1M, 512K, 200K, 128K, 64K, 32K]
to [128K, 64K, 32K, 16K, 8K]. Unknown models no longer start at 2M.
- hermes model: prompts for context_length when configuring custom
endpoints. Supports shorthand (32k, 128K). Saved to custom_providers
per-model config.
- custom_providers schema extended with optional models dict for
per-model context_length (backward compatible).
- Nous Portal: suffix-matches bare IDs (claude-opus-4-6) against
OpenRouter's prefixed IDs (anthropic/claude-opus-4.6) with dot/dash
normalization. Handles all 15 current Nous models.
- Anthropic direct: queries /v1/models for max_input_tokens. Only works
with regular API keys (sk-ant-api*), not OAuth tokens. Falls through
to models.dev for OAuth users.
Tests: 5574 passed (18 new tests for models_dev + updated probe tiers)
Docs: Updated configuration.md context length section, AGENTS.md
Co-authored-by: Test <test@test.com>
2026-03-20 06:04:33 -07:00
|
|
|
|
"gpt-4.1": 1047576,
|
|
|
|
|
|
"gpt-4": 128000,
|
|
|
|
|
|
# Google
|
|
|
|
|
|
"gemini": 1048576,
|
2026-04-06 10:14:01 -07:00
|
|
|
|
# Gemma (open models served via AI Studio)
|
2026-04-22 22:56:54 +01:00
|
|
|
|
"gemma-4": 256000, # Gemma 4 family
|
|
|
|
|
|
"gemma4": 256000, # Ollama-style naming (e.g. gemma4:31b-cloud)
|
2026-04-06 10:19:19 -07:00
|
|
|
|
"gemma-4-31b": 256000,
|
2026-04-06 10:14:01 -07:00
|
|
|
|
"gemma-3": 131072,
|
|
|
|
|
|
"gemma": 8192, # fallback for older gemma models
|
2026-04-24 14:48:55 +08:00
|
|
|
|
# DeepSeek — V4 family ships with a 1M context window. The legacy
|
|
|
|
|
|
# aliases ``deepseek-chat`` / ``deepseek-reasoner`` are server-side
|
|
|
|
|
|
# mapped to the non-thinking / thinking modes of ``deepseek-v4-flash``
|
|
|
|
|
|
# and inherit the same 1M window. The ``deepseek`` substring entry
|
|
|
|
|
|
# below remains as a 128K fallback for older / unknown DeepSeek model
|
|
|
|
|
|
# ids (e.g. via custom endpoints).
|
|
|
|
|
|
# https://api-docs.deepseek.com/zh-cn/quick_start/pricing
|
|
|
|
|
|
"deepseek-v4-pro": 1_000_000,
|
|
|
|
|
|
"deepseek-v4-flash": 1_000_000,
|
|
|
|
|
|
"deepseek-chat": 1_000_000,
|
|
|
|
|
|
"deepseek-reasoner": 1_000_000,
|
feat: overhaul context length detection with models.dev and provider-aware resolution (#2158)
Replace the fragile hardcoded context length system with a multi-source
resolution chain that correctly identifies context windows per provider.
Key changes:
- New agent/models_dev.py: Fetches and caches the models.dev registry
(3800+ models across 100+ providers with per-provider context windows).
In-memory cache (1hr TTL) + disk cache for cold starts.
- Rewritten get_model_context_length() resolution chain:
0. Config override (model.context_length)
1. Custom providers per-model context_length
2. Persistent disk cache
3. Endpoint /models (local servers)
4. Anthropic /v1/models API (max_input_tokens, API-key only)
5. OpenRouter live API (existing, unchanged)
6. Nous suffix-match via OpenRouter (dot/dash normalization)
7. models.dev registry lookup (provider-aware)
8. Thin hardcoded defaults (broad family patterns)
9. 128K fallback (was 2M)
- Provider-aware context: same model now correctly resolves to different
context windows per provider (e.g. claude-opus-4.6: 1M on Anthropic,
128K on GitHub Copilot). Provider name flows through ContextCompressor.
- DEFAULT_CONTEXT_LENGTHS shrunk from 80+ entries to ~16 broad patterns.
models.dev replaces the per-model hardcoding.
- CONTEXT_PROBE_TIERS changed from [2M, 1M, 512K, 200K, 128K, 64K, 32K]
to [128K, 64K, 32K, 16K, 8K]. Unknown models no longer start at 2M.
- hermes model: prompts for context_length when configuring custom
endpoints. Supports shorthand (32k, 128K). Saved to custom_providers
per-model config.
- custom_providers schema extended with optional models dict for
per-model context_length (backward compatible).
- Nous Portal: suffix-matches bare IDs (claude-opus-4-6) against
OpenRouter's prefixed IDs (anthropic/claude-opus-4.6) with dot/dash
normalization. Handles all 15 current Nous models.
- Anthropic direct: queries /v1/models for max_input_tokens. Only works
with regular API keys (sk-ant-api*), not OAuth tokens. Falls through
to models.dev for OAuth users.
Tests: 5574 passed (18 new tests for models_dev + updated probe tiers)
Docs: Updated configuration.md context length section, AGENTS.md
Co-authored-by: Test <test@test.com>
2026-03-20 06:04:33 -07:00
|
|
|
|
"deepseek": 128000,
|
|
|
|
|
|
# Meta
|
|
|
|
|
|
"llama": 131072,
|
2026-04-11 03:29:09 -07:00
|
|
|
|
# Qwen — specific model families before the catch-all.
|
|
|
|
|
|
# Official docs: https://help.aliyun.com/zh/model-studio/developer-reference/
|
2026-05-17 02:29:27 -07:00
|
|
|
|
"qwen3.6-plus": 1048576, # 1M context (DashScope/Alibaba & OpenRouter)
|
2026-04-11 03:29:09 -07:00
|
|
|
|
"qwen3-coder-plus": 1000000, # 1M context
|
|
|
|
|
|
"qwen3-coder": 262144, # 256K context
|
feat: overhaul context length detection with models.dev and provider-aware resolution (#2158)
Replace the fragile hardcoded context length system with a multi-source
resolution chain that correctly identifies context windows per provider.
Key changes:
- New agent/models_dev.py: Fetches and caches the models.dev registry
(3800+ models across 100+ providers with per-provider context windows).
In-memory cache (1hr TTL) + disk cache for cold starts.
- Rewritten get_model_context_length() resolution chain:
0. Config override (model.context_length)
1. Custom providers per-model context_length
2. Persistent disk cache
3. Endpoint /models (local servers)
4. Anthropic /v1/models API (max_input_tokens, API-key only)
5. OpenRouter live API (existing, unchanged)
6. Nous suffix-match via OpenRouter (dot/dash normalization)
7. models.dev registry lookup (provider-aware)
8. Thin hardcoded defaults (broad family patterns)
9. 128K fallback (was 2M)
- Provider-aware context: same model now correctly resolves to different
context windows per provider (e.g. claude-opus-4.6: 1M on Anthropic,
128K on GitHub Copilot). Provider name flows through ContextCompressor.
- DEFAULT_CONTEXT_LENGTHS shrunk from 80+ entries to ~16 broad patterns.
models.dev replaces the per-model hardcoding.
- CONTEXT_PROBE_TIERS changed from [2M, 1M, 512K, 200K, 128K, 64K, 32K]
to [128K, 64K, 32K, 16K, 8K]. Unknown models no longer start at 2M.
- hermes model: prompts for context_length when configuring custom
endpoints. Supports shorthand (32k, 128K). Saved to custom_providers
per-model config.
- custom_providers schema extended with optional models dict for
per-model context_length (backward compatible).
- Nous Portal: suffix-matches bare IDs (claude-opus-4-6) against
OpenRouter's prefixed IDs (anthropic/claude-opus-4.6) with dot/dash
normalization. Handles all 15 current Nous models.
- Anthropic direct: queries /v1/models for max_input_tokens. Only works
with regular API keys (sk-ant-api*), not OAuth tokens. Falls through
to models.dev for OAuth users.
Tests: 5574 passed (18 new tests for models_dev + updated probe tiers)
Docs: Updated configuration.md context length section, AGENTS.md
Co-authored-by: Test <test@test.com>
2026-03-20 06:04:33 -07:00
|
|
|
|
"qwen": 131072,
|
2026-05-31 20:18:05 -07:00
|
|
|
|
# MiniMax — M3 is 1M context (max output 512K); M2.x series is 204,800.
|
|
|
|
|
|
# Keys use substring matching (longest-first), so "minimax-m3" wins over
|
|
|
|
|
|
# the generic "minimax" catch-all for the M3 slug on every surface
|
|
|
|
|
|
# (native MiniMax-M3, OpenRouter/Nous minimax/minimax-m3).
|
|
|
|
|
|
# https://platform.minimax.io/docs/api-reference/text-chat-openai
|
|
|
|
|
|
"minimax-m3": 1000000,
|
fix: align MiniMax provider with official API docs
Aligns MiniMax provider with official API documentation. Fixes 6 bugs:
transport mismatch (openai_chat -> anthropic_messages), credential leak
in switch_model(), prompt caching sent to non-Anthropic endpoints,
dot-to-hyphen model name corruption, trajectory compressor URL routing,
and stale doctor health check.
Also corrects context window (204,800), thinking support (manual mode),
max output (131,072), and model catalog (M2 family only on /anthropic).
Source: https://platform.minimax.io/docs/api-reference/text-anthropic-api
Co-authored-by: kshitijk4poor <kshitijk4poor@users.noreply.github.com>
2026-04-10 03:53:18 -07:00
|
|
|
|
"minimax": 204800,
|
2026-06-13 10:08:29 -05:00
|
|
|
|
# GLM — GLM-5.2 ships with a 1M context window (verified empirically:
|
|
|
|
|
|
# needle-in-a-haystack retrieval at 789K prompt tokens succeeded with
|
|
|
|
|
|
# zero errors on api.z.ai/api/coding/paas/v4). Older GLM models
|
|
|
|
|
|
# (5, 5.1, 5-turbo) are ~202K. Longest-key-first substring matching
|
|
|
|
|
|
# ensures "glm-5.2" resolves to 1M while older variants still hit the
|
|
|
|
|
|
# generic 202K fallback.
|
|
|
|
|
|
"glm-5.2": 1_048_576,
|
feat: overhaul context length detection with models.dev and provider-aware resolution (#2158)
Replace the fragile hardcoded context length system with a multi-source
resolution chain that correctly identifies context windows per provider.
Key changes:
- New agent/models_dev.py: Fetches and caches the models.dev registry
(3800+ models across 100+ providers with per-provider context windows).
In-memory cache (1hr TTL) + disk cache for cold starts.
- Rewritten get_model_context_length() resolution chain:
0. Config override (model.context_length)
1. Custom providers per-model context_length
2. Persistent disk cache
3. Endpoint /models (local servers)
4. Anthropic /v1/models API (max_input_tokens, API-key only)
5. OpenRouter live API (existing, unchanged)
6. Nous suffix-match via OpenRouter (dot/dash normalization)
7. models.dev registry lookup (provider-aware)
8. Thin hardcoded defaults (broad family patterns)
9. 128K fallback (was 2M)
- Provider-aware context: same model now correctly resolves to different
context windows per provider (e.g. claude-opus-4.6: 1M on Anthropic,
128K on GitHub Copilot). Provider name flows through ContextCompressor.
- DEFAULT_CONTEXT_LENGTHS shrunk from 80+ entries to ~16 broad patterns.
models.dev replaces the per-model hardcoding.
- CONTEXT_PROBE_TIERS changed from [2M, 1M, 512K, 200K, 128K, 64K, 32K]
to [128K, 64K, 32K, 16K, 8K]. Unknown models no longer start at 2M.
- hermes model: prompts for context_length when configuring custom
endpoints. Supports shorthand (32k, 128K). Saved to custom_providers
per-model config.
- custom_providers schema extended with optional models dict for
per-model context_length (backward compatible).
- Nous Portal: suffix-matches bare IDs (claude-opus-4-6) against
OpenRouter's prefixed IDs (anthropic/claude-opus-4.6) with dot/dash
normalization. Handles all 15 current Nous models.
- Anthropic direct: queries /v1/models for max_input_tokens. Only works
with regular API keys (sk-ant-api*), not OAuth tokens. Falls through
to models.dev for OAuth users.
Tests: 5574 passed (18 new tests for models_dev + updated probe tiers)
Docs: Updated configuration.md context length section, AGENTS.md
Co-authored-by: Test <test@test.com>
2026-03-20 06:04:33 -07:00
|
|
|
|
"glm": 202752,
|
fix(model_metadata): add xAI Grok context length fallbacks
xAI /v1/models does not return context_length metadata, so Hermes
probes down to the 128k default whenever a user configures a custom
provider pointing at https://api.x.ai/v1. This forces every xAI user
to manually override model.context_length in config.yaml (2M for
Grok 4.20 / 4.1-fast / 4-fast) or lose most of the usable context
window.
Add DEFAULT_CONTEXT_LENGTHS entries for the Grok family so the
fallback lookup returns the correct value via substring matching.
Values sourced from models.dev (2026-04) and cross-checked against
the xAI /v1/models listing:
- grok-4.20-* 2,000,000 (reasoning, non-reasoning, multi-agent)
- grok-4-1-fast-* 2,000,000
- grok-4-fast-* 2,000,000
- grok-4 / grok-4-0709 256,000
- grok-code-fast-1 256,000
- grok-3* 131,072
- grok-2 / latest 131,072
- grok-2-vision* 8,192
- grok (catch-all) 131,072
Keys are ordered longest-first so that specific variants match before
the catch-all, consistent with the existing Claude/Gemma/MiniMax entries.
Add TestDefaultContextLengths.test_grok_models_context_lengths and
test_grok_substring_matching to pin the values and verify the full
lookup path. All 77 tests in test_model_metadata.py pass.
2026-04-10 12:08:16 +04:00
|
|
|
|
# xAI Grok — xAI /v1/models does not return context_length metadata,
|
|
|
|
|
|
# so these hardcoded fallbacks prevent Hermes from probing-down to
|
|
|
|
|
|
# the default 128k when the user points at https://api.x.ai/v1
|
|
|
|
|
|
# via a custom provider. Values sourced from models.dev (2026-04).
|
|
|
|
|
|
# Keys use substring matching (longest-first), so e.g. "grok-4.20"
|
|
|
|
|
|
# matches "grok-4.20-0309-reasoning" / "-non-reasoning" / "-multi-agent-0309".
|
2026-06-17 16:44:12 -07:00
|
|
|
|
# OAuth-only slug; absent from GET /v1/models. xAI publishes a 200k
|
|
|
|
|
|
# usable context window for Composer 2.5 on Grok Build (SuperGrok /
|
|
|
|
|
|
# Premium+); /v1/responses additionally enforces a ~262144 input+output
|
|
|
|
|
|
# budget, but the usable context (what we track here) is 200k.
|
2026-06-17 19:54:24 +05:30
|
|
|
|
"grok-composer": 200000, # grok-composer-2.5-fast (Grok Build CLI)
|
2026-05-22 21:57:43 +04:00
|
|
|
|
"grok-build": 256000, # grok-build-0.1
|
fix(model_metadata): add xAI Grok context length fallbacks
xAI /v1/models does not return context_length metadata, so Hermes
probes down to the 128k default whenever a user configures a custom
provider pointing at https://api.x.ai/v1. This forces every xAI user
to manually override model.context_length in config.yaml (2M for
Grok 4.20 / 4.1-fast / 4-fast) or lose most of the usable context
window.
Add DEFAULT_CONTEXT_LENGTHS entries for the Grok family so the
fallback lookup returns the correct value via substring matching.
Values sourced from models.dev (2026-04) and cross-checked against
the xAI /v1/models listing:
- grok-4.20-* 2,000,000 (reasoning, non-reasoning, multi-agent)
- grok-4-1-fast-* 2,000,000
- grok-4-fast-* 2,000,000
- grok-4 / grok-4-0709 256,000
- grok-code-fast-1 256,000
- grok-3* 131,072
- grok-2 / latest 131,072
- grok-2-vision* 8,192
- grok (catch-all) 131,072
Keys are ordered longest-first so that specific variants match before
the catch-all, consistent with the existing Claude/Gemma/MiniMax entries.
Add TestDefaultContextLengths.test_grok_models_context_lengths and
test_grok_substring_matching to pin the values and verify the full
lookup path. All 77 tests in test_model_metadata.py pass.
2026-04-10 12:08:16 +04:00
|
|
|
|
"grok-code-fast": 256000, # grok-code-fast-1
|
|
|
|
|
|
"grok-2-vision": 8192, # grok-2-vision, -1212, -latest
|
2026-05-20 17:15:51 -05:00
|
|
|
|
"grok-4-fast": 2000000, # grok-4-fast-(non-)reasoning, also matches -reasoning
|
fix(model_metadata): add xAI Grok context length fallbacks
xAI /v1/models does not return context_length metadata, so Hermes
probes down to the 128k default whenever a user configures a custom
provider pointing at https://api.x.ai/v1. This forces every xAI user
to manually override model.context_length in config.yaml (2M for
Grok 4.20 / 4.1-fast / 4-fast) or lose most of the usable context
window.
Add DEFAULT_CONTEXT_LENGTHS entries for the Grok family so the
fallback lookup returns the correct value via substring matching.
Values sourced from models.dev (2026-04) and cross-checked against
the xAI /v1/models listing:
- grok-4.20-* 2,000,000 (reasoning, non-reasoning, multi-agent)
- grok-4-1-fast-* 2,000,000
- grok-4-fast-* 2,000,000
- grok-4 / grok-4-0709 256,000
- grok-code-fast-1 256,000
- grok-3* 131,072
- grok-2 / latest 131,072
- grok-2-vision* 8,192
- grok (catch-all) 131,072
Keys are ordered longest-first so that specific variants match before
the catch-all, consistent with the existing Claude/Gemma/MiniMax entries.
Add TestDefaultContextLengths.test_grok_models_context_lengths and
test_grok_substring_matching to pin the values and verify the full
lookup path. All 77 tests in test_model_metadata.py pass.
2026-04-10 12:08:16 +04:00
|
|
|
|
"grok-4.20": 2000000, # grok-4.20-0309-(non-)reasoning, -multi-agent-0309
|
2026-05-15 17:11:06 -07:00
|
|
|
|
"grok-4.3": 1000000, # grok-4.3, grok-4.3-latest — 1M context per docs.x.ai
|
fix(model_metadata): add xAI Grok context length fallbacks
xAI /v1/models does not return context_length metadata, so Hermes
probes down to the 128k default whenever a user configures a custom
provider pointing at https://api.x.ai/v1. This forces every xAI user
to manually override model.context_length in config.yaml (2M for
Grok 4.20 / 4.1-fast / 4-fast) or lose most of the usable context
window.
Add DEFAULT_CONTEXT_LENGTHS entries for the Grok family so the
fallback lookup returns the correct value via substring matching.
Values sourced from models.dev (2026-04) and cross-checked against
the xAI /v1/models listing:
- grok-4.20-* 2,000,000 (reasoning, non-reasoning, multi-agent)
- grok-4-1-fast-* 2,000,000
- grok-4-fast-* 2,000,000
- grok-4 / grok-4-0709 256,000
- grok-code-fast-1 256,000
- grok-3* 131,072
- grok-2 / latest 131,072
- grok-2-vision* 8,192
- grok (catch-all) 131,072
Keys are ordered longest-first so that specific variants match before
the catch-all, consistent with the existing Claude/Gemma/MiniMax entries.
Add TestDefaultContextLengths.test_grok_models_context_lengths and
test_grok_substring_matching to pin the values and verify the full
lookup path. All 77 tests in test_model_metadata.py pass.
2026-04-10 12:08:16 +04:00
|
|
|
|
"grok-4": 256000, # grok-4, grok-4-0709
|
|
|
|
|
|
"grok-3": 131072, # grok-3, grok-3-mini, grok-3-fast, grok-3-mini-fast
|
|
|
|
|
|
"grok-2": 131072, # grok-2, grok-2-1212, grok-2-latest
|
|
|
|
|
|
"grok": 131072, # catch-all (grok-beta, unknown grok-*)
|
feat: overhaul context length detection with models.dev and provider-aware resolution (#2158)
Replace the fragile hardcoded context length system with a multi-source
resolution chain that correctly identifies context windows per provider.
Key changes:
- New agent/models_dev.py: Fetches and caches the models.dev registry
(3800+ models across 100+ providers with per-provider context windows).
In-memory cache (1hr TTL) + disk cache for cold starts.
- Rewritten get_model_context_length() resolution chain:
0. Config override (model.context_length)
1. Custom providers per-model context_length
2. Persistent disk cache
3. Endpoint /models (local servers)
4. Anthropic /v1/models API (max_input_tokens, API-key only)
5. OpenRouter live API (existing, unchanged)
6. Nous suffix-match via OpenRouter (dot/dash normalization)
7. models.dev registry lookup (provider-aware)
8. Thin hardcoded defaults (broad family patterns)
9. 128K fallback (was 2M)
- Provider-aware context: same model now correctly resolves to different
context windows per provider (e.g. claude-opus-4.6: 1M on Anthropic,
128K on GitHub Copilot). Provider name flows through ContextCompressor.
- DEFAULT_CONTEXT_LENGTHS shrunk from 80+ entries to ~16 broad patterns.
models.dev replaces the per-model hardcoding.
- CONTEXT_PROBE_TIERS changed from [2M, 1M, 512K, 200K, 128K, 64K, 32K]
to [128K, 64K, 32K, 16K, 8K]. Unknown models no longer start at 2M.
- hermes model: prompts for context_length when configuring custom
endpoints. Supports shorthand (32k, 128K). Saved to custom_providers
per-model config.
- custom_providers schema extended with optional models dict for
per-model context_length (backward compatible).
- Nous Portal: suffix-matches bare IDs (claude-opus-4-6) against
OpenRouter's prefixed IDs (anthropic/claude-opus-4.6) with dot/dash
normalization. Handles all 15 current Nous models.
- Anthropic direct: queries /v1/models for max_input_tokens. Only works
with regular API keys (sk-ant-api*), not OAuth tokens. Falls through
to models.dev for OAuth users.
Tests: 5574 passed (18 new tests for models_dev + updated probe tiers)
Docs: Updated configuration.md context length section, AGENTS.md
Co-authored-by: Test <test@test.com>
2026-03-20 06:04:33 -07:00
|
|
|
|
# Kimi
|
|
|
|
|
|
"kimi": 262144,
|
2026-05-09 13:37:19 -07:00
|
|
|
|
# Tencent — Hy3 Preview (Hunyuan) with 256K context window.
|
|
|
|
|
|
# OpenRouter live metadata reports 262144 (256 × 1024); align the
|
|
|
|
|
|
# static fallback so cache and offline both agree (issue #22268).
|
|
|
|
|
|
"hy3-preview": 262144,
|
2026-04-17 13:09:14 -07:00
|
|
|
|
# Nemotron — NVIDIA's open-weights series (128K context across all sizes)
|
|
|
|
|
|
"nemotron": 131072,
|
2026-04-03 13:45:16 -07:00
|
|
|
|
# Arcee
|
|
|
|
|
|
"trinity": 262144,
|
2026-04-13 21:16:14 -07:00
|
|
|
|
# OpenRouter
|
|
|
|
|
|
"elephant": 262144,
|
feat: add Hugging Face as a first-class inference provider (#3419)
Salvage of PR #1747 (original PR #1171 by @davanstrien) onto current main.
Registers Hugging Face Inference Providers (router.huggingface.co/v1) as a named provider:
- hermes chat --provider huggingface (or --provider hf)
- 18 curated open models via hermes model picker
- HF_TOKEN in ~/.hermes/.env
- OpenAI-compatible endpoint with automatic failover (Groq, Together, SambaNova, etc.)
Files: auth.py, models.py, main.py, setup.py, config.py, model_metadata.py, .env.example, 5 docs pages, 17 new tests.
Co-authored-by: Daniel van Strien <davanstrien@gmail.com>
2026-03-27 12:41:59 -07:00
|
|
|
|
# Hugging Face Inference Providers — model IDs use org/name format
|
|
|
|
|
|
"Qwen/Qwen3.5-397B-A17B": 131072,
|
2026-03-27 13:54:46 -07:00
|
|
|
|
"Qwen/Qwen3.5-35B-A3B": 131072,
|
feat: add Hugging Face as a first-class inference provider (#3419)
Salvage of PR #1747 (original PR #1171 by @davanstrien) onto current main.
Registers Hugging Face Inference Providers (router.huggingface.co/v1) as a named provider:
- hermes chat --provider huggingface (or --provider hf)
- 18 curated open models via hermes model picker
- HF_TOKEN in ~/.hermes/.env
- OpenAI-compatible endpoint with automatic failover (Groq, Together, SambaNova, etc.)
Files: auth.py, models.py, main.py, setup.py, config.py, model_metadata.py, .env.example, 5 docs pages, 17 new tests.
Co-authored-by: Daniel van Strien <davanstrien@gmail.com>
2026-03-27 12:41:59 -07:00
|
|
|
|
"deepseek-ai/DeepSeek-V3.2": 65536,
|
|
|
|
|
|
"moonshotai/Kimi-K2.5": 262144,
|
2026-04-20 23:20:33 -07:00
|
|
|
|
"moonshotai/Kimi-K2.6": 262144,
|
feat: add Hugging Face as a first-class inference provider (#3419)
Salvage of PR #1747 (original PR #1171 by @davanstrien) onto current main.
Registers Hugging Face Inference Providers (router.huggingface.co/v1) as a named provider:
- hermes chat --provider huggingface (or --provider hf)
- 18 curated open models via hermes model picker
- HF_TOKEN in ~/.hermes/.env
- OpenAI-compatible endpoint with automatic failover (Groq, Together, SambaNova, etc.)
Files: auth.py, models.py, main.py, setup.py, config.py, model_metadata.py, .env.example, 5 docs pages, 17 new tests.
Co-authored-by: Daniel van Strien <davanstrien@gmail.com>
2026-03-27 12:41:59 -07:00
|
|
|
|
"moonshotai/Kimi-K2-Thinking": 262144,
|
fix: align MiniMax provider with official API docs
Aligns MiniMax provider with official API documentation. Fixes 6 bugs:
transport mismatch (openai_chat -> anthropic_messages), credential leak
in switch_model(), prompt caching sent to non-Anthropic endpoints,
dot-to-hyphen model name corruption, trajectory compressor URL routing,
and stale doctor health check.
Also corrects context window (204,800), thinking support (manual mode),
max output (131,072), and model catalog (M2 family only on /anthropic).
Source: https://platform.minimax.io/docs/api-reference/text-anthropic-api
Co-authored-by: kshitijk4poor <kshitijk4poor@users.noreply.github.com>
2026-04-10 03:53:18 -07:00
|
|
|
|
"MiniMaxAI/MiniMax-M2.5": 204800,
|
feat: add Xiaomi MiMo v2.5-pro and v2.5 model support (#14635)
## Merged
Adds MiMo v2.5-pro and v2.5 support to Xiaomi native provider, OpenCode Go, and setup wizard.
### Changes
- Context lengths: added v2.5-pro (1M) and v2.5 (1M), corrected existing MiMo entries to exact values (262144)
- Provider lists: xiaomi, opencode-go, setup wizard
- Vision: upgraded from mimo-v2-omni to mimo-v2.5 (omnimodal)
- Config description updated for XIAOMI_API_KEY
- Tests updated for new vision model preference
### Verification
- 4322 tests passed, 0 new regressions
- Live API tested on Xiaomi portal: basic, reasoning, tool calling, multi-tool, file ops, system prompt, vision — all pass
- Self-review found and fixed 2 issues (redundant vision check, stale HuggingFace context length)
2026-04-23 10:06:25 -07:00
|
|
|
|
"XiaomiMiMo/MiMo-V2-Flash": 262144,
|
|
|
|
|
|
"mimo-v2-pro": 1048576,
|
|
|
|
|
|
"mimo-v2.5-pro": 1048576,
|
|
|
|
|
|
"mimo-v2.5": 1048576,
|
|
|
|
|
|
"mimo-v2-omni": 262144,
|
|
|
|
|
|
"mimo-v2-flash": 262144,
|
feat: add Hugging Face as a first-class inference provider (#3419)
Salvage of PR #1747 (original PR #1171 by @davanstrien) onto current main.
Registers Hugging Face Inference Providers (router.huggingface.co/v1) as a named provider:
- hermes chat --provider huggingface (or --provider hf)
- 18 curated open models via hermes model picker
- HF_TOKEN in ~/.hermes/.env
- OpenAI-compatible endpoint with automatic failover (Groq, Together, SambaNova, etc.)
Files: auth.py, models.py, main.py, setup.py, config.py, model_metadata.py, .env.example, 5 docs pages, 17 new tests.
Co-authored-by: Daniel van Strien <davanstrien@gmail.com>
2026-03-27 12:41:59 -07:00
|
|
|
|
"zai-org/GLM-5": 202752,
|
2026-02-21 22:31:43 -08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-10 15:21:30 -07:00
|
|
|
|
# xAI Grok models that ACCEPT the `reasoning.effort` parameter on
|
|
|
|
|
|
# api.x.ai. Verified live against /v1/responses 2026-05-10:
|
|
|
|
|
|
#
|
|
|
|
|
|
# ACCEPTS effort: grok-3-mini, grok-3-mini-fast, grok-4.20-multi-agent-0309,
|
|
|
|
|
|
# grok-4.3
|
|
|
|
|
|
# REJECTS effort: grok-3, grok-4, grok-4-0709, grok-4-fast-(non-)reasoning,
|
|
|
|
|
|
# grok-4-1-fast-(non-)reasoning, grok-4.20-0309-(non-)reasoning,
|
|
|
|
|
|
# grok-code-fast-1
|
|
|
|
|
|
#
|
|
|
|
|
|
# REJECTS-side models still reason natively — they just don't expose an
|
|
|
|
|
|
# effort dial — so callers should send no `reasoning` key at all rather
|
|
|
|
|
|
# than a default `medium` (which 400s with "Model X does not support
|
|
|
|
|
|
# parameter reasoningEffort").
|
|
|
|
|
|
_GROK_EFFORT_CAPABLE_PREFIXES = (
|
|
|
|
|
|
"grok-3-mini",
|
|
|
|
|
|
"grok-4.20-multi-agent",
|
|
|
|
|
|
"grok-4.3",
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def grok_supports_reasoning_effort(model: str) -> bool:
|
|
|
|
|
|
"""Return True when an xAI Grok model accepts ``reasoning.effort``.
|
|
|
|
|
|
|
|
|
|
|
|
Allowlist by substring (matches both bare ``grok-3-mini`` and
|
|
|
|
|
|
aggregator-prefixed ``x-ai/grok-3-mini``). Conservative by design:
|
|
|
|
|
|
if a future Grok model isn't listed, we send no effort dial rather
|
|
|
|
|
|
than 400.
|
|
|
|
|
|
"""
|
|
|
|
|
|
name = (model or "").strip().lower()
|
|
|
|
|
|
if not name:
|
|
|
|
|
|
return False
|
|
|
|
|
|
# Strip common aggregator prefixes (x-ai/, openrouter/x-ai/, xai/, ...)
|
|
|
|
|
|
for sep in ("/",):
|
|
|
|
|
|
if sep in name:
|
|
|
|
|
|
name = name.rsplit(sep, 1)[-1]
|
|
|
|
|
|
return any(name.startswith(prefix) for prefix in _GROK_EFFORT_CAPABLE_PREFIXES)
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-03-18 03:04:07 -07:00
|
|
|
|
_CONTEXT_LENGTH_KEYS = (
|
|
|
|
|
|
"context_length",
|
|
|
|
|
|
"context_window",
|
feat: add NovitaAI as LLM provider
Add NovitaAI as a first-class provider with dedicated model selection
flow, live pricing, and authoritative context length resolution.
- Register provider in PROVIDER_REGISTRY, HERMES_OVERLAYS, and all
alias/label maps (ID: novita, aliases: novita-ai, novitaai)
- Add dedicated _model_flow_novita() with 3-tier model list fallback:
Novita API → models.dev → static curated list
- Fetch live pricing from /v1/models with correct unit conversion
(input_token_price_per_m is 0.0001 USD per Mtok)
- Add Novita-specific context length resolution (step 4b) in
get_model_context_length(), prioritized over models.dev/OpenRouter
- Register api.novita.ai in _URL_TO_PROVIDER to prevent early return
from the custom-endpoint code path
- Add models.dev mapping (novita → novita-ai)
- Add default auxiliary model (deepseek/deepseek-v3-0324)
- Add NOVITA_API_KEY to test isolation (conftest.py)
- Update docs: providers page, env vars reference, CLI reference,
.env.example, README, and landing page
2026-04-10 22:22:47 +08:00
|
|
|
|
"context_size",
|
2026-03-18 03:04:07 -07:00
|
|
|
|
"max_context_length",
|
|
|
|
|
|
"max_position_embeddings",
|
|
|
|
|
|
"max_model_len",
|
|
|
|
|
|
"max_input_tokens",
|
|
|
|
|
|
"max_sequence_length",
|
|
|
|
|
|
"max_seq_len",
|
2026-03-19 06:01:16 -07:00
|
|
|
|
"n_ctx_train",
|
|
|
|
|
|
"n_ctx",
|
2026-04-12 14:13:02 -04:00
|
|
|
|
"ctx_size",
|
2026-03-18 03:04:07 -07:00
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
_MAX_COMPLETION_KEYS = (
|
|
|
|
|
|
"max_completion_tokens",
|
|
|
|
|
|
"max_output_tokens",
|
|
|
|
|
|
"max_tokens",
|
|
|
|
|
|
)
|
|
|
|
|
|
|
2026-03-18 21:38:41 +01:00
|
|
|
|
# Local server hostnames / address patterns
|
|
|
|
|
|
_LOCAL_HOSTS = ("localhost", "127.0.0.1", "::1", "0.0.0.0")
|
2026-04-11 14:46:18 -07:00
|
|
|
|
# Docker / Podman / Lima DNS names that resolve to the host machine
|
|
|
|
|
|
_CONTAINER_LOCAL_SUFFIXES = (
|
|
|
|
|
|
".docker.internal",
|
|
|
|
|
|
".containers.internal",
|
|
|
|
|
|
".lima.internal",
|
|
|
|
|
|
)
|
2026-03-18 21:38:41 +01:00
|
|
|
|
|
2026-03-18 03:04:07 -07:00
|
|
|
|
|
|
|
|
|
|
def _normalize_base_url(base_url: str) -> str:
|
|
|
|
|
|
return (base_url or "").strip().rstrip("/")
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-04-20 20:49:44 -07:00
|
|
|
|
def _auth_headers(api_key: str = "") -> Dict[str, str]:
|
|
|
|
|
|
token = str(api_key or "").strip()
|
|
|
|
|
|
if not token:
|
|
|
|
|
|
return {}
|
|
|
|
|
|
return {"Authorization": f"Bearer {token}"}
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-03-18 03:04:07 -07:00
|
|
|
|
def _is_openrouter_base_url(base_url: str) -> bool:
|
fix: sweep remaining provider-URL substring checks across codebase
Completes the hostname-hardening sweep — every substring check against a
provider host in live-routing code is now hostname-based. This closes the
same false-positive class for OpenRouter, GitHub Copilot, Kimi, Qwen,
ChatGPT/Codex, Bedrock, GitHub Models, Vercel AI Gateway, Nous, Z.AI,
Moonshot, Arcee, and MiniMax that the original PR closed for OpenAI, xAI,
and Anthropic.
New helper:
- utils.base_url_host_matches(base_url, domain) — safe counterpart to
'domain in base_url'. Accepts hostname equality and subdomain matches;
rejects path segments, host suffixes, and prefix collisions.
Call sites converted (real-code only; tests, optional-skills, red-teaming
scripts untouched):
run_agent.py (10 sites):
- AIAgent.__init__ Bedrock branch, ChatGPT/Codex branch (also path check)
- header cascade for openrouter / copilot / kimi / qwen / chatgpt
- interleaved-thinking trigger (openrouter + claude)
- _is_openrouter_url(), _is_qwen_portal()
- is_native_anthropic check
- github-models-vs-copilot detection (3 sites)
- reasoning-capable route gate (nousresearch, vercel, github)
- codex-backend detection in API kwargs build
- fallback api_mode Bedrock detection
agent/auxiliary_client.py (7 sites):
- extra-headers cascades in 4 distinct client-construction paths
(resolve custom, resolve auto, OpenRouter-fallback-to-custom,
_async_client_from_sync, resolve_provider_client explicit-custom,
resolve_auto_with_codex)
- _is_openrouter_client() base_url sniff
agent/usage_pricing.py:
- resolve_billing_route openrouter branch
agent/model_metadata.py:
- _is_openrouter_base_url(), Bedrock context-length lookup
hermes_cli/providers.py:
- determine_api_mode Bedrock heuristic
hermes_cli/runtime_provider.py:
- _is_openrouter_url flag for API-key preference (issues #420, #560)
hermes_cli/doctor.py:
- Kimi User-Agent header for /models probes
tools/delegate_tool.py:
- subagent Codex endpoint detection
trajectory_compressor.py:
- _detect_provider() cascade (8 providers: openrouter, nous, codex, zai,
kimi-coding, arcee, minimax-cn, minimax)
cli.py, gateway/run.py:
- /model-switch cache-enabled hint (openrouter + claude)
Bedrock detection tightened from 'bedrock-runtime in url' to
'hostname starts with bedrock-runtime. AND host is under amazonaws.com'.
ChatGPT/Codex detection tightened from 'chatgpt.com/backend-api/codex in
url' to 'hostname is chatgpt.com AND path contains /backend-api/codex'.
Tests:
- tests/test_base_url_hostname.py extended with a base_url_host_matches
suite (exact match, subdomain, path-segment rejection, host-suffix
rejection, host-prefix rejection, empty-input, case-insensitivity,
trailing dot).
Validation: 651 targeted tests pass (runtime_provider, minimax, bedrock,
gemini, auxiliary, codex_cloudflare, usage_pricing, compressor_fallback,
fallback_model, openai_client_lifecycle, provider_parity, cli_provider_resolution,
delegate, credential_pool, context_compressor, plus the 4 hostname test
modules). 26-assertion E2E call-site verification across 6 modules passes.
2026-04-20 21:17:28 -07:00
|
|
|
|
return base_url_host_matches(base_url, "openrouter.ai")
|
2026-03-18 03:04:07 -07:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _is_custom_endpoint(base_url: str) -> bool:
|
|
|
|
|
|
normalized = _normalize_base_url(base_url)
|
|
|
|
|
|
return bool(normalized) and not _is_openrouter_base_url(normalized)
|
|
|
|
|
|
|
|
|
|
|
|
|
fix: infer provider from base URL for models.dev context length lookup
Custom endpoint users (DashScope/Alibaba, Z.AI, Kimi, DeepSeek, etc.)
get wrong context lengths because their provider resolves as "openrouter"
or "custom", skipping the models.dev lookup entirely. For example,
qwen3.5-plus on DashScope falls to the generic "qwen" hardcoded default
(131K) instead of the correct 1M.
Add _infer_provider_from_url() that maps known API hostnames to their
models.dev provider IDs. When the explicit provider is generic
(openrouter/custom/empty), infer from the base URL before the models.dev
lookup. This resolves context lengths correctly for DashScope, Z.AI,
Kimi, MiniMax, DeepSeek, and Nous endpoints without requiring users to
manually set context_length in config.
Also refactors _is_known_provider_base_url() to use the same URL mapping,
removing the duplicated hostname list.
2026-03-20 11:57:24 -07:00
|
|
|
|
_URL_TO_PROVIDER: Dict[str, str] = {
|
|
|
|
|
|
"api.openai.com": "openai",
|
|
|
|
|
|
"chatgpt.com": "openai",
|
|
|
|
|
|
"api.anthropic.com": "anthropic",
|
|
|
|
|
|
"api.z.ai": "zai",
|
2026-04-11 23:20:53 +08:00
|
|
|
|
"open.bigmodel.cn": "zai",
|
fix: infer provider from base URL for models.dev context length lookup
Custom endpoint users (DashScope/Alibaba, Z.AI, Kimi, DeepSeek, etc.)
get wrong context lengths because their provider resolves as "openrouter"
or "custom", skipping the models.dev lookup entirely. For example,
qwen3.5-plus on DashScope falls to the generic "qwen" hardcoded default
(131K) instead of the correct 1M.
Add _infer_provider_from_url() that maps known API hostnames to their
models.dev provider IDs. When the explicit provider is generic
(openrouter/custom/empty), infer from the base URL before the models.dev
lookup. This resolves context lengths correctly for DashScope, Z.AI,
Kimi, MiniMax, DeepSeek, and Nous endpoints without requiring users to
manually set context_length in config.
Also refactors _is_known_provider_base_url() to use the same URL mapping,
removing the duplicated hostname list.
2026-03-20 11:57:24 -07:00
|
|
|
|
"api.moonshot.ai": "kimi-coding",
|
2026-04-13 11:13:09 -07:00
|
|
|
|
"api.moonshot.cn": "kimi-coding-cn",
|
fix: infer provider from base URL for models.dev context length lookup
Custom endpoint users (DashScope/Alibaba, Z.AI, Kimi, DeepSeek, etc.)
get wrong context lengths because their provider resolves as "openrouter"
or "custom", skipping the models.dev lookup entirely. For example,
qwen3.5-plus on DashScope falls to the generic "qwen" hardcoded default
(131K) instead of the correct 1M.
Add _infer_provider_from_url() that maps known API hostnames to their
models.dev provider IDs. When the explicit provider is generic
(openrouter/custom/empty), infer from the base URL before the models.dev
lookup. This resolves context lengths correctly for DashScope, Z.AI,
Kimi, MiniMax, DeepSeek, and Nous endpoints without requiring users to
manually set context_length in config.
Also refactors _is_known_provider_base_url() to use the same URL mapping,
removing the duplicated hostname list.
2026-03-20 11:57:24 -07:00
|
|
|
|
"api.kimi.com": "kimi-coding",
|
2026-04-22 13:28:01 +05:30
|
|
|
|
"api.stepfun.ai": "stepfun",
|
|
|
|
|
|
"api.stepfun.com": "stepfun",
|
feat(providers): add Arcee AI as direct API provider
Adds Arcee AI as a standard direct provider (ARCEEAI_API_KEY) with
Trinity models: trinity-large-thinking, trinity-large-preview, trinity-mini.
Standard OpenAI-compatible provider checklist: auth.py, config.py,
models.py, main.py, providers.py, doctor.py, model_normalize.py,
model_metadata.py, setup.py, trajectory_compressor.py.
Based on PR #9274 by arthurbr11, simplified to a standard direct
provider without dual-endpoint OpenRouter routing.
2026-04-13 17:16:43 -07:00
|
|
|
|
"api.arcee.ai": "arcee",
|
fix: infer provider from base URL for models.dev context length lookup
Custom endpoint users (DashScope/Alibaba, Z.AI, Kimi, DeepSeek, etc.)
get wrong context lengths because their provider resolves as "openrouter"
or "custom", skipping the models.dev lookup entirely. For example,
qwen3.5-plus on DashScope falls to the generic "qwen" hardcoded default
(131K) instead of the correct 1M.
Add _infer_provider_from_url() that maps known API hostnames to their
models.dev provider IDs. When the explicit provider is generic
(openrouter/custom/empty), infer from the base URL before the models.dev
lookup. This resolves context lengths correctly for DashScope, Z.AI,
Kimi, MiniMax, DeepSeek, and Nous endpoints without requiring users to
manually set context_length in config.
Also refactors _is_known_provider_base_url() to use the same URL mapping,
removing the duplicated hostname list.
2026-03-20 11:57:24 -07:00
|
|
|
|
"api.minimax": "minimax",
|
|
|
|
|
|
"dashscope.aliyuncs.com": "alibaba",
|
2026-03-20 12:51:39 -07:00
|
|
|
|
"dashscope-intl.aliyuncs.com": "alibaba",
|
feat(qwen): add Qwen OAuth provider with portal request support
Based on #6079 by @tunamitom with critical fixes and comprehensive tests.
Changes from #6079:
- Fix: sanitization overwrite bug — Qwen message prep now runs AFTER codex
field sanitization, not before (was silently discarding Qwen transforms)
- Fix: missing try/except AuthError in runtime_provider.py — stale Qwen
credentials now fall through to next provider on auto-detect
- Fix: 'qwen' alias conflict — bare 'qwen' stays mapped to 'alibaba'
(DashScope); use 'qwen-portal' or 'qwen-cli' for the OAuth provider
- Fix: hardcoded ['coder-model'] replaced with live API fetch + curated
fallback list (qwen3-coder-plus, qwen3-coder)
- Fix: extract _is_qwen_portal() helper + _qwen_portal_headers() to replace
5 inline 'portal.qwen.ai' string checks and share headers between init
and credential swap
- Fix: add Qwen branch to _apply_client_headers_for_base_url for mid-session
credential swaps
- Fix: remove suspicious TypeError catch blocks around _prompt_provider_choice
- Fix: handle bare string items in content lists (were silently dropped)
- Fix: remove redundant dict() copies after deepcopy in message prep
- Revert: unrelated ai-gateway test mock removal and model_switch.py comment deletion
New tests (30 test functions):
- _qwen_cli_auth_path, _read_qwen_cli_tokens (success + 3 error paths)
- _save_qwen_cli_tokens (roundtrip, parent creation, permissions)
- _qwen_access_token_is_expiring (5 edge cases: fresh, expired, within skew,
None, non-numeric)
- _refresh_qwen_cli_tokens (success, preserve old refresh, 4 error paths,
default expires_in, disk persistence)
- resolve_qwen_runtime_credentials (fresh, auto-refresh, force-refresh,
missing token, env override)
- get_qwen_auth_status (logged in, not logged in)
- Runtime provider resolution (direct, pool entry, alias)
- _build_api_kwargs (metadata, vl_high_resolution_images, message formatting,
max_tokens suppression)
2026-04-08 20:48:21 +05:30
|
|
|
|
"portal.qwen.ai": "qwen-oauth",
|
fix: infer provider from base URL for models.dev context length lookup
Custom endpoint users (DashScope/Alibaba, Z.AI, Kimi, DeepSeek, etc.)
get wrong context lengths because their provider resolves as "openrouter"
or "custom", skipping the models.dev lookup entirely. For example,
qwen3.5-plus on DashScope falls to the generic "qwen" hardcoded default
(131K) instead of the correct 1M.
Add _infer_provider_from_url() that maps known API hostnames to their
models.dev provider IDs. When the explicit provider is generic
(openrouter/custom/empty), infer from the base URL before the models.dev
lookup. This resolves context lengths correctly for DashScope, Z.AI,
Kimi, MiniMax, DeepSeek, and Nous endpoints without requiring users to
manually set context_length in config.
Also refactors _is_known_provider_base_url() to use the same URL mapping,
removing the duplicated hostname list.
2026-03-20 11:57:24 -07:00
|
|
|
|
"openrouter.ai": "openrouter",
|
2026-04-06 10:14:01 -07:00
|
|
|
|
"generativelanguage.googleapis.com": "gemini",
|
fix: infer provider from base URL for models.dev context length lookup
Custom endpoint users (DashScope/Alibaba, Z.AI, Kimi, DeepSeek, etc.)
get wrong context lengths because their provider resolves as "openrouter"
or "custom", skipping the models.dev lookup entirely. For example,
qwen3.5-plus on DashScope falls to the generic "qwen" hardcoded default
(131K) instead of the correct 1M.
Add _infer_provider_from_url() that maps known API hostnames to their
models.dev provider IDs. When the explicit provider is generic
(openrouter/custom/empty), infer from the base URL before the models.dev
lookup. This resolves context lengths correctly for DashScope, Z.AI,
Kimi, MiniMax, DeepSeek, and Nous endpoints without requiring users to
manually set context_length in config.
Also refactors _is_known_provider_base_url() to use the same URL mapping,
removing the duplicated hostname list.
2026-03-20 11:57:24 -07:00
|
|
|
|
"inference-api.nousresearch.com": "nous",
|
|
|
|
|
|
"api.deepseek.com": "deepseek",
|
2026-03-22 08:15:06 -07:00
|
|
|
|
"api.githubcopilot.com": "copilot",
|
2026-05-22 15:30:22 +00:00
|
|
|
|
# Enterprise Copilot endpoints look like api.enterprise.githubcopilot.com,
|
|
|
|
|
|
# api.business.githubcopilot.com, etc. Match the suffix so context-window
|
|
|
|
|
|
# resolution works for enterprise accounts too.
|
|
|
|
|
|
".githubcopilot.com": "copilot",
|
2026-03-22 08:15:06 -07:00
|
|
|
|
"models.github.ai": "copilot",
|
2026-05-16 01:58:13 -07:00
|
|
|
|
# GitHub Models free tier (Azure-hosted prototyping endpoint) — same
|
|
|
|
|
|
# canonical provider as the Copilot API. Hard per-request token cap
|
|
|
|
|
|
# (often 8K) makes it unusable for Hermes' system prompt, but mapping
|
|
|
|
|
|
# it here lets us recognize the endpoint and emit a targeted hint
|
|
|
|
|
|
# instead of falling through the unknown-custom-endpoint path.
|
|
|
|
|
|
"models.inference.ai.azure.com": "copilot",
|
2026-03-30 20:37:08 -07:00
|
|
|
|
"api.fireworks.ai": "fireworks",
|
2026-04-02 19:59:19 -05:00
|
|
|
|
"opencode.ai": "opencode-go",
|
2026-04-10 12:51:30 +04:00
|
|
|
|
"api.x.ai": "xai",
|
feat(providers): add native NVIDIA NIM provider
Adds NVIDIA NIM as a first-class provider: ProviderConfig in
auth.py, HermesOverlay in providers.py, curated models
(Nemotron plus other open source models hosted on
build.nvidia.com), URL mapping in model_metadata.py, aliases
(nim, nvidia-nim, build-nvidia, nemotron), and env var tests.
Docs updated: providers page, quickstart table, fallback
providers table, and README provider list.
2026-04-17 09:55:58 -07:00
|
|
|
|
"integrate.api.nvidia.com": "nvidia",
|
feat(xiaomi): add Xiaomi MiMo as first-class provider
Cherry-picked from PR #7702 by kshitijk4poor.
Adds Xiaomi MiMo as a direct provider (XIAOMI_API_KEY) with models:
- mimo-v2-pro (1M context), mimo-v2-omni (256K, multimodal), mimo-v2-flash (256K, cheapest)
Standard OpenAI-compatible provider checklist: auth.py, config.py, models.py,
main.py, providers.py, doctor.py, model_normalize.py, model_metadata.py,
models_dev.py, auxiliary_client.py, .env.example, cli-config.yaml.example.
Follow-up: vision tasks use mimo-v2-omni (multimodal) instead of the user's
main model. Non-vision aux uses the user's selected model. Added
_PROVIDER_VISION_MODELS dict for provider-specific vision model overrides.
On failure, falls back to aggregators (gemini flash) via existing fallback chain.
Corrects pre-existing context lengths: mimo-v2-pro 1048576→1000000,
mimo-v2-omni 1048576→256000, adds mimo-v2-flash 256000.
36 tests covering registry, aliases, auto-detect, credentials, models.dev,
normalization, URL mapping, providers module, doctor, aux client, vision
model override, and agent init.
2026-04-11 10:10:31 -07:00
|
|
|
|
"api.xiaomimimo.com": "xiaomi",
|
|
|
|
|
|
"xiaomimimo.com": "xiaomi",
|
feat(providers): add GMI Cloud as a first-class API-key provider (#11955)
Add GMI Cloud (api.gmi-serving.com) as a full first-class API-key provider
with built-in auth, aliases, model catalog, CLI entry points, auxiliary client
routing, context length resolution, doctor checks, env var tracking, and docs.
- auth.py: ProviderConfig for 'gmi' (api_key, GMI_API_KEY / GMI_BASE_URL)
- providers.py: HermesOverlay with extra_env_vars for models.dev detection
- models.py: curated slash-form model catalog; live /v1/models fetch
- main.py: 'gmi' in _named_custom_provider_map and --provider choices
- model_metadata.py: _URL_TO_PROVIDER, _PROVIDER_PREFIXES, dedicated
context-length probe block (GMI's /models has authoritative data)
- auxiliary_client.py: alias entries; _compat_model fix for slash-form
models on cached aggregator-style clients; gmi aux default model
- doctor.py: GMI in provider connectivity checks
- config.py: GMI_API_KEY / GMI_BASE_URL in OPTIONAL_ENV_VARS
- conftest.py: explicit GMI_BASE_URL clearing (not caught by _API_KEY suffix)
- docs: providers.md, environment-variables.md, fallback-providers.md,
configuration.md, quickstart.md (expands provider table)
Co-authored-by: Isaac Huang <isaachuang@Isaacs-MacBook-Pro.local>
2026-04-17 11:19:56 -07:00
|
|
|
|
"api.gmi-serving.com": "gmi",
|
feat: add NovitaAI as LLM provider
Add NovitaAI as a first-class provider with dedicated model selection
flow, live pricing, and authoritative context length resolution.
- Register provider in PROVIDER_REGISTRY, HERMES_OVERLAYS, and all
alias/label maps (ID: novita, aliases: novita-ai, novitaai)
- Add dedicated _model_flow_novita() with 3-tier model list fallback:
Novita API → models.dev → static curated list
- Fetch live pricing from /v1/models with correct unit conversion
(input_token_price_per_m is 0.0001 USD per Mtok)
- Add Novita-specific context length resolution (step 4b) in
get_model_context_length(), prioritized over models.dev/OpenRouter
- Register api.novita.ai in _URL_TO_PROVIDER to prevent early return
from the custom-endpoint code path
- Add models.dev mapping (novita → novita-ai)
- Add default auxiliary model (deepseek/deepseek-v3-0324)
- Add NOVITA_API_KEY to test isolation (conftest.py)
- Update docs: providers page, env vars reference, CLI reference,
.env.example, README, and landing page
2026-04-10 22:22:47 +08:00
|
|
|
|
"api.novita.ai": "novita",
|
feat(providers): add tencent-tokenhub provider support
Registers tencent-tokenhub (https://tokenhub.tencentmaas.com/v1) as a
new API-key provider with model tencent/hy3-preview (256K context).
- PROVIDER_REGISTRY entry + TOKENHUB_API_KEY / TOKENHUB_BASE_URL env vars
- Aliases: tencent, tokenhub, tencent-cloud, tencentmaas
- openai_chat transport with is_tokenhub branch for top-level
reasoning_effort (Hy3 is a reasoning model)
- tencent/hy3-preview:free added to OpenRouter curated list
- 60+ tests (provider registry, aliases, runtime resolution,
credentials, model catalog, URL mapping, context length)
- Docs: integrations/providers.md, environment-variables.md,
model-catalog.json
Author: simonweng <simonweng@tencent.com>
Salvaged from PR #16860 onto current main (resolved conflicts with
#16935 Azure Anthropic env-var hint tests and the --provider choices=
list removal in chat_parser).
2026-04-28 03:40:45 -07:00
|
|
|
|
"tokenhub.tencentmaas.com": "tencent-tokenhub",
|
2026-04-15 22:32:05 -07:00
|
|
|
|
"ollama.com": "ollama-cloud",
|
fix: infer provider from base URL for models.dev context length lookup
Custom endpoint users (DashScope/Alibaba, Z.AI, Kimi, DeepSeek, etc.)
get wrong context lengths because their provider resolves as "openrouter"
or "custom", skipping the models.dev lookup entirely. For example,
qwen3.5-plus on DashScope falls to the generic "qwen" hardcoded default
(131K) instead of the correct 1M.
Add _infer_provider_from_url() that maps known API hostnames to their
models.dev provider IDs. When the explicit provider is generic
(openrouter/custom/empty), infer from the base URL before the models.dev
lookup. This resolves context lengths correctly for DashScope, Z.AI,
Kimi, MiniMax, DeepSeek, and Nous endpoints without requiring users to
manually set context_length in config.
Also refactors _is_known_provider_base_url() to use the same URL mapping,
removing the duplicated hostname list.
2026-03-20 11:57:24 -07:00
|
|
|
|
}
|
|
|
|
|
|
|
feat: provider modules — ProviderProfile ABC, 33 providers, fetch_models, transport single-path
Introduces providers/ package — single source of truth for every
inference provider. Adding a simple api-key provider now requires one
providers/<name>.py file with zero edits anywhere else.
What this PR ships:
- providers/ package (ProviderProfile ABC + 33 profiles across 4 api_modes)
- ProviderProfile declarative fields: name, api_mode, aliases, display_name,
env_vars, base_url, models_url, auth_type, fallback_models, hostname,
default_headers, fixed_temperature, default_max_tokens, default_aux_model
- 4 overridable hooks: prepare_messages, build_extra_body,
build_api_kwargs_extras, fetch_models
- chat_completions.build_kwargs: profile path via _build_kwargs_from_profile,
legacy flag path retained for lmstudio/tencent-tokenhub (which have
session-aware reasoning probing that doesn't map cleanly to hooks yet)
- run_agent.py: profile path for all registered providers; legacy path
variable scoping fixed (all flags defined before branching)
- Auto-wires: auth.PROVIDER_REGISTRY, models.CANONICAL_PROVIDERS,
doctor health checks, config.OPTIONAL_ENV_VARS, model_metadata._URL_TO_PROVIDER
- GeminiProfile: thinking_config translation (native + openai-compat nested)
- New tests/providers/ (79 tests covering profile declarations, transport
parity, hook overrides, e2e kwargs assembly)
Deltas vs original PR (salvaged onto current main):
- Added profiles: alibaba-coding-plan, azure-foundry, minimax-oauth
(were added to main since original PR)
- Skipped profiles: lmstudio, tencent-tokenhub stay on legacy path (their
reasoning_effort probing has no clean hook equivalent yet)
- Removed lmstudio alias from custom profile (it's a separate provider now)
- Skipped openrouter/custom from PROVIDER_REGISTRY auto-extension
(resolve_provider special-cases them; adding breaks runtime resolution)
- runtime_provider: profile.api_mode only as fallback when URL detection
finds nothing (was breaking minimax /v1 override)
- Preserved main's legacy-path improvements: deepseek reasoning_content
preserve, gemini Gemma skip, OpenRouter response caching, Anthropic 1M
beta recovery, etc.
- Kept agent/copilot_acp_client.py in place (rejected PR's relocation —
main has 7 fixes landed since; relocation would revert them)
- _API_KEY_PROVIDER_AUX_MODELS alias kept for backward compat with existing
test imports
Co-authored-by: kshitijk4poor <82637225+kshitijk4poor@users.noreply.github.com>
Closes #14418
2026-05-05 10:18:49 -07:00
|
|
|
|
# Auto-extend with hostnames derived from provider profiles.
|
|
|
|
|
|
# Any provider with a base_url not already in the map gets added automatically.
|
|
|
|
|
|
try:
|
|
|
|
|
|
from providers import list_providers as _list_providers
|
|
|
|
|
|
for _pp in _list_providers():
|
|
|
|
|
|
_host = _pp.get_hostname()
|
|
|
|
|
|
if _host and _host not in _URL_TO_PROVIDER:
|
|
|
|
|
|
_URL_TO_PROVIDER[_host] = _pp.name
|
|
|
|
|
|
except Exception:
|
|
|
|
|
|
pass
|
|
|
|
|
|
|
fix: infer provider from base URL for models.dev context length lookup
Custom endpoint users (DashScope/Alibaba, Z.AI, Kimi, DeepSeek, etc.)
get wrong context lengths because their provider resolves as "openrouter"
or "custom", skipping the models.dev lookup entirely. For example,
qwen3.5-plus on DashScope falls to the generic "qwen" hardcoded default
(131K) instead of the correct 1M.
Add _infer_provider_from_url() that maps known API hostnames to their
models.dev provider IDs. When the explicit provider is generic
(openrouter/custom/empty), infer from the base URL before the models.dev
lookup. This resolves context lengths correctly for DashScope, Z.AI,
Kimi, MiniMax, DeepSeek, and Nous endpoints without requiring users to
manually set context_length in config.
Also refactors _is_known_provider_base_url() to use the same URL mapping,
removing the duplicated hostname list.
2026-03-20 11:57:24 -07:00
|
|
|
|
|
|
|
|
|
|
def _infer_provider_from_url(base_url: str) -> Optional[str]:
|
|
|
|
|
|
"""Infer the models.dev provider name from a base URL.
|
|
|
|
|
|
|
|
|
|
|
|
This allows context length resolution via models.dev for custom endpoints
|
|
|
|
|
|
like DashScope (Alibaba), Z.AI, Kimi, etc. without requiring the user to
|
|
|
|
|
|
explicitly set the provider name in config.
|
|
|
|
|
|
"""
|
2026-03-18 03:04:07 -07:00
|
|
|
|
normalized = _normalize_base_url(base_url)
|
|
|
|
|
|
if not normalized:
|
fix: infer provider from base URL for models.dev context length lookup
Custom endpoint users (DashScope/Alibaba, Z.AI, Kimi, DeepSeek, etc.)
get wrong context lengths because their provider resolves as "openrouter"
or "custom", skipping the models.dev lookup entirely. For example,
qwen3.5-plus on DashScope falls to the generic "qwen" hardcoded default
(131K) instead of the correct 1M.
Add _infer_provider_from_url() that maps known API hostnames to their
models.dev provider IDs. When the explicit provider is generic
(openrouter/custom/empty), infer from the base URL before the models.dev
lookup. This resolves context lengths correctly for DashScope, Z.AI,
Kimi, MiniMax, DeepSeek, and Nous endpoints without requiring users to
manually set context_length in config.
Also refactors _is_known_provider_base_url() to use the same URL mapping,
removing the duplicated hostname list.
2026-03-20 11:57:24 -07:00
|
|
|
|
return None
|
2026-03-18 03:04:07 -07:00
|
|
|
|
parsed = urlparse(normalized if "://" in normalized else f"https://{normalized}")
|
|
|
|
|
|
host = parsed.netloc.lower() or parsed.path.lower()
|
fix: infer provider from base URL for models.dev context length lookup
Custom endpoint users (DashScope/Alibaba, Z.AI, Kimi, DeepSeek, etc.)
get wrong context lengths because their provider resolves as "openrouter"
or "custom", skipping the models.dev lookup entirely. For example,
qwen3.5-plus on DashScope falls to the generic "qwen" hardcoded default
(131K) instead of the correct 1M.
Add _infer_provider_from_url() that maps known API hostnames to their
models.dev provider IDs. When the explicit provider is generic
(openrouter/custom/empty), infer from the base URL before the models.dev
lookup. This resolves context lengths correctly for DashScope, Z.AI,
Kimi, MiniMax, DeepSeek, and Nous endpoints without requiring users to
manually set context_length in config.
Also refactors _is_known_provider_base_url() to use the same URL mapping,
removing the duplicated hostname list.
2026-03-20 11:57:24 -07:00
|
|
|
|
for url_part, provider in _URL_TO_PROVIDER.items():
|
|
|
|
|
|
if url_part in host:
|
|
|
|
|
|
return provider
|
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-06-27 08:06:51 +10:00
|
|
|
|
def _lmstudio_server_root(base_url: str) -> str:
|
|
|
|
|
|
"""Return the LM Studio server root for native ``/api/v1`` endpoints."""
|
|
|
|
|
|
root = _normalize_base_url(base_url).rstrip("/")
|
|
|
|
|
|
for suffix in ("/api/v1", "/api", "/v1"):
|
|
|
|
|
|
if root.endswith(suffix):
|
|
|
|
|
|
root = root[: -len(suffix)].rstrip("/")
|
|
|
|
|
|
break
|
|
|
|
|
|
return root
|
|
|
|
|
|
|
|
|
|
|
|
|
fix: infer provider from base URL for models.dev context length lookup
Custom endpoint users (DashScope/Alibaba, Z.AI, Kimi, DeepSeek, etc.)
get wrong context lengths because their provider resolves as "openrouter"
or "custom", skipping the models.dev lookup entirely. For example,
qwen3.5-plus on DashScope falls to the generic "qwen" hardcoded default
(131K) instead of the correct 1M.
Add _infer_provider_from_url() that maps known API hostnames to their
models.dev provider IDs. When the explicit provider is generic
(openrouter/custom/empty), infer from the base URL before the models.dev
lookup. This resolves context lengths correctly for DashScope, Z.AI,
Kimi, MiniMax, DeepSeek, and Nous endpoints without requiring users to
manually set context_length in config.
Also refactors _is_known_provider_base_url() to use the same URL mapping,
removing the duplicated hostname list.
2026-03-20 11:57:24 -07:00
|
|
|
|
def _is_known_provider_base_url(base_url: str) -> bool:
|
|
|
|
|
|
return _infer_provider_from_url(base_url) is not None
|
2026-03-18 03:04:07 -07:00
|
|
|
|
|
|
|
|
|
|
|
2026-03-18 21:38:41 +01:00
|
|
|
|
def is_local_endpoint(base_url: str) -> bool:
|
2026-04-13 06:17:13 +02:00
|
|
|
|
"""Return True if base_url points to a local machine.
|
|
|
|
|
|
|
|
|
|
|
|
Recognises loopback (``localhost``, ``127.0.0.0/8``, ``::1``),
|
|
|
|
|
|
container-internal DNS names (``host.docker.internal`` et al.),
|
|
|
|
|
|
RFC-1918 private ranges (``10/8``, ``172.16/12``, ``192.168/16``),
|
|
|
|
|
|
link-local, and Tailscale CGNAT (``100.64.0.0/10``). Tailscale CGNAT
|
|
|
|
|
|
is included so remote-but-trusted Ollama boxes reached over a
|
|
|
|
|
|
Tailscale mesh get the same timeout auto-bumps as localhost Ollama.
|
|
|
|
|
|
"""
|
2026-03-18 21:38:41 +01:00
|
|
|
|
normalized = _normalize_base_url(base_url)
|
|
|
|
|
|
if not normalized:
|
|
|
|
|
|
return False
|
|
|
|
|
|
url = normalized if "://" in normalized else f"http://{normalized}"
|
|
|
|
|
|
try:
|
|
|
|
|
|
parsed = urlparse(url)
|
|
|
|
|
|
host = parsed.hostname or ""
|
|
|
|
|
|
except Exception:
|
|
|
|
|
|
return False
|
|
|
|
|
|
if host in _LOCAL_HOSTS:
|
|
|
|
|
|
return True
|
2026-04-11 14:46:18 -07:00
|
|
|
|
# Docker / Podman / Lima internal DNS names (e.g. host.docker.internal)
|
|
|
|
|
|
if any(host.endswith(suffix) for suffix in _CONTAINER_LOCAL_SUFFIXES):
|
|
|
|
|
|
return True
|
2026-06-05 10:18:10 +10:00
|
|
|
|
# Unqualified hostnames (no dots) are local by definition — Docker
|
|
|
|
|
|
# Compose service names, /etc/hosts entries, or mDNS names.
|
|
|
|
|
|
if host and "." not in host:
|
|
|
|
|
|
return True
|
2026-04-13 06:17:13 +02:00
|
|
|
|
# RFC-1918 private ranges, link-local, and Tailscale CGNAT
|
2026-03-18 21:38:41 +01:00
|
|
|
|
try:
|
|
|
|
|
|
addr = ipaddress.ip_address(host)
|
2026-04-13 06:17:13 +02:00
|
|
|
|
if addr.is_private or addr.is_loopback or addr.is_link_local:
|
|
|
|
|
|
return True
|
|
|
|
|
|
if isinstance(addr, ipaddress.IPv4Address) and addr in _TAILSCALE_CGNAT:
|
|
|
|
|
|
return True
|
2026-03-18 21:38:41 +01:00
|
|
|
|
except ValueError:
|
|
|
|
|
|
pass
|
|
|
|
|
|
# Bare IP that looks like a private range (e.g. 172.26.x.x for WSL)
|
2026-04-13 06:17:13 +02:00
|
|
|
|
# or Tailscale CGNAT (100.64.x.x–100.127.x.x).
|
2026-03-18 21:38:41 +01:00
|
|
|
|
parts = host.split(".")
|
|
|
|
|
|
if len(parts) == 4:
|
|
|
|
|
|
try:
|
|
|
|
|
|
first, second = int(parts[0]), int(parts[1])
|
|
|
|
|
|
if first == 10:
|
|
|
|
|
|
return True
|
|
|
|
|
|
if first == 172 and 16 <= second <= 31:
|
|
|
|
|
|
return True
|
|
|
|
|
|
if first == 192 and second == 168:
|
|
|
|
|
|
return True
|
2026-04-13 06:17:13 +02:00
|
|
|
|
if first == 100 and 64 <= second <= 127:
|
|
|
|
|
|
return True
|
2026-03-18 21:38:41 +01:00
|
|
|
|
except ValueError:
|
|
|
|
|
|
pass
|
|
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-04-20 20:49:44 -07:00
|
|
|
|
def detect_local_server_type(base_url: str, api_key: str = "") -> Optional[str]:
|
2026-03-18 21:38:41 +01:00
|
|
|
|
"""Detect which local server is running at base_url by probing known endpoints.
|
|
|
|
|
|
|
2026-03-19 21:32:04 +01:00
|
|
|
|
Returns one of: "ollama", "lm-studio", "vllm", "llamacpp", or None.
|
2026-03-18 21:38:41 +01:00
|
|
|
|
"""
|
|
|
|
|
|
import httpx
|
|
|
|
|
|
|
|
|
|
|
|
normalized = _normalize_base_url(base_url)
|
|
|
|
|
|
server_url = normalized
|
|
|
|
|
|
if server_url.endswith("/v1"):
|
|
|
|
|
|
server_url = server_url[:-3]
|
2026-06-27 08:06:51 +10:00
|
|
|
|
lmstudio_url = _lmstudio_server_root(base_url)
|
2026-03-18 21:38:41 +01:00
|
|
|
|
|
2026-04-20 20:49:44 -07:00
|
|
|
|
headers = _auth_headers(api_key)
|
|
|
|
|
|
|
2026-03-18 21:38:41 +01:00
|
|
|
|
try:
|
2026-04-20 20:49:44 -07:00
|
|
|
|
with httpx.Client(timeout=2.0, headers=headers) as client:
|
2026-03-19 21:32:04 +01:00
|
|
|
|
# LM Studio exposes /api/v1/models — check first (most specific)
|
2026-03-18 21:38:41 +01:00
|
|
|
|
try:
|
2026-06-27 08:06:51 +10:00
|
|
|
|
r = client.get(f"{lmstudio_url}/api/v1/models")
|
2026-03-18 21:38:41 +01:00
|
|
|
|
if r.status_code == 200:
|
2026-03-19 21:32:04 +01:00
|
|
|
|
return "lm-studio"
|
2026-03-18 21:38:41 +01:00
|
|
|
|
except Exception:
|
|
|
|
|
|
pass
|
2026-03-19 21:32:04 +01:00
|
|
|
|
# Ollama exposes /api/tags and responds with {"models": [...]}
|
|
|
|
|
|
# LM Studio returns {"error": "Unexpected endpoint"} with status 200
|
|
|
|
|
|
# on this path, so we must verify the response contains "models".
|
2026-03-18 21:38:41 +01:00
|
|
|
|
try:
|
2026-03-19 21:32:04 +01:00
|
|
|
|
r = client.get(f"{server_url}/api/tags")
|
2026-03-18 21:38:41 +01:00
|
|
|
|
if r.status_code == 200:
|
2026-03-19 21:32:04 +01:00
|
|
|
|
try:
|
|
|
|
|
|
data = r.json()
|
|
|
|
|
|
if "models" in data:
|
|
|
|
|
|
return "ollama"
|
|
|
|
|
|
except Exception:
|
|
|
|
|
|
pass
|
2026-03-18 21:38:41 +01:00
|
|
|
|
except Exception:
|
|
|
|
|
|
pass
|
2026-03-21 18:07:18 -07:00
|
|
|
|
# llama.cpp exposes /v1/props (older builds used /props without the /v1 prefix)
|
2026-03-18 21:38:41 +01:00
|
|
|
|
try:
|
2026-03-21 18:07:18 -07:00
|
|
|
|
r = client.get(f"{server_url}/v1/props")
|
|
|
|
|
|
if r.status_code != 200:
|
|
|
|
|
|
r = client.get(f"{server_url}/props") # fallback for older builds
|
2026-03-18 21:38:41 +01:00
|
|
|
|
if r.status_code == 200 and "default_generation_settings" in r.text:
|
|
|
|
|
|
return "llamacpp"
|
|
|
|
|
|
except Exception:
|
|
|
|
|
|
pass
|
|
|
|
|
|
# vLLM: /version
|
|
|
|
|
|
try:
|
|
|
|
|
|
r = client.get(f"{server_url}/version")
|
|
|
|
|
|
if r.status_code == 200:
|
|
|
|
|
|
data = r.json()
|
|
|
|
|
|
if "version" in data:
|
|
|
|
|
|
return "vllm"
|
|
|
|
|
|
except Exception:
|
|
|
|
|
|
pass
|
|
|
|
|
|
except Exception:
|
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-03-18 03:04:07 -07:00
|
|
|
|
def _iter_nested_dicts(value: Any):
|
|
|
|
|
|
if isinstance(value, dict):
|
|
|
|
|
|
yield value
|
|
|
|
|
|
for nested in value.values():
|
|
|
|
|
|
yield from _iter_nested_dicts(nested)
|
|
|
|
|
|
elif isinstance(value, list):
|
|
|
|
|
|
for item in value:
|
|
|
|
|
|
yield from _iter_nested_dicts(item)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _coerce_reasonable_int(value: Any, minimum: int = 1024, maximum: int = 10_000_000) -> Optional[int]:
|
|
|
|
|
|
try:
|
|
|
|
|
|
if isinstance(value, bool):
|
|
|
|
|
|
return None
|
|
|
|
|
|
if isinstance(value, str):
|
|
|
|
|
|
value = value.strip().replace(",", "")
|
|
|
|
|
|
result = int(value)
|
|
|
|
|
|
except (TypeError, ValueError):
|
|
|
|
|
|
return None
|
|
|
|
|
|
if minimum <= result <= maximum:
|
|
|
|
|
|
return result
|
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _extract_first_int(payload: Dict[str, Any], keys: tuple[str, ...]) -> Optional[int]:
|
|
|
|
|
|
keyset = {key.lower() for key in keys}
|
|
|
|
|
|
for mapping in _iter_nested_dicts(payload):
|
|
|
|
|
|
for key, value in mapping.items():
|
|
|
|
|
|
if str(key).lower() not in keyset:
|
|
|
|
|
|
continue
|
|
|
|
|
|
coerced = _coerce_reasonable_int(value)
|
|
|
|
|
|
if coerced is not None:
|
|
|
|
|
|
return coerced
|
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _extract_context_length(payload: Dict[str, Any]) -> Optional[int]:
|
|
|
|
|
|
return _extract_first_int(payload, _CONTEXT_LENGTH_KEYS)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _extract_max_completion_tokens(payload: Dict[str, Any]) -> Optional[int]:
|
|
|
|
|
|
return _extract_first_int(payload, _MAX_COMPLETION_KEYS)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _extract_pricing(payload: Dict[str, Any]) -> Dict[str, Any]:
|
feat: add NovitaAI as LLM provider
Add NovitaAI as a first-class provider with dedicated model selection
flow, live pricing, and authoritative context length resolution.
- Register provider in PROVIDER_REGISTRY, HERMES_OVERLAYS, and all
alias/label maps (ID: novita, aliases: novita-ai, novitaai)
- Add dedicated _model_flow_novita() with 3-tier model list fallback:
Novita API → models.dev → static curated list
- Fetch live pricing from /v1/models with correct unit conversion
(input_token_price_per_m is 0.0001 USD per Mtok)
- Add Novita-specific context length resolution (step 4b) in
get_model_context_length(), prioritized over models.dev/OpenRouter
- Register api.novita.ai in _URL_TO_PROVIDER to prevent early return
from the custom-endpoint code path
- Add models.dev mapping (novita → novita-ai)
- Add default auxiliary model (deepseek/deepseek-v3-0324)
- Add NOVITA_API_KEY to test isolation (conftest.py)
- Update docs: providers page, env vars reference, CLI reference,
.env.example, README, and landing page
2026-04-10 22:22:47 +08:00
|
|
|
|
novita_input = payload.get("input_token_price_per_m")
|
|
|
|
|
|
novita_output = payload.get("output_token_price_per_m")
|
|
|
|
|
|
if novita_input is not None or novita_output is not None:
|
|
|
|
|
|
pricing: Dict[str, Any] = {}
|
|
|
|
|
|
if novita_input is not None:
|
|
|
|
|
|
pricing["prompt"] = str(float(novita_input) / 10_000 / 1_000_000)
|
|
|
|
|
|
if novita_output is not None:
|
|
|
|
|
|
pricing["completion"] = str(float(novita_output) / 10_000 / 1_000_000)
|
|
|
|
|
|
return pricing
|
|
|
|
|
|
|
2026-03-18 03:04:07 -07:00
|
|
|
|
alias_map = {
|
|
|
|
|
|
"prompt": ("prompt", "input", "input_cost_per_token", "prompt_token_cost"),
|
|
|
|
|
|
"completion": ("completion", "output", "output_cost_per_token", "completion_token_cost"),
|
|
|
|
|
|
"request": ("request", "request_cost"),
|
|
|
|
|
|
"cache_read": ("cache_read", "cached_prompt", "input_cache_read", "cache_read_cost_per_token"),
|
|
|
|
|
|
"cache_write": ("cache_write", "cache_creation", "input_cache_write", "cache_write_cost_per_token"),
|
|
|
|
|
|
}
|
|
|
|
|
|
for mapping in _iter_nested_dicts(payload):
|
|
|
|
|
|
normalized = {str(key).lower(): value for key, value in mapping.items()}
|
|
|
|
|
|
if not any(any(alias in normalized for alias in aliases) for aliases in alias_map.values()):
|
|
|
|
|
|
continue
|
|
|
|
|
|
pricing: Dict[str, Any] = {}
|
|
|
|
|
|
for target, aliases in alias_map.items():
|
|
|
|
|
|
for alias in aliases:
|
2026-05-11 11:13:25 -07:00
|
|
|
|
if alias in normalized and normalized[alias] not in {None, ""}:
|
2026-03-18 03:04:07 -07:00
|
|
|
|
pricing[target] = normalized[alias]
|
|
|
|
|
|
break
|
|
|
|
|
|
if pricing:
|
|
|
|
|
|
return pricing
|
|
|
|
|
|
return {}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _add_model_aliases(cache: Dict[str, Dict[str, Any]], model_id: str, entry: Dict[str, Any]) -> None:
|
|
|
|
|
|
cache[model_id] = entry
|
|
|
|
|
|
if "/" in model_id:
|
|
|
|
|
|
bare_model = model_id.split("/", 1)[1]
|
|
|
|
|
|
cache.setdefault(bare_model, entry)
|
|
|
|
|
|
|
2026-02-21 22:31:43 -08:00
|
|
|
|
|
|
|
|
|
|
def fetch_model_metadata(force_refresh: bool = False) -> Dict[str, Dict[str, Any]]:
|
|
|
|
|
|
"""Fetch model metadata from OpenRouter (cached for 1 hour)."""
|
|
|
|
|
|
global _model_metadata_cache, _model_metadata_cache_time
|
|
|
|
|
|
|
|
|
|
|
|
if not force_refresh and _model_metadata_cache and (time.time() - _model_metadata_cache_time) < _MODEL_CACHE_TTL:
|
|
|
|
|
|
return _model_metadata_cache
|
|
|
|
|
|
|
2026-06-14 04:45:46 -07:00
|
|
|
|
if not force_refresh:
|
|
|
|
|
|
disk_age = _model_metadata_disk_cache_age_seconds()
|
|
|
|
|
|
if disk_age is not None and disk_age < _MODEL_CACHE_TTL:
|
|
|
|
|
|
disk_cache = _load_model_metadata_disk_cache()
|
|
|
|
|
|
if disk_cache:
|
|
|
|
|
|
_model_metadata_cache = disk_cache
|
|
|
|
|
|
_model_metadata_cache_time = time.time() - disk_age
|
|
|
|
|
|
return _model_metadata_cache
|
|
|
|
|
|
|
2026-02-21 22:31:43 -08:00
|
|
|
|
try:
|
2026-04-23 14:59:26 +03:00
|
|
|
|
response = requests.get(OPENROUTER_MODELS_URL, timeout=10, verify=_resolve_requests_verify())
|
2026-02-21 22:31:43 -08:00
|
|
|
|
response.raise_for_status()
|
|
|
|
|
|
data = response.json()
|
|
|
|
|
|
|
|
|
|
|
|
cache = {}
|
|
|
|
|
|
for model in data.get("data", []):
|
|
|
|
|
|
model_id = model.get("id", "")
|
2026-03-18 03:04:07 -07:00
|
|
|
|
entry = {
|
2026-02-21 22:31:43 -08:00
|
|
|
|
"context_length": model.get("context_length", 128000),
|
|
|
|
|
|
"max_completion_tokens": model.get("top_provider", {}).get("max_completion_tokens", 4096),
|
|
|
|
|
|
"name": model.get("name", model_id),
|
|
|
|
|
|
"pricing": model.get("pricing", {}),
|
|
|
|
|
|
}
|
2026-03-18 03:04:07 -07:00
|
|
|
|
_add_model_aliases(cache, model_id, entry)
|
2026-02-21 22:31:43 -08:00
|
|
|
|
canonical = model.get("canonical_slug", "")
|
|
|
|
|
|
if canonical and canonical != model_id:
|
2026-03-18 03:04:07 -07:00
|
|
|
|
_add_model_aliases(cache, canonical, entry)
|
2026-02-21 22:31:43 -08:00
|
|
|
|
|
|
|
|
|
|
_model_metadata_cache = cache
|
|
|
|
|
|
_model_metadata_cache_time = time.time()
|
2026-06-14 04:45:46 -07:00
|
|
|
|
_save_model_metadata_disk_cache(cache)
|
2026-02-21 22:31:43 -08:00
|
|
|
|
logger.debug("Fetched metadata for %s models from OpenRouter", len(cache))
|
|
|
|
|
|
return cache
|
|
|
|
|
|
|
|
|
|
|
|
except Exception as e:
|
2026-05-21 14:09:30 +03:00
|
|
|
|
logger.warning(f"Failed to fetch model metadata from OpenRouter: {e}")
|
2026-06-14 04:45:46 -07:00
|
|
|
|
if _model_metadata_cache:
|
|
|
|
|
|
return _model_metadata_cache
|
|
|
|
|
|
disk_cache = _load_model_metadata_disk_cache()
|
|
|
|
|
|
if disk_cache:
|
|
|
|
|
|
_model_metadata_cache = disk_cache
|
|
|
|
|
|
disk_age = _model_metadata_disk_cache_age_seconds()
|
|
|
|
|
|
if disk_age is not None:
|
|
|
|
|
|
_model_metadata_cache_time = time.time() - min(disk_age, _MODEL_CACHE_TTL)
|
|
|
|
|
|
else:
|
|
|
|
|
|
_model_metadata_cache_time = time.time() - _MODEL_CACHE_TTL + 1
|
|
|
|
|
|
return _model_metadata_cache
|
|
|
|
|
|
return {}
|
2026-02-21 22:31:43 -08:00
|
|
|
|
|
|
|
|
|
|
|
2026-03-18 03:04:07 -07:00
|
|
|
|
def fetch_endpoint_model_metadata(
|
|
|
|
|
|
base_url: str,
|
|
|
|
|
|
api_key: str = "",
|
|
|
|
|
|
force_refresh: bool = False,
|
|
|
|
|
|
) -> Dict[str, Dict[str, Any]]:
|
|
|
|
|
|
"""Fetch model metadata from an OpenAI-compatible ``/models`` endpoint.
|
|
|
|
|
|
|
|
|
|
|
|
This is used for explicit custom endpoints where hardcoded global model-name
|
|
|
|
|
|
defaults are unreliable. Results are cached in memory per base URL.
|
|
|
|
|
|
"""
|
|
|
|
|
|
normalized = _normalize_base_url(base_url)
|
|
|
|
|
|
if not normalized or _is_openrouter_base_url(normalized):
|
|
|
|
|
|
return {}
|
|
|
|
|
|
|
|
|
|
|
|
if not force_refresh:
|
|
|
|
|
|
cached = _endpoint_model_metadata_cache.get(normalized)
|
|
|
|
|
|
cached_at = _endpoint_model_metadata_cache_time.get(normalized, 0)
|
|
|
|
|
|
if cached is not None and (time.time() - cached_at) < _ENDPOINT_MODEL_CACHE_TTL:
|
|
|
|
|
|
return cached
|
|
|
|
|
|
|
|
|
|
|
|
candidates = [normalized]
|
|
|
|
|
|
if normalized.endswith("/v1"):
|
|
|
|
|
|
alternate = normalized[:-3].rstrip("/")
|
|
|
|
|
|
else:
|
|
|
|
|
|
alternate = normalized + "/v1"
|
|
|
|
|
|
if alternate and alternate not in candidates:
|
|
|
|
|
|
candidates.append(alternate)
|
|
|
|
|
|
|
|
|
|
|
|
headers = {"Authorization": f"Bearer {api_key}"} if api_key else {}
|
|
|
|
|
|
last_error: Optional[Exception] = None
|
|
|
|
|
|
|
2026-04-20 20:49:44 -07:00
|
|
|
|
if is_local_endpoint(normalized):
|
|
|
|
|
|
try:
|
|
|
|
|
|
if detect_local_server_type(normalized, api_key=api_key) == "lm-studio":
|
2026-06-27 08:06:51 +10:00
|
|
|
|
server_url = _lmstudio_server_root(normalized)
|
2026-04-20 20:49:44 -07:00
|
|
|
|
response = requests.get(
|
|
|
|
|
|
server_url.rstrip("/") + "/api/v1/models",
|
|
|
|
|
|
headers=headers,
|
|
|
|
|
|
timeout=10,
|
2026-04-23 14:59:26 +03:00
|
|
|
|
verify=_resolve_requests_verify(),
|
2026-04-20 20:49:44 -07:00
|
|
|
|
)
|
|
|
|
|
|
response.raise_for_status()
|
|
|
|
|
|
payload = response.json()
|
|
|
|
|
|
cache: Dict[str, Dict[str, Any]] = {}
|
|
|
|
|
|
for model in payload.get("models", []):
|
|
|
|
|
|
if not isinstance(model, dict):
|
|
|
|
|
|
continue
|
|
|
|
|
|
model_id = model.get("key") or model.get("id")
|
|
|
|
|
|
if not model_id:
|
|
|
|
|
|
continue
|
|
|
|
|
|
entry: Dict[str, Any] = {"name": model.get("name", model_id)}
|
|
|
|
|
|
|
|
|
|
|
|
context_length = None
|
|
|
|
|
|
for inst in model.get("loaded_instances", []) or []:
|
|
|
|
|
|
if not isinstance(inst, dict):
|
|
|
|
|
|
continue
|
|
|
|
|
|
cfg = inst.get("config", {})
|
|
|
|
|
|
ctx = cfg.get("context_length") if isinstance(cfg, dict) else None
|
|
|
|
|
|
if isinstance(ctx, int) and ctx > 0:
|
|
|
|
|
|
context_length = ctx
|
|
|
|
|
|
break
|
|
|
|
|
|
if context_length is not None:
|
|
|
|
|
|
entry["context_length"] = context_length
|
|
|
|
|
|
|
|
|
|
|
|
max_completion_tokens = _extract_max_completion_tokens(model)
|
|
|
|
|
|
if max_completion_tokens is not None:
|
|
|
|
|
|
entry["max_completion_tokens"] = max_completion_tokens
|
|
|
|
|
|
|
|
|
|
|
|
pricing = _extract_pricing(model)
|
|
|
|
|
|
if pricing:
|
|
|
|
|
|
entry["pricing"] = pricing
|
|
|
|
|
|
|
|
|
|
|
|
_add_model_aliases(cache, model_id, entry)
|
|
|
|
|
|
alt_id = model.get("id")
|
|
|
|
|
|
if isinstance(alt_id, str) and alt_id and alt_id != model_id:
|
|
|
|
|
|
_add_model_aliases(cache, alt_id, entry)
|
|
|
|
|
|
|
|
|
|
|
|
_endpoint_model_metadata_cache[normalized] = cache
|
|
|
|
|
|
_endpoint_model_metadata_cache_time[normalized] = time.time()
|
|
|
|
|
|
return cache
|
|
|
|
|
|
except Exception as exc:
|
|
|
|
|
|
last_error = exc
|
|
|
|
|
|
|
2026-03-18 03:04:07 -07:00
|
|
|
|
for candidate in candidates:
|
|
|
|
|
|
url = candidate.rstrip("/") + "/models"
|
|
|
|
|
|
try:
|
2026-04-23 14:59:26 +03:00
|
|
|
|
response = requests.get(url, headers=headers, timeout=10, verify=_resolve_requests_verify())
|
2026-03-18 03:04:07 -07:00
|
|
|
|
response.raise_for_status()
|
|
|
|
|
|
payload = response.json()
|
|
|
|
|
|
cache: Dict[str, Dict[str, Any]] = {}
|
|
|
|
|
|
for model in payload.get("data", []):
|
|
|
|
|
|
if not isinstance(model, dict):
|
|
|
|
|
|
continue
|
|
|
|
|
|
model_id = model.get("id")
|
|
|
|
|
|
if not model_id:
|
|
|
|
|
|
continue
|
|
|
|
|
|
entry: Dict[str, Any] = {"name": model.get("name", model_id)}
|
|
|
|
|
|
context_length = _extract_context_length(model)
|
|
|
|
|
|
if context_length is not None:
|
|
|
|
|
|
entry["context_length"] = context_length
|
|
|
|
|
|
max_completion_tokens = _extract_max_completion_tokens(model)
|
|
|
|
|
|
if max_completion_tokens is not None:
|
|
|
|
|
|
entry["max_completion_tokens"] = max_completion_tokens
|
|
|
|
|
|
pricing = _extract_pricing(model)
|
|
|
|
|
|
if pricing:
|
|
|
|
|
|
entry["pricing"] = pricing
|
|
|
|
|
|
_add_model_aliases(cache, model_id, entry)
|
|
|
|
|
|
|
2026-03-19 06:01:16 -07:00
|
|
|
|
# If this is a llama.cpp server, query /props for actual allocated context
|
|
|
|
|
|
is_llamacpp = any(
|
|
|
|
|
|
m.get("owned_by") == "llamacpp"
|
|
|
|
|
|
for m in payload.get("data", []) if isinstance(m, dict)
|
|
|
|
|
|
)
|
|
|
|
|
|
if is_llamacpp:
|
|
|
|
|
|
try:
|
2026-03-21 18:07:18 -07:00
|
|
|
|
# Try /v1/props first (current llama.cpp); fall back to /props for older builds
|
|
|
|
|
|
base = candidate.rstrip("/").replace("/v1", "")
|
2026-04-23 14:59:26 +03:00
|
|
|
|
_verify = _resolve_requests_verify()
|
|
|
|
|
|
props_resp = requests.get(base + "/v1/props", headers=headers, timeout=5, verify=_verify)
|
2026-03-21 18:07:18 -07:00
|
|
|
|
if not props_resp.ok:
|
2026-04-23 14:59:26 +03:00
|
|
|
|
props_resp = requests.get(base + "/props", headers=headers, timeout=5, verify=_verify)
|
2026-03-19 06:01:16 -07:00
|
|
|
|
if props_resp.ok:
|
|
|
|
|
|
props = props_resp.json()
|
|
|
|
|
|
gen_settings = props.get("default_generation_settings", {})
|
|
|
|
|
|
n_ctx = gen_settings.get("n_ctx")
|
|
|
|
|
|
model_alias = props.get("model_alias", "")
|
|
|
|
|
|
if n_ctx and model_alias and model_alias in cache:
|
|
|
|
|
|
cache[model_alias]["context_length"] = n_ctx
|
|
|
|
|
|
except Exception:
|
|
|
|
|
|
pass
|
|
|
|
|
|
|
2026-03-18 03:04:07 -07:00
|
|
|
|
_endpoint_model_metadata_cache[normalized] = cache
|
|
|
|
|
|
_endpoint_model_metadata_cache_time[normalized] = time.time()
|
|
|
|
|
|
return cache
|
|
|
|
|
|
except Exception as exc:
|
|
|
|
|
|
last_error = exc
|
|
|
|
|
|
|
|
|
|
|
|
if last_error:
|
|
|
|
|
|
logger.debug("Failed to fetch model metadata from %s/models: %s", normalized, last_error)
|
|
|
|
|
|
_endpoint_model_metadata_cache[normalized] = {}
|
|
|
|
|
|
_endpoint_model_metadata_cache_time[normalized] = time.time()
|
|
|
|
|
|
return {}
|
|
|
|
|
|
|
|
|
|
|
|
|
feat(providers): add GMI Cloud as a first-class API-key provider (#11955)
Add GMI Cloud (api.gmi-serving.com) as a full first-class API-key provider
with built-in auth, aliases, model catalog, CLI entry points, auxiliary client
routing, context length resolution, doctor checks, env var tracking, and docs.
- auth.py: ProviderConfig for 'gmi' (api_key, GMI_API_KEY / GMI_BASE_URL)
- providers.py: HermesOverlay with extra_env_vars for models.dev detection
- models.py: curated slash-form model catalog; live /v1/models fetch
- main.py: 'gmi' in _named_custom_provider_map and --provider choices
- model_metadata.py: _URL_TO_PROVIDER, _PROVIDER_PREFIXES, dedicated
context-length probe block (GMI's /models has authoritative data)
- auxiliary_client.py: alias entries; _compat_model fix for slash-form
models on cached aggregator-style clients; gmi aux default model
- doctor.py: GMI in provider connectivity checks
- config.py: GMI_API_KEY / GMI_BASE_URL in OPTIONAL_ENV_VARS
- conftest.py: explicit GMI_BASE_URL clearing (not caught by _API_KEY suffix)
- docs: providers.md, environment-variables.md, fallback-providers.md,
configuration.md, quickstart.md (expands provider table)
Co-authored-by: Isaac Huang <isaachuang@Isaacs-MacBook-Pro.local>
2026-04-17 11:19:56 -07:00
|
|
|
|
def _resolve_endpoint_context_length(
|
|
|
|
|
|
model: str,
|
|
|
|
|
|
base_url: str,
|
|
|
|
|
|
api_key: str = "",
|
|
|
|
|
|
) -> Optional[int]:
|
|
|
|
|
|
"""Resolve context length from an endpoint's live ``/models`` metadata."""
|
|
|
|
|
|
endpoint_metadata = fetch_endpoint_model_metadata(base_url, api_key=api_key)
|
|
|
|
|
|
matched = endpoint_metadata.get(model)
|
|
|
|
|
|
if not matched:
|
|
|
|
|
|
if len(endpoint_metadata) == 1:
|
|
|
|
|
|
matched = next(iter(endpoint_metadata.values()))
|
|
|
|
|
|
else:
|
|
|
|
|
|
for key, entry in endpoint_metadata.items():
|
|
|
|
|
|
if model in key or key in model:
|
|
|
|
|
|
matched = entry
|
|
|
|
|
|
break
|
|
|
|
|
|
if matched:
|
|
|
|
|
|
context_length = matched.get("context_length")
|
|
|
|
|
|
if isinstance(context_length, int):
|
|
|
|
|
|
return context_length
|
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-03-05 16:09:57 -08:00
|
|
|
|
def _get_context_cache_path() -> Path:
|
|
|
|
|
|
"""Return path to the persistent context length cache file."""
|
refactor: replace inline HERMES_HOME re-implementations with get_hermes_home()
16 callsites across 14 files were re-deriving the hermes home path
via os.environ.get('HERMES_HOME', ...) instead of using the canonical
get_hermes_home() from hermes_constants. This breaks profiles — each
profile has its own HERMES_HOME, and the inline fallback defaults to
~/.hermes regardless.
Fixed by importing and calling get_hermes_home() at each site. For
files already inside the hermes process (agent/, hermes_cli/, tools/,
gateway/, plugins/), this is always safe. Files that run outside the
process context (mcp_serve.py, mcp_oauth.py) already had correct
try/except ImportError fallbacks and were left alone.
Skipped: hermes_constants.py (IS the implementation), env_loader.py
(bootstrap), profiles.py (intentionally manipulates the env var),
standalone scripts (optional-skills/, skills/), and tests.
2026-04-07 10:40:34 -07:00
|
|
|
|
from hermes_constants import get_hermes_home
|
|
|
|
|
|
return get_hermes_home() / "context_length_cache.yaml"
|
2026-03-05 16:09:57 -08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _load_context_cache() -> Dict[str, int]:
|
2026-03-18 21:38:41 +01:00
|
|
|
|
"""Load the model+provider -> context_length cache from disk."""
|
2026-03-05 16:09:57 -08:00
|
|
|
|
path = _get_context_cache_path()
|
|
|
|
|
|
if not path.exists():
|
|
|
|
|
|
return {}
|
|
|
|
|
|
try:
|
codebase: add encoding='utf-8' to all bare open() calls (PLW1514)
Closes the last Python-on-Windows UTF-8 exposure by making every
text-mode open() call explicit about its encoding.
Before: on Windows, bare open(path, 'r') defaults to the system
locale encoding (cp1252 on US-locale installs). That means reading
any config/yaml/markdown/json file with non-ASCII content either
crashes with UnicodeDecodeError or silently mis-decodes bytes.
After: all 89 affected call sites in production code now pass
encoding='utf-8' explicitly. Works identically on every platform
and every locale, no surprise behavior.
Mechanical sweep via:
ruff check --preview --extend-select PLW1514 --unsafe-fixes --fix --exclude 'tests,venv,.venv,node_modules,website,optional-skills, skills,tinker-atropos,plugins' .
All 89 fixes have the same shape: open(x) or open(x, mode) became
open(x, encoding='utf-8') or open(x, mode, encoding='utf-8'). Nothing
else changed. Every modified file still parses and the Windows/sandbox
test suite is still green (85 passed, 14 skipped, 0 failed across
tests/tools/test_code_execution_windows_env.py +
tests/tools/test_code_execution_modes.py + tests/tools/test_env_passthrough.py +
tests/test_hermes_bootstrap.py).
Scope notes:
- tests/ excluded: test fixtures can use locale encoding intentionally
(exercising edge cases). If we want to tighten tests later that's
a separate PR.
- plugins/ excluded: plugin-specific conventions may differ; plugin
authors own their code.
- optional-skills/ and skills/ excluded: skill scripts are user-authored
and we don't want to mass-edit them.
- website/ and tinker-atropos/ excluded: vendored / generated content.
46 files touched, 89 +/- lines (symmetric replacement). No behavior
change on POSIX or on Windows when the file is ASCII; bug fix on
Windows when the file contains non-ASCII.
2026-05-07 19:24:45 -07:00
|
|
|
|
with open(path, encoding="utf-8") as f:
|
2026-03-05 16:09:57 -08:00
|
|
|
|
data = yaml.safe_load(f) or {}
|
|
|
|
|
|
return data.get("context_lengths", {})
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
logger.debug("Failed to load context length cache: %s", e)
|
|
|
|
|
|
return {}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def save_context_length(model: str, base_url: str, length: int) -> None:
|
|
|
|
|
|
"""Persist a discovered context length for a model+provider combo.
|
|
|
|
|
|
|
|
|
|
|
|
Cache key is ``model@base_url`` so the same model name served from
|
|
|
|
|
|
different providers can have different limits.
|
|
|
|
|
|
"""
|
|
|
|
|
|
key = f"{model}@{base_url}"
|
|
|
|
|
|
cache = _load_context_cache()
|
|
|
|
|
|
if cache.get(key) == length:
|
|
|
|
|
|
return # already stored
|
|
|
|
|
|
cache[key] = length
|
|
|
|
|
|
path = _get_context_cache_path()
|
|
|
|
|
|
try:
|
|
|
|
|
|
path.parent.mkdir(parents=True, exist_ok=True)
|
codebase: add encoding='utf-8' to all bare open() calls (PLW1514)
Closes the last Python-on-Windows UTF-8 exposure by making every
text-mode open() call explicit about its encoding.
Before: on Windows, bare open(path, 'r') defaults to the system
locale encoding (cp1252 on US-locale installs). That means reading
any config/yaml/markdown/json file with non-ASCII content either
crashes with UnicodeDecodeError or silently mis-decodes bytes.
After: all 89 affected call sites in production code now pass
encoding='utf-8' explicitly. Works identically on every platform
and every locale, no surprise behavior.
Mechanical sweep via:
ruff check --preview --extend-select PLW1514 --unsafe-fixes --fix --exclude 'tests,venv,.venv,node_modules,website,optional-skills, skills,tinker-atropos,plugins' .
All 89 fixes have the same shape: open(x) or open(x, mode) became
open(x, encoding='utf-8') or open(x, mode, encoding='utf-8'). Nothing
else changed. Every modified file still parses and the Windows/sandbox
test suite is still green (85 passed, 14 skipped, 0 failed across
tests/tools/test_code_execution_windows_env.py +
tests/tools/test_code_execution_modes.py + tests/tools/test_env_passthrough.py +
tests/test_hermes_bootstrap.py).
Scope notes:
- tests/ excluded: test fixtures can use locale encoding intentionally
(exercising edge cases). If we want to tighten tests later that's
a separate PR.
- plugins/ excluded: plugin-specific conventions may differ; plugin
authors own their code.
- optional-skills/ and skills/ excluded: skill scripts are user-authored
and we don't want to mass-edit them.
- website/ and tinker-atropos/ excluded: vendored / generated content.
46 files touched, 89 +/- lines (symmetric replacement). No behavior
change on POSIX or on Windows when the file is ASCII; bug fix on
Windows when the file contains non-ASCII.
2026-05-07 19:24:45 -07:00
|
|
|
|
with open(path, "w", encoding="utf-8") as f:
|
2026-03-05 16:09:57 -08:00
|
|
|
|
yaml.dump({"context_lengths": cache}, f, default_flow_style=False)
|
2026-03-18 21:38:41 +01:00
|
|
|
|
logger.info("Cached context length %s -> %s tokens", key, f"{length:,}")
|
2026-03-05 16:09:57 -08:00
|
|
|
|
except Exception as e:
|
|
|
|
|
|
logger.debug("Failed to save context length cache: %s", e)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def get_cached_context_length(model: str, base_url: str) -> Optional[int]:
|
|
|
|
|
|
"""Look up a previously discovered context length for model+provider."""
|
|
|
|
|
|
key = f"{model}@{base_url}"
|
|
|
|
|
|
cache = _load_context_cache()
|
|
|
|
|
|
return cache.get(key)
|
|
|
|
|
|
|
|
|
|
|
|
|
fix(context): invalidate stale Codex OAuth cache entries >= 400k (#15078)
PR #14935 added a Codex-aware context resolver but only new lookups
hit the live /models probe. Users who had run Hermes on gpt-5.5 / 5.4
BEFORE that PR already had the wrong value (e.g. 1,050,000 from
models.dev) persisted in ~/.hermes/context_length_cache.yaml, and the
cache-first lookup in get_model_context_length() returns it forever.
Symptom (reported in the wild by Ludwig, min heo, Gaoge on current
main at 6051fba9d, which is AFTER #14935):
* Startup banner shows context usage against 1M
* Compression fires late and then OpenAI hard-rejects with
'context length will be reduced from 1,050,000 to 128,000'
around the real 272k boundary.
Fix: when the step-1 cache returns a value for an openai-codex lookup,
check whether it's >= 400k. Codex OAuth caps every slug at 272k (live
probe values) so anything at or above 400k is definitionally a
pre-#14935 leftover. Drop that entry from the on-disk cache and fall
through to step 5, which runs the live /models probe and repersists
the correct value (or 272k from the hardcoded fallback if the probe
fails). Non-Codex providers and legitimately-cached Codex entries at
272k are untouched.
Changes:
- agent/model_metadata.py:
* _invalidate_cached_context_length() — drop a single entry from
context_length_cache.yaml and rewrite the file.
* Step-1 cache check in get_model_context_length() now gates
provider=='openai-codex' entries >= 400k through invalidation
instead of returning them.
Tests (3 new in TestCodexOAuthContextLength):
- stale 1.05M Codex entry is dropped from disk AND re-resolved
through the live probe to 272k; unrelated cache entries survive.
- fresh 272k Codex entry is respected (no probe call, no invalidation).
- non-Codex 1M entries (e.g. anthropic/claude-opus-4.6 on OpenRouter)
are unaffected — the guard is strictly scoped to openai-codex.
Full tests/agent/test_model_metadata.py: 88 passed.
2026-04-24 04:46:07 -07:00
|
|
|
|
def _invalidate_cached_context_length(model: str, base_url: str) -> None:
|
|
|
|
|
|
"""Drop a stale cache entry so it gets re-resolved on the next lookup."""
|
|
|
|
|
|
key = f"{model}@{base_url}"
|
|
|
|
|
|
cache = _load_context_cache()
|
|
|
|
|
|
if key not in cache:
|
|
|
|
|
|
return
|
|
|
|
|
|
del cache[key]
|
|
|
|
|
|
path = _get_context_cache_path()
|
|
|
|
|
|
try:
|
|
|
|
|
|
path.parent.mkdir(parents=True, exist_ok=True)
|
codebase: add encoding='utf-8' to all bare open() calls (PLW1514)
Closes the last Python-on-Windows UTF-8 exposure by making every
text-mode open() call explicit about its encoding.
Before: on Windows, bare open(path, 'r') defaults to the system
locale encoding (cp1252 on US-locale installs). That means reading
any config/yaml/markdown/json file with non-ASCII content either
crashes with UnicodeDecodeError or silently mis-decodes bytes.
After: all 89 affected call sites in production code now pass
encoding='utf-8' explicitly. Works identically on every platform
and every locale, no surprise behavior.
Mechanical sweep via:
ruff check --preview --extend-select PLW1514 --unsafe-fixes --fix --exclude 'tests,venv,.venv,node_modules,website,optional-skills, skills,tinker-atropos,plugins' .
All 89 fixes have the same shape: open(x) or open(x, mode) became
open(x, encoding='utf-8') or open(x, mode, encoding='utf-8'). Nothing
else changed. Every modified file still parses and the Windows/sandbox
test suite is still green (85 passed, 14 skipped, 0 failed across
tests/tools/test_code_execution_windows_env.py +
tests/tools/test_code_execution_modes.py + tests/tools/test_env_passthrough.py +
tests/test_hermes_bootstrap.py).
Scope notes:
- tests/ excluded: test fixtures can use locale encoding intentionally
(exercising edge cases). If we want to tighten tests later that's
a separate PR.
- plugins/ excluded: plugin-specific conventions may differ; plugin
authors own their code.
- optional-skills/ and skills/ excluded: skill scripts are user-authored
and we don't want to mass-edit them.
- website/ and tinker-atropos/ excluded: vendored / generated content.
46 files touched, 89 +/- lines (symmetric replacement). No behavior
change on POSIX or on Windows when the file is ASCII; bug fix on
Windows when the file contains non-ASCII.
2026-05-07 19:24:45 -07:00
|
|
|
|
with open(path, "w", encoding="utf-8") as f:
|
fix(context): invalidate stale Codex OAuth cache entries >= 400k (#15078)
PR #14935 added a Codex-aware context resolver but only new lookups
hit the live /models probe. Users who had run Hermes on gpt-5.5 / 5.4
BEFORE that PR already had the wrong value (e.g. 1,050,000 from
models.dev) persisted in ~/.hermes/context_length_cache.yaml, and the
cache-first lookup in get_model_context_length() returns it forever.
Symptom (reported in the wild by Ludwig, min heo, Gaoge on current
main at 6051fba9d, which is AFTER #14935):
* Startup banner shows context usage against 1M
* Compression fires late and then OpenAI hard-rejects with
'context length will be reduced from 1,050,000 to 128,000'
around the real 272k boundary.
Fix: when the step-1 cache returns a value for an openai-codex lookup,
check whether it's >= 400k. Codex OAuth caps every slug at 272k (live
probe values) so anything at or above 400k is definitionally a
pre-#14935 leftover. Drop that entry from the on-disk cache and fall
through to step 5, which runs the live /models probe and repersists
the correct value (or 272k from the hardcoded fallback if the probe
fails). Non-Codex providers and legitimately-cached Codex entries at
272k are untouched.
Changes:
- agent/model_metadata.py:
* _invalidate_cached_context_length() — drop a single entry from
context_length_cache.yaml and rewrite the file.
* Step-1 cache check in get_model_context_length() now gates
provider=='openai-codex' entries >= 400k through invalidation
instead of returning them.
Tests (3 new in TestCodexOAuthContextLength):
- stale 1.05M Codex entry is dropped from disk AND re-resolved
through the live probe to 272k; unrelated cache entries survive.
- fresh 272k Codex entry is respected (no probe call, no invalidation).
- non-Codex 1M entries (e.g. anthropic/claude-opus-4.6 on OpenRouter)
are unaffected — the guard is strictly scoped to openai-codex.
Full tests/agent/test_model_metadata.py: 88 passed.
2026-04-24 04:46:07 -07:00
|
|
|
|
yaml.dump({"context_lengths": cache}, f, default_flow_style=False)
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
logger.debug("Failed to invalidate context length cache entry %s: %s", key, e)
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-03-05 16:09:57 -08:00
|
|
|
|
def get_next_probe_tier(current_length: int) -> Optional[int]:
|
|
|
|
|
|
"""Return the next lower probe tier, or None if already at minimum."""
|
|
|
|
|
|
for tier in CONTEXT_PROBE_TIERS:
|
|
|
|
|
|
if tier < current_length:
|
|
|
|
|
|
return tier
|
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def parse_context_limit_from_error(error_msg: str) -> Optional[int]:
|
|
|
|
|
|
"""Try to extract the actual context limit from an API error message.
|
|
|
|
|
|
|
|
|
|
|
|
Many providers include the limit in their error text, e.g.:
|
|
|
|
|
|
- "maximum context length is 32768 tokens"
|
|
|
|
|
|
- "context_length_exceeded: 131072"
|
|
|
|
|
|
- "Maximum context size 32768 exceeded"
|
|
|
|
|
|
- "model's max context length is 65536"
|
|
|
|
|
|
"""
|
|
|
|
|
|
error_lower = error_msg.lower()
|
|
|
|
|
|
# Pattern: look for numbers near context-related keywords
|
|
|
|
|
|
patterns = [
|
|
|
|
|
|
r'(?:max(?:imum)?|limit)\s*(?:context\s*)?(?:length|size|window)?\s*(?:is|of|:)?\s*(\d{4,})',
|
|
|
|
|
|
r'context\s*(?:length|size|window)\s*(?:is|of|:)?\s*(\d{4,})',
|
|
|
|
|
|
r'(\d{4,})\s*(?:token)?\s*(?:context|limit)',
|
test: comprehensive tests for model metadata + firecrawl config
model_metadata tests (61 tests, was 39):
- Token estimation: concrete value assertions, unicode, tool_call messages,
vision multimodal content, additive verification
- Context length resolution: cache-over-API priority, no-base_url skips cache,
missing context_length key in API response
- API metadata fetch: canonical_slug aliasing, TTL expiry with time mock,
stale cache fallback on API failure, malformed JSON resilience
- Probe tiers: above-max returns 2M, zero returns None
- Error parsing: Anthropic format ('X > Y maximum'), LM Studio, empty string,
unreasonably large numbers — also fixed parser to handle Anthropic format
- Cache: corruption resilience (garbage YAML, wrong structure), value updates,
special chars in model names
Firecrawl config tests (8 tests, was 4):
- Singleton caching (core purpose — verified constructor called once)
- Constructor failure recovery (retry after exception)
- Return value actually asserted (not just constructor args)
- Empty string env vars treated as absent
- Proper setup/teardown for env var isolation
2026-03-05 18:22:39 -08:00
|
|
|
|
r'>\s*(\d{4,})\s*(?:max|limit|token)', # "250000 tokens > 200000 maximum"
|
|
|
|
|
|
r'(\d{4,})\s*(?:max(?:imum)?)\b', # "200000 maximum"
|
2026-03-05 16:09:57 -08:00
|
|
|
|
]
|
|
|
|
|
|
for pattern in patterns:
|
|
|
|
|
|
match = re.search(pattern, error_lower)
|
|
|
|
|
|
if match:
|
|
|
|
|
|
limit = int(match.group(1))
|
|
|
|
|
|
# Sanity check: must be a reasonable context length
|
|
|
|
|
|
if 1024 <= limit <= 10_000_000:
|
|
|
|
|
|
return limit
|
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-05-28 12:57:50 +08:00
|
|
|
|
def get_context_length_from_provider_error(
|
|
|
|
|
|
error_msg: str,
|
|
|
|
|
|
current_context_length: int,
|
|
|
|
|
|
) -> Optional[int]:
|
|
|
|
|
|
"""Return a provider-reported lower context limit, if one is present.
|
|
|
|
|
|
|
|
|
|
|
|
Context-overflow recovery must not invent a new model window size. Some
|
|
|
|
|
|
providers only say that the input exceeds the context window without
|
|
|
|
|
|
reporting the actual maximum. In that case callers should keep the
|
|
|
|
|
|
configured context length and try compression only, rather than stepping
|
|
|
|
|
|
down through guessed probe tiers (1M → 256K → 128K → ...).
|
|
|
|
|
|
"""
|
|
|
|
|
|
parsed_limit = parse_context_limit_from_error(error_msg)
|
|
|
|
|
|
if parsed_limit is None:
|
|
|
|
|
|
return None
|
|
|
|
|
|
if parsed_limit < current_context_length:
|
|
|
|
|
|
return parsed_limit
|
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
|
|
|
fix(compaction): don't halve context_length on output-cap-too-large errors
When the API returns "max_tokens too large given prompt" (input tokens
are within the context window, but input + requested output > window),
the old code incorrectly routed through the same handler as "prompt too
long" errors, calling get_next_probe_tier() and permanently halving
context_length. This made things worse: the window was fine, only the
requested output size needed trimming for that one call.
Two distinct error classes now handled separately:
Prompt too long — input itself exceeds context window.
Fix: compress history + halve context_length (existing behaviour,
unchanged).
Output cap too large — input OK, but input + max_tokens > window.
Fix: parse available_tokens from the error message, set a one-shot
_ephemeral_max_output_tokens override for the retry, and leave
context_length completely untouched.
Changes:
- agent/model_metadata.py: add parse_available_output_tokens_from_error()
that detects Anthropic's "available_tokens: N" error format and returns
the available output budget, or None for all other error types.
- run_agent.py: call the new parser first in the is_context_length_error
block; if it fires, set _ephemeral_max_output_tokens (with a 64-token
safety margin) and break to retry without touching context_length.
_build_api_kwargs consumes the ephemeral value exactly once then clears
it so subsequent calls use self.max_tokens normally.
- agent/anthropic_adapter.py: expand build_anthropic_kwargs docstring to
clearly document the max_tokens (output cap) vs context_length (total
window) distinction, which is a persistent source of confusion due to
the OpenAI-inherited "max_tokens" name.
- cli-config.yaml.example: add inline comments explaining both keys side
by side where users are most likely to look.
- website/docs/integrations/providers.md: add a callout box at the top
of "Context Length Detection" and clarify the troubleshooting entry.
- tests/test_ctx_halving_fix.py: 24 tests across four classes covering
the parser, build_anthropic_kwargs clamping, ephemeral one-shot
consumption, and the invariant that context_length is never mutated
on output-cap errors.
2026-04-09 16:54:23 +02:00
|
|
|
|
def parse_available_output_tokens_from_error(error_msg: str) -> Optional[int]:
|
|
|
|
|
|
"""Detect an "output cap too large" error and return how many output tokens are available.
|
|
|
|
|
|
|
|
|
|
|
|
Background — two distinct context errors exist:
|
|
|
|
|
|
1. "Prompt too long" — the INPUT itself exceeds the context window.
|
2026-05-28 12:57:50 +08:00
|
|
|
|
Fix: compress history, and only reduce context_length if the
|
|
|
|
|
|
provider explicitly reports the actual lower limit.
|
fix(compaction): don't halve context_length on output-cap-too-large errors
When the API returns "max_tokens too large given prompt" (input tokens
are within the context window, but input + requested output > window),
the old code incorrectly routed through the same handler as "prompt too
long" errors, calling get_next_probe_tier() and permanently halving
context_length. This made things worse: the window was fine, only the
requested output size needed trimming for that one call.
Two distinct error classes now handled separately:
Prompt too long — input itself exceeds context window.
Fix: compress history + halve context_length (existing behaviour,
unchanged).
Output cap too large — input OK, but input + max_tokens > window.
Fix: parse available_tokens from the error message, set a one-shot
_ephemeral_max_output_tokens override for the retry, and leave
context_length completely untouched.
Changes:
- agent/model_metadata.py: add parse_available_output_tokens_from_error()
that detects Anthropic's "available_tokens: N" error format and returns
the available output budget, or None for all other error types.
- run_agent.py: call the new parser first in the is_context_length_error
block; if it fires, set _ephemeral_max_output_tokens (with a 64-token
safety margin) and break to retry without touching context_length.
_build_api_kwargs consumes the ephemeral value exactly once then clears
it so subsequent calls use self.max_tokens normally.
- agent/anthropic_adapter.py: expand build_anthropic_kwargs docstring to
clearly document the max_tokens (output cap) vs context_length (total
window) distinction, which is a persistent source of confusion due to
the OpenAI-inherited "max_tokens" name.
- cli-config.yaml.example: add inline comments explaining both keys side
by side where users are most likely to look.
- website/docs/integrations/providers.md: add a callout box at the top
of "Context Length Detection" and clarify the troubleshooting entry.
- tests/test_ctx_halving_fix.py: 24 tests across four classes covering
the parser, build_anthropic_kwargs clamping, ephemeral one-shot
consumption, and the invariant that context_length is never mutated
on output-cap errors.
2026-04-09 16:54:23 +02:00
|
|
|
|
2. "max_tokens too large" — input is fine, but input + requested_output > window.
|
|
|
|
|
|
Fix: reduce max_tokens (the output cap) for this call.
|
|
|
|
|
|
Do NOT touch context_length — the window hasn't shrunk.
|
|
|
|
|
|
|
|
|
|
|
|
Anthropic's API returns errors like:
|
|
|
|
|
|
"max_tokens: 32768 > context_window: 200000 - input_tokens: 190000 = available_tokens: 10000"
|
|
|
|
|
|
|
|
|
|
|
|
Returns the number of output tokens that would fit (e.g. 10000 above), or None if
|
|
|
|
|
|
the error does not look like a max_tokens-too-large error.
|
|
|
|
|
|
"""
|
|
|
|
|
|
error_lower = error_msg.lower()
|
|
|
|
|
|
|
|
|
|
|
|
# Must look like an output-cap error, not a prompt-length error.
|
|
|
|
|
|
is_output_cap_error = (
|
|
|
|
|
|
"max_tokens" in error_lower
|
|
|
|
|
|
and ("available_tokens" in error_lower or "available tokens" in error_lower)
|
2026-06-07 03:52:09 -07:00
|
|
|
|
) or (
|
|
|
|
|
|
# OpenRouter/Nous phrasing of the same condition.
|
|
|
|
|
|
"in the output" in error_lower
|
|
|
|
|
|
and "maximum context length" in error_lower
|
2026-06-09 16:51:07 +07:00
|
|
|
|
) or (
|
|
|
|
|
|
# LM Studio / llama.cpp / some OpenAI-compatible servers:
|
|
|
|
|
|
# "This model's maximum context length is 65536 tokens. However, you
|
|
|
|
|
|
# requested 65536 output tokens and your prompt contains 77409
|
|
|
|
|
|
# characters ..."
|
|
|
|
|
|
# The "requested N output tokens" phrasing means the OUTPUT cap is the
|
|
|
|
|
|
# problem (the input itself fits) — reduce max_tokens, don't compress.
|
|
|
|
|
|
"maximum context length" in error_lower
|
|
|
|
|
|
and "requested" in error_lower
|
|
|
|
|
|
and "output tokens" in error_lower
|
2026-06-30 03:26:41 -07:00
|
|
|
|
) or (
|
|
|
|
|
|
# DashScope / Alibaba Cloud (Qwen) phrasing. The provider rejects an
|
|
|
|
|
|
# over-cap output request with a bounded range whose upper bound IS the
|
|
|
|
|
|
# real max-output cap, e.g.
|
|
|
|
|
|
# "Range of max_tokens should be [1, 65536]"
|
|
|
|
|
|
# The input itself fits — this is purely an output-cap error, so reduce
|
|
|
|
|
|
# max_tokens and retry; do NOT compress.
|
|
|
|
|
|
"range of max_tokens should be" in error_lower
|
fix(compaction): don't halve context_length on output-cap-too-large errors
When the API returns "max_tokens too large given prompt" (input tokens
are within the context window, but input + requested output > window),
the old code incorrectly routed through the same handler as "prompt too
long" errors, calling get_next_probe_tier() and permanently halving
context_length. This made things worse: the window was fine, only the
requested output size needed trimming for that one call.
Two distinct error classes now handled separately:
Prompt too long — input itself exceeds context window.
Fix: compress history + halve context_length (existing behaviour,
unchanged).
Output cap too large — input OK, but input + max_tokens > window.
Fix: parse available_tokens from the error message, set a one-shot
_ephemeral_max_output_tokens override for the retry, and leave
context_length completely untouched.
Changes:
- agent/model_metadata.py: add parse_available_output_tokens_from_error()
that detects Anthropic's "available_tokens: N" error format and returns
the available output budget, or None for all other error types.
- run_agent.py: call the new parser first in the is_context_length_error
block; if it fires, set _ephemeral_max_output_tokens (with a 64-token
safety margin) and break to retry without touching context_length.
_build_api_kwargs consumes the ephemeral value exactly once then clears
it so subsequent calls use self.max_tokens normally.
- agent/anthropic_adapter.py: expand build_anthropic_kwargs docstring to
clearly document the max_tokens (output cap) vs context_length (total
window) distinction, which is a persistent source of confusion due to
the OpenAI-inherited "max_tokens" name.
- cli-config.yaml.example: add inline comments explaining both keys side
by side where users are most likely to look.
- website/docs/integrations/providers.md: add a callout box at the top
of "Context Length Detection" and clarify the troubleshooting entry.
- tests/test_ctx_halving_fix.py: 24 tests across four classes covering
the parser, build_anthropic_kwargs clamping, ephemeral one-shot
consumption, and the invariant that context_length is never mutated
on output-cap errors.
2026-04-09 16:54:23 +02:00
|
|
|
|
)
|
|
|
|
|
|
if not is_output_cap_error:
|
|
|
|
|
|
return None
|
|
|
|
|
|
|
2026-06-30 03:26:41 -07:00
|
|
|
|
# DashScope / Alibaba range form: "Range of max_tokens should be [1, 65536]".
|
|
|
|
|
|
# The upper bound is the available output cap.
|
|
|
|
|
|
_m_range = re.search(
|
|
|
|
|
|
r'range of max_tokens should be\s*\[\s*\d+\s*,\s*(\d+)\s*\]',
|
|
|
|
|
|
error_lower,
|
|
|
|
|
|
)
|
|
|
|
|
|
if _m_range:
|
|
|
|
|
|
_cap = int(_m_range.group(1))
|
|
|
|
|
|
if _cap >= 1:
|
|
|
|
|
|
return _cap
|
|
|
|
|
|
|
fix(compaction): don't halve context_length on output-cap-too-large errors
When the API returns "max_tokens too large given prompt" (input tokens
are within the context window, but input + requested output > window),
the old code incorrectly routed through the same handler as "prompt too
long" errors, calling get_next_probe_tier() and permanently halving
context_length. This made things worse: the window was fine, only the
requested output size needed trimming for that one call.
Two distinct error classes now handled separately:
Prompt too long — input itself exceeds context window.
Fix: compress history + halve context_length (existing behaviour,
unchanged).
Output cap too large — input OK, but input + max_tokens > window.
Fix: parse available_tokens from the error message, set a one-shot
_ephemeral_max_output_tokens override for the retry, and leave
context_length completely untouched.
Changes:
- agent/model_metadata.py: add parse_available_output_tokens_from_error()
that detects Anthropic's "available_tokens: N" error format and returns
the available output budget, or None for all other error types.
- run_agent.py: call the new parser first in the is_context_length_error
block; if it fires, set _ephemeral_max_output_tokens (with a 64-token
safety margin) and break to retry without touching context_length.
_build_api_kwargs consumes the ephemeral value exactly once then clears
it so subsequent calls use self.max_tokens normally.
- agent/anthropic_adapter.py: expand build_anthropic_kwargs docstring to
clearly document the max_tokens (output cap) vs context_length (total
window) distinction, which is a persistent source of confusion due to
the OpenAI-inherited "max_tokens" name.
- cli-config.yaml.example: add inline comments explaining both keys side
by side where users are most likely to look.
- website/docs/integrations/providers.md: add a callout box at the top
of "Context Length Detection" and clarify the troubleshooting entry.
- tests/test_ctx_halving_fix.py: 24 tests across four classes covering
the parser, build_anthropic_kwargs clamping, ephemeral one-shot
consumption, and the invariant that context_length is never mutated
on output-cap errors.
2026-04-09 16:54:23 +02:00
|
|
|
|
# Extract the available_tokens figure.
|
|
|
|
|
|
# Anthropic format: "… = available_tokens: 10000"
|
|
|
|
|
|
patterns = [
|
|
|
|
|
|
r'available_tokens[:\s]+(\d+)',
|
|
|
|
|
|
r'available\s+tokens[:\s]+(\d+)',
|
|
|
|
|
|
# fallback: last number after "=" in expressions like "200000 - 190000 = 10000"
|
|
|
|
|
|
r'=\s*(\d+)\s*$',
|
|
|
|
|
|
]
|
|
|
|
|
|
for pattern in patterns:
|
|
|
|
|
|
match = re.search(pattern, error_lower)
|
|
|
|
|
|
if match:
|
|
|
|
|
|
tokens = int(match.group(1))
|
|
|
|
|
|
if tokens >= 1:
|
|
|
|
|
|
return tokens
|
2026-06-07 03:52:09 -07:00
|
|
|
|
|
|
|
|
|
|
# OpenRouter/Nous format: "maximum context length is N … (A of text input,
|
|
|
|
|
|
# B of tool input, C in the output)". Available output = ctx - text - tool.
|
|
|
|
|
|
_m_ctx = re.search(r'maximum context length is (\d+)', error_lower)
|
|
|
|
|
|
_m_parts = re.search(
|
|
|
|
|
|
r'\((\d+)\s+of text input,\s*(\d+)\s+of tool input,\s*(\d+)\s+in the output\)',
|
|
|
|
|
|
error_lower,
|
|
|
|
|
|
)
|
|
|
|
|
|
if _m_ctx and _m_parts:
|
|
|
|
|
|
_available = int(_m_ctx.group(1)) - int(_m_parts.group(1)) - int(_m_parts.group(2))
|
|
|
|
|
|
if _available >= 1:
|
|
|
|
|
|
return _available
|
|
|
|
|
|
|
2026-06-09 16:51:07 +07:00
|
|
|
|
# LM Studio / llama.cpp style: context window is reported in tokens but the
|
|
|
|
|
|
# prompt size is reported in CHARACTERS, e.g.
|
|
|
|
|
|
# "maximum context length is 65536 tokens ... your prompt contains 77409
|
|
|
|
|
|
# characters ...".
|
|
|
|
|
|
# Estimate the input tokens conservatively (~3 chars/token, which
|
|
|
|
|
|
# over-reserves the input so the retried output cap stays safely inside the
|
|
|
|
|
|
# window) and leave the remainder of the window for output.
|
|
|
|
|
|
_m_ctx_tok = re.search(r'maximum context length is (\d+)\s*token', error_lower)
|
|
|
|
|
|
_m_chars = re.search(r'prompt contains (\d+)\s*character', error_lower)
|
|
|
|
|
|
if _m_ctx_tok and _m_chars:
|
|
|
|
|
|
_ctx = int(_m_ctx_tok.group(1))
|
|
|
|
|
|
_est_input = (int(_m_chars.group(1)) + 2) // 3
|
|
|
|
|
|
_available = _ctx - _est_input
|
|
|
|
|
|
if _available >= 1:
|
|
|
|
|
|
return _available
|
|
|
|
|
|
|
fix(compaction): don't halve context_length on output-cap-too-large errors
When the API returns "max_tokens too large given prompt" (input tokens
are within the context window, but input + requested output > window),
the old code incorrectly routed through the same handler as "prompt too
long" errors, calling get_next_probe_tier() and permanently halving
context_length. This made things worse: the window was fine, only the
requested output size needed trimming for that one call.
Two distinct error classes now handled separately:
Prompt too long — input itself exceeds context window.
Fix: compress history + halve context_length (existing behaviour,
unchanged).
Output cap too large — input OK, but input + max_tokens > window.
Fix: parse available_tokens from the error message, set a one-shot
_ephemeral_max_output_tokens override for the retry, and leave
context_length completely untouched.
Changes:
- agent/model_metadata.py: add parse_available_output_tokens_from_error()
that detects Anthropic's "available_tokens: N" error format and returns
the available output budget, or None for all other error types.
- run_agent.py: call the new parser first in the is_context_length_error
block; if it fires, set _ephemeral_max_output_tokens (with a 64-token
safety margin) and break to retry without touching context_length.
_build_api_kwargs consumes the ephemeral value exactly once then clears
it so subsequent calls use self.max_tokens normally.
- agent/anthropic_adapter.py: expand build_anthropic_kwargs docstring to
clearly document the max_tokens (output cap) vs context_length (total
window) distinction, which is a persistent source of confusion due to
the OpenAI-inherited "max_tokens" name.
- cli-config.yaml.example: add inline comments explaining both keys side
by side where users are most likely to look.
- website/docs/integrations/providers.md: add a callout box at the top
of "Context Length Detection" and clarify the troubleshooting entry.
- tests/test_ctx_halving_fix.py: 24 tests across four classes covering
the parser, build_anthropic_kwargs clamping, ephemeral one-shot
consumption, and the invariant that context_length is never mutated
on output-cap errors.
2026-04-09 16:54:23 +02:00
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-06-30 03:26:41 -07:00
|
|
|
|
def is_output_cap_error(error_msg: str) -> bool:
|
|
|
|
|
|
"""Return True if a 400 is about the OUTPUT cap (max_tokens) being too large.
|
|
|
|
|
|
|
|
|
|
|
|
This is the broader sibling of :func:`parse_available_output_tokens_from_error`:
|
|
|
|
|
|
that function only returns a number when it can extract the available output
|
|
|
|
|
|
budget from a *known* provider phrasing. This one answers the cheaper
|
|
|
|
|
|
yes/no question — "is this an output-cap error at all?" — across providers
|
|
|
|
|
|
whose exact wording we may not yet parse a number from.
|
|
|
|
|
|
|
|
|
|
|
|
Why this matters: an output-cap 400 is deterministic (every retry with the
|
|
|
|
|
|
same ``max_tokens`` gets the identical rejection). If such an error is
|
|
|
|
|
|
misclassified as a context-overflow it gets routed into the compression
|
|
|
|
|
|
loop, the compressor re-issues the call with the same oversized
|
|
|
|
|
|
``max_tokens``, the provider rejects it identically, and the session
|
|
|
|
|
|
death-loops until "cannot compress further" (issue #55546, DashScope/Qwen:
|
|
|
|
|
|
"Range of max_tokens should be [1, 65536]"). Compression cannot help an
|
|
|
|
|
|
output-cap error — the input already fits.
|
|
|
|
|
|
|
|
|
|
|
|
The signal: the error talks about ``max_tokens`` (or its aliases) as a
|
|
|
|
|
|
cap/range/limit, and does NOT talk about the INPUT/prompt/context window
|
|
|
|
|
|
being too long. When both are present we defer to the context-overflow
|
|
|
|
|
|
path (a real input overflow can also mention max_tokens).
|
|
|
|
|
|
"""
|
|
|
|
|
|
error_lower = error_msg.lower()
|
|
|
|
|
|
|
|
|
|
|
|
mentions_output_param = (
|
|
|
|
|
|
"max_tokens" in error_lower
|
|
|
|
|
|
or "max_output_tokens" in error_lower
|
|
|
|
|
|
or "max_completion_tokens" in error_lower
|
|
|
|
|
|
)
|
|
|
|
|
|
if not mentions_output_param:
|
|
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
|
|
|
# Phrasing that signals the OUTPUT cap specifically is the problem.
|
|
|
|
|
|
output_cap_signal = (
|
|
|
|
|
|
"range of max_tokens should be" in error_lower # DashScope / Alibaba
|
|
|
|
|
|
or "available_tokens" in error_lower # Anthropic
|
|
|
|
|
|
or "available tokens" in error_lower
|
|
|
|
|
|
or ("in the output" in error_lower # OpenRouter / Nous
|
|
|
|
|
|
and "maximum context length" in error_lower)
|
|
|
|
|
|
or ("requested" in error_lower # LM Studio / llama.cpp
|
|
|
|
|
|
and "output tokens" in error_lower)
|
|
|
|
|
|
or "should be" in error_lower # generic "max_tokens should be <= N"
|
|
|
|
|
|
or "less than or equal" in error_lower
|
|
|
|
|
|
or "must be" in error_lower
|
|
|
|
|
|
)
|
|
|
|
|
|
if not output_cap_signal:
|
|
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
|
|
|
# If the error ALSO clearly describes an oversized INPUT, it is a genuine
|
|
|
|
|
|
# context overflow that happens to mention max_tokens — let the
|
|
|
|
|
|
# context-overflow path handle it (it can compress the input).
|
|
|
|
|
|
input_overflow_signal = (
|
|
|
|
|
|
"prompt is too long" in error_lower
|
|
|
|
|
|
or "prompt too long" in error_lower
|
|
|
|
|
|
or "input is too long" in error_lower
|
|
|
|
|
|
or "input token" in error_lower
|
|
|
|
|
|
or "prompt length" in error_lower
|
|
|
|
|
|
or "prompt contains" in error_lower
|
|
|
|
|
|
or "reduce the length" in error_lower
|
|
|
|
|
|
)
|
|
|
|
|
|
return not input_overflow_signal
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-03-18 22:00:53 +01:00
|
|
|
|
def _model_id_matches(candidate_id: str, lookup_model: str) -> bool:
|
|
|
|
|
|
"""Return True if *candidate_id* (from server) matches *lookup_model* (configured).
|
|
|
|
|
|
|
|
|
|
|
|
Supports two forms:
|
|
|
|
|
|
- Exact match: "nvidia-nemotron-super-49b-v1" == "nvidia-nemotron-super-49b-v1"
|
|
|
|
|
|
- Slug match: "nvidia/nvidia-nemotron-super-49b-v1" matches "nvidia-nemotron-super-49b-v1"
|
|
|
|
|
|
(the part after the last "/" equals lookup_model)
|
|
|
|
|
|
|
|
|
|
|
|
This covers LM Studio's native API which stores models as "publisher/slug"
|
|
|
|
|
|
while users typically configure only the slug after the "local:" prefix.
|
|
|
|
|
|
"""
|
|
|
|
|
|
if candidate_id == lookup_model:
|
|
|
|
|
|
return True
|
|
|
|
|
|
# Slug match: basename of candidate equals the lookup name
|
|
|
|
|
|
if "/" in candidate_id and candidate_id.rsplit("/", 1)[1] == lookup_model:
|
|
|
|
|
|
return True
|
|
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-04-20 20:49:44 -07:00
|
|
|
|
def query_ollama_num_ctx(model: str, base_url: str, api_key: str = "") -> Optional[int]:
|
2026-04-07 22:23:28 -07:00
|
|
|
|
"""Query an Ollama server for the model's context length.
|
|
|
|
|
|
|
|
|
|
|
|
Returns the model's maximum context from GGUF metadata via ``/api/show``,
|
|
|
|
|
|
or the explicit ``num_ctx`` from the Modelfile if set. Returns None if
|
|
|
|
|
|
the server is unreachable or not Ollama.
|
|
|
|
|
|
|
|
|
|
|
|
This is the value that should be passed as ``num_ctx`` in Ollama chat
|
|
|
|
|
|
requests to override the default 2048.
|
|
|
|
|
|
"""
|
|
|
|
|
|
import httpx
|
|
|
|
|
|
|
|
|
|
|
|
bare_model = _strip_provider_prefix(model)
|
|
|
|
|
|
server_url = base_url.rstrip("/")
|
|
|
|
|
|
if server_url.endswith("/v1"):
|
|
|
|
|
|
server_url = server_url[:-3]
|
|
|
|
|
|
|
|
|
|
|
|
try:
|
2026-04-20 20:49:44 -07:00
|
|
|
|
server_type = detect_local_server_type(base_url, api_key=api_key)
|
2026-04-07 22:23:28 -07:00
|
|
|
|
except Exception:
|
|
|
|
|
|
return None
|
|
|
|
|
|
if server_type != "ollama":
|
|
|
|
|
|
return None
|
|
|
|
|
|
|
2026-04-20 20:49:44 -07:00
|
|
|
|
headers = _auth_headers(api_key)
|
|
|
|
|
|
|
2026-04-07 22:23:28 -07:00
|
|
|
|
try:
|
2026-04-20 20:49:44 -07:00
|
|
|
|
with httpx.Client(timeout=3.0, headers=headers) as client:
|
2026-04-07 22:23:28 -07:00
|
|
|
|
resp = client.post(f"{server_url}/api/show", json={"name": bare_model})
|
|
|
|
|
|
if resp.status_code != 200:
|
|
|
|
|
|
return None
|
|
|
|
|
|
data = resp.json()
|
|
|
|
|
|
|
|
|
|
|
|
# Prefer explicit num_ctx from Modelfile parameters (user override)
|
|
|
|
|
|
params = data.get("parameters", "")
|
|
|
|
|
|
if "num_ctx" in params:
|
|
|
|
|
|
for line in params.split("\n"):
|
|
|
|
|
|
if "num_ctx" in line:
|
|
|
|
|
|
parts = line.strip().split()
|
|
|
|
|
|
if len(parts) >= 2:
|
|
|
|
|
|
try:
|
|
|
|
|
|
return int(parts[-1])
|
|
|
|
|
|
except ValueError:
|
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
|
|
# Fall back to GGUF model_info context_length (training max)
|
|
|
|
|
|
model_info = data.get("model_info", {})
|
|
|
|
|
|
for key, value in model_info.items():
|
|
|
|
|
|
if "context_length" in key and isinstance(value, (int, float)):
|
|
|
|
|
|
return int(value)
|
|
|
|
|
|
except Exception:
|
|
|
|
|
|
pass
|
2026-06-29 07:13:00 +07:00
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def query_ollama_supports_vision(model: str, base_url: str, api_key: str = "") -> Optional[bool]:
|
|
|
|
|
|
"""Return True/False when Ollama ``/api/show`` reports vision support.
|
|
|
|
|
|
|
|
|
|
|
|
Uses the ``capabilities`` field on Ollama 0.6.0+ and falls back to
|
|
|
|
|
|
``model_info.*.vision.block_count`` on older servers. Returns None when
|
|
|
|
|
|
the server is unreachable, not Ollama, or the model is unknown.
|
|
|
|
|
|
"""
|
|
|
|
|
|
import httpx
|
|
|
|
|
|
|
|
|
|
|
|
bare_model = _strip_provider_prefix(model)
|
|
|
|
|
|
if not bare_model or not base_url:
|
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
|
if detect_local_server_type(base_url, api_key=api_key) != "ollama":
|
|
|
|
|
|
return None
|
|
|
|
|
|
except Exception:
|
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
|
|
server_url = base_url.rstrip("/")
|
|
|
|
|
|
if server_url.endswith("/v1"):
|
|
|
|
|
|
server_url = server_url[:-3]
|
|
|
|
|
|
|
|
|
|
|
|
headers = _auth_headers(api_key)
|
|
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
|
with httpx.Client(timeout=3.0, headers=headers) as client:
|
|
|
|
|
|
resp = client.post(f"{server_url}/api/show", json={"name": bare_model})
|
|
|
|
|
|
if resp.status_code != 200:
|
|
|
|
|
|
return None
|
|
|
|
|
|
data = resp.json()
|
|
|
|
|
|
except Exception:
|
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
|
|
caps = data.get("capabilities")
|
|
|
|
|
|
if isinstance(caps, list):
|
|
|
|
|
|
if any(str(cap).lower() == "vision" for cap in caps):
|
|
|
|
|
|
return True
|
|
|
|
|
|
if caps:
|
|
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
|
|
|
model_info = data.get("model_info")
|
|
|
|
|
|
if isinstance(model_info, dict):
|
|
|
|
|
|
for key in model_info:
|
|
|
|
|
|
if "vision.block_count" in str(key).lower():
|
|
|
|
|
|
return True
|
|
|
|
|
|
|
2026-04-07 22:23:28 -07:00
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-05-12 01:05:25 +05:30
|
|
|
|
def _query_ollama_api_show(model: str, base_url: str, api_key: str = "") -> Optional[int]:
|
|
|
|
|
|
"""Query an Ollama server's native ``/api/show`` for context length.
|
|
|
|
|
|
|
|
|
|
|
|
Provider-agnostic: works against ANY Ollama-compatible server regardless
|
|
|
|
|
|
of hostname — local Ollama, Ollama Cloud (``ollama.com``), custom Ollama
|
|
|
|
|
|
hosting behind a reverse proxy, etc. For non-Ollama servers the POST
|
|
|
|
|
|
returns 404/405 quickly; the function handles errors gracefully.
|
|
|
|
|
|
|
|
|
|
|
|
For hosted servers the GGUF ``model_info.*.context_length`` is the
|
|
|
|
|
|
authoritative source: the user can't set their own ``num_ctx``, and the
|
|
|
|
|
|
OpenAI-compat ``/v1/models`` endpoint correctly omits ``context_length``
|
|
|
|
|
|
per the OpenAI schema.
|
|
|
|
|
|
|
|
|
|
|
|
Resolution order for hosted Ollama:
|
|
|
|
|
|
1. ``model_info.*.context_length`` — GGUF training max (authoritative)
|
|
|
|
|
|
2. ``parameters`` → ``num_ctx`` — server-side Modelfile override
|
|
|
|
|
|
The order is flipped vs ``query_ollama_num_ctx()`` because local users
|
|
|
|
|
|
control ``num_ctx`` themselves; hosted users can't.
|
|
|
|
|
|
"""
|
|
|
|
|
|
import httpx
|
|
|
|
|
|
|
|
|
|
|
|
server_url = base_url.rstrip("/")
|
|
|
|
|
|
if server_url.endswith("/v1"):
|
|
|
|
|
|
server_url = server_url[:-3]
|
|
|
|
|
|
|
|
|
|
|
|
headers = _auth_headers(api_key)
|
|
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
|
with httpx.Client(timeout=5.0, headers=headers) as client:
|
|
|
|
|
|
resp = client.post(f"{server_url}/api/show", json={"name": model})
|
|
|
|
|
|
if resp.status_code != 200:
|
|
|
|
|
|
return None
|
|
|
|
|
|
data = resp.json()
|
|
|
|
|
|
|
|
|
|
|
|
# Hosted Ollama: GGUF model_info is the real max — prefer it over
|
|
|
|
|
|
# num_ctx which the Cloud operator may have capped arbitrarily.
|
|
|
|
|
|
model_info = data.get("model_info", {})
|
|
|
|
|
|
for key, value in model_info.items():
|
|
|
|
|
|
if "context_length" in key and isinstance(value, (int, float)):
|
|
|
|
|
|
ctx = int(value)
|
|
|
|
|
|
if ctx >= 1024:
|
|
|
|
|
|
return ctx
|
|
|
|
|
|
|
|
|
|
|
|
# Fall back to num_ctx from Modelfile parameters (rare on Cloud)
|
|
|
|
|
|
params = data.get("parameters", "")
|
|
|
|
|
|
if "num_ctx" in params:
|
|
|
|
|
|
for line in params.split("\n"):
|
|
|
|
|
|
if "num_ctx" in line:
|
|
|
|
|
|
parts = line.strip().split()
|
|
|
|
|
|
if len(parts) >= 2:
|
|
|
|
|
|
try:
|
|
|
|
|
|
ctx = int(parts[-1])
|
|
|
|
|
|
if ctx >= 1024:
|
|
|
|
|
|
return ctx
|
|
|
|
|
|
except ValueError:
|
|
|
|
|
|
pass
|
|
|
|
|
|
except Exception:
|
|
|
|
|
|
pass
|
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _model_name_suggests_kimi(model: str) -> bool:
|
|
|
|
|
|
"""Return True if the model name looks like a Kimi-family model.
|
|
|
|
|
|
|
|
|
|
|
|
Catches ``kimi-k2.6``, ``kimi-k2.5``, ``kimi-k2-thinking``,
|
|
|
|
|
|
``moonshotai/Kimi-K2.6``, and similar variants. Used as a guard
|
|
|
|
|
|
against stale OpenRouter metadata that underreports these models
|
|
|
|
|
|
as 32K context when they actually support 262K+.
|
|
|
|
|
|
"""
|
|
|
|
|
|
lower = model.lower()
|
|
|
|
|
|
return lower.startswith("kimi") or "moonshot" in lower
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-06-01 14:59:07 -07:00
|
|
|
|
def _model_name_suggests_minimax_m3(model: str) -> bool:
|
|
|
|
|
|
"""Return True if the model name looks like MiniMax M3.
|
|
|
|
|
|
|
|
|
|
|
|
Catches ``MiniMax-M3``, ``minimax/minimax-m3``, and similar variants
|
|
|
|
|
|
across surfaces (native MiniMax-M3, OpenRouter/Nous minimax/minimax-m3).
|
|
|
|
|
|
Used as a guard against stale cache entries seeded by pre-catalog builds
|
|
|
|
|
|
that resolved M3 via the generic ``minimax`` catch-all (204,800) before
|
|
|
|
|
|
the ``minimax-m3`` (1M) entry existed in DEFAULT_CONTEXT_LENGTHS.
|
|
|
|
|
|
"""
|
|
|
|
|
|
return "minimax-m3" in model.lower()
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-06-02 02:43:38 +03:00
|
|
|
|
def _model_name_suggests_grok_4_3(model: str) -> bool:
|
|
|
|
|
|
"""Return True if the model name looks like a Grok 4.3 variant.
|
|
|
|
|
|
|
|
|
|
|
|
Catches ``grok-4.3``, ``grok-4.3-latest``, and similar slugs.
|
|
|
|
|
|
Used as a guard against stale cache entries seeded by pre-catalog builds
|
|
|
|
|
|
that resolved grok-4.3 via the generic ``grok-4`` catch-all (256,000)
|
|
|
|
|
|
before the ``grok-4.3`` (1M) entry was added to DEFAULT_CONTEXT_LENGTHS
|
|
|
|
|
|
on 2026-05-15.
|
|
|
|
|
|
"""
|
|
|
|
|
|
return "grok-4.3" in model.lower()
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-04-20 20:49:44 -07:00
|
|
|
|
def _query_local_context_length(model: str, base_url: str, api_key: str = "") -> Optional[int]:
|
2026-03-18 21:38:41 +01:00
|
|
|
|
"""Query a local server for the model's context length."""
|
|
|
|
|
|
import httpx
|
|
|
|
|
|
|
2026-03-20 03:19:31 -07:00
|
|
|
|
# Strip recognised provider prefix (e.g., "local:model-name" → "model-name").
|
|
|
|
|
|
# Ollama "model:tag" colons (e.g. "qwen3.5:27b") are intentionally preserved.
|
|
|
|
|
|
model = _strip_provider_prefix(model)
|
2026-03-18 22:00:53 +01:00
|
|
|
|
|
2026-03-18 21:38:41 +01:00
|
|
|
|
# Strip /v1 suffix to get the server root
|
|
|
|
|
|
server_url = base_url.rstrip("/")
|
|
|
|
|
|
if server_url.endswith("/v1"):
|
|
|
|
|
|
server_url = server_url[:-3]
|
2026-06-27 08:06:51 +10:00
|
|
|
|
lmstudio_url = _lmstudio_server_root(base_url)
|
2026-03-18 21:38:41 +01:00
|
|
|
|
|
2026-04-20 20:49:44 -07:00
|
|
|
|
headers = _auth_headers(api_key)
|
|
|
|
|
|
|
2026-03-18 21:38:41 +01:00
|
|
|
|
try:
|
2026-04-20 20:49:44 -07:00
|
|
|
|
server_type = detect_local_server_type(base_url, api_key=api_key)
|
2026-03-18 21:38:41 +01:00
|
|
|
|
except Exception:
|
|
|
|
|
|
server_type = None
|
|
|
|
|
|
|
|
|
|
|
|
try:
|
2026-04-20 20:49:44 -07:00
|
|
|
|
with httpx.Client(timeout=3.0, headers=headers) as client:
|
2026-03-18 21:38:41 +01:00
|
|
|
|
# Ollama: /api/show returns model details with context info
|
|
|
|
|
|
if server_type == "ollama":
|
|
|
|
|
|
resp = client.post(f"{server_url}/api/show", json={"name": model})
|
|
|
|
|
|
if resp.status_code == 200:
|
|
|
|
|
|
data = resp.json()
|
2026-04-13 11:41:45 +02:00
|
|
|
|
# Prefer explicit num_ctx from Modelfile parameters: this is
|
|
|
|
|
|
# the *runtime* context Ollama will actually allocate KV cache
|
|
|
|
|
|
# for. The GGUF model_info.context_length is the training max,
|
|
|
|
|
|
# which can be larger than num_ctx — using it here would let
|
|
|
|
|
|
# Hermes grow conversations past the runtime limit and Ollama
|
|
|
|
|
|
# would silently truncate. Matches query_ollama_num_ctx().
|
2026-03-18 21:38:41 +01:00
|
|
|
|
params = data.get("parameters", "")
|
|
|
|
|
|
if "num_ctx" in params:
|
|
|
|
|
|
for line in params.split("\n"):
|
|
|
|
|
|
if "num_ctx" in line:
|
|
|
|
|
|
parts = line.strip().split()
|
|
|
|
|
|
if len(parts) >= 2:
|
|
|
|
|
|
try:
|
|
|
|
|
|
return int(parts[-1])
|
|
|
|
|
|
except ValueError:
|
|
|
|
|
|
pass
|
2026-04-13 11:41:45 +02:00
|
|
|
|
# Fall back to GGUF model_info context_length (training max)
|
|
|
|
|
|
model_info = data.get("model_info", {})
|
|
|
|
|
|
for key, value in model_info.items():
|
|
|
|
|
|
if "context_length" in key and isinstance(value, (int, float)):
|
|
|
|
|
|
return int(value)
|
2026-03-18 21:38:41 +01:00
|
|
|
|
|
2026-03-18 22:00:53 +01:00
|
|
|
|
# LM Studio native API: /api/v1/models returns max_context_length.
|
|
|
|
|
|
# This is more reliable than the OpenAI-compat /v1/models which
|
|
|
|
|
|
# doesn't include context window information for LM Studio servers.
|
|
|
|
|
|
# Use _model_id_matches for fuzzy matching: LM Studio stores models as
|
|
|
|
|
|
# "publisher/slug" but users configure only "slug" after "local:" prefix.
|
|
|
|
|
|
if server_type == "lm-studio":
|
2026-06-27 08:06:51 +10:00
|
|
|
|
resp = client.get(f"{lmstudio_url}/api/v1/models")
|
2026-03-18 22:00:53 +01:00
|
|
|
|
if resp.status_code == 200:
|
|
|
|
|
|
data = resp.json()
|
|
|
|
|
|
for m in data.get("models", []):
|
|
|
|
|
|
if _model_id_matches(m.get("key", ""), model) or _model_id_matches(m.get("id", ""), model):
|
|
|
|
|
|
# Prefer loaded instance context (actual runtime value)
|
|
|
|
|
|
for inst in m.get("loaded_instances", []):
|
|
|
|
|
|
cfg = inst.get("config", {})
|
|
|
|
|
|
ctx = cfg.get("context_length")
|
|
|
|
|
|
if ctx and isinstance(ctx, (int, float)):
|
|
|
|
|
|
return int(ctx)
|
2026-04-27 11:59:32 -04:00
|
|
|
|
break
|
2026-03-18 22:00:53 +01:00
|
|
|
|
|
2026-03-18 21:38:41 +01:00
|
|
|
|
# LM Studio / vLLM / llama.cpp: try /v1/models/{model}
|
|
|
|
|
|
resp = client.get(f"{server_url}/v1/models/{model}")
|
|
|
|
|
|
if resp.status_code == 200:
|
|
|
|
|
|
data = resp.json()
|
|
|
|
|
|
# vLLM returns max_model_len
|
|
|
|
|
|
ctx = data.get("max_model_len") or data.get("context_length") or data.get("max_tokens")
|
|
|
|
|
|
if ctx and isinstance(ctx, (int, float)):
|
|
|
|
|
|
return int(ctx)
|
|
|
|
|
|
|
2026-03-18 22:00:53 +01:00
|
|
|
|
# Try /v1/models and find the model in the list.
|
|
|
|
|
|
# Use _model_id_matches to handle "publisher/slug" vs bare "slug".
|
2026-03-18 21:38:41 +01:00
|
|
|
|
resp = client.get(f"{server_url}/v1/models")
|
|
|
|
|
|
if resp.status_code == 200:
|
|
|
|
|
|
data = resp.json()
|
|
|
|
|
|
models_list = data.get("data", [])
|
|
|
|
|
|
for m in models_list:
|
2026-03-18 22:00:53 +01:00
|
|
|
|
if _model_id_matches(m.get("id", ""), model):
|
2026-03-18 21:38:41 +01:00
|
|
|
|
ctx = m.get("max_model_len") or m.get("context_length") or m.get("max_tokens")
|
|
|
|
|
|
if ctx and isinstance(ctx, (int, float)):
|
|
|
|
|
|
return int(ctx)
|
|
|
|
|
|
except Exception:
|
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
|
|
|
feat: overhaul context length detection with models.dev and provider-aware resolution (#2158)
Replace the fragile hardcoded context length system with a multi-source
resolution chain that correctly identifies context windows per provider.
Key changes:
- New agent/models_dev.py: Fetches and caches the models.dev registry
(3800+ models across 100+ providers with per-provider context windows).
In-memory cache (1hr TTL) + disk cache for cold starts.
- Rewritten get_model_context_length() resolution chain:
0. Config override (model.context_length)
1. Custom providers per-model context_length
2. Persistent disk cache
3. Endpoint /models (local servers)
4. Anthropic /v1/models API (max_input_tokens, API-key only)
5. OpenRouter live API (existing, unchanged)
6. Nous suffix-match via OpenRouter (dot/dash normalization)
7. models.dev registry lookup (provider-aware)
8. Thin hardcoded defaults (broad family patterns)
9. 128K fallback (was 2M)
- Provider-aware context: same model now correctly resolves to different
context windows per provider (e.g. claude-opus-4.6: 1M on Anthropic,
128K on GitHub Copilot). Provider name flows through ContextCompressor.
- DEFAULT_CONTEXT_LENGTHS shrunk from 80+ entries to ~16 broad patterns.
models.dev replaces the per-model hardcoding.
- CONTEXT_PROBE_TIERS changed from [2M, 1M, 512K, 200K, 128K, 64K, 32K]
to [128K, 64K, 32K, 16K, 8K]. Unknown models no longer start at 2M.
- hermes model: prompts for context_length when configuring custom
endpoints. Supports shorthand (32k, 128K). Saved to custom_providers
per-model config.
- custom_providers schema extended with optional models dict for
per-model context_length (backward compatible).
- Nous Portal: suffix-matches bare IDs (claude-opus-4-6) against
OpenRouter's prefixed IDs (anthropic/claude-opus-4.6) with dot/dash
normalization. Handles all 15 current Nous models.
- Anthropic direct: queries /v1/models for max_input_tokens. Only works
with regular API keys (sk-ant-api*), not OAuth tokens. Falls through
to models.dev for OAuth users.
Tests: 5574 passed (18 new tests for models_dev + updated probe tiers)
Docs: Updated configuration.md context length section, AGENTS.md
Co-authored-by: Test <test@test.com>
2026-03-20 06:04:33 -07:00
|
|
|
|
def _normalize_model_version(model: str) -> str:
|
|
|
|
|
|
"""Normalize version separators for matching.
|
|
|
|
|
|
|
|
|
|
|
|
Nous uses dashes: claude-opus-4-6, claude-sonnet-4-5
|
|
|
|
|
|
OpenRouter uses dots: claude-opus-4.6, claude-sonnet-4.5
|
|
|
|
|
|
Normalize both to dashes for comparison.
|
|
|
|
|
|
"""
|
|
|
|
|
|
return model.replace(".", "-")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _query_anthropic_context_length(model: str, base_url: str, api_key: str) -> Optional[int]:
|
|
|
|
|
|
"""Query Anthropic's /v1/models endpoint for context length.
|
|
|
|
|
|
|
|
|
|
|
|
Only works with regular ANTHROPIC_API_KEY (sk-ant-api*).
|
|
|
|
|
|
OAuth tokens (sk-ant-oat*) from Claude Code return 401.
|
|
|
|
|
|
"""
|
|
|
|
|
|
if not api_key or api_key.startswith("sk-ant-oat"):
|
|
|
|
|
|
return None # OAuth tokens can't access /v1/models
|
|
|
|
|
|
try:
|
|
|
|
|
|
base = base_url.rstrip("/")
|
|
|
|
|
|
if base.endswith("/v1"):
|
|
|
|
|
|
base = base[:-3]
|
|
|
|
|
|
url = f"{base}/v1/models?limit=1000"
|
|
|
|
|
|
headers = {
|
|
|
|
|
|
"x-api-key": api_key,
|
|
|
|
|
|
"anthropic-version": "2023-06-01",
|
|
|
|
|
|
}
|
2026-04-23 14:59:26 +03:00
|
|
|
|
resp = requests.get(url, headers=headers, timeout=10, verify=_resolve_requests_verify())
|
feat: overhaul context length detection with models.dev and provider-aware resolution (#2158)
Replace the fragile hardcoded context length system with a multi-source
resolution chain that correctly identifies context windows per provider.
Key changes:
- New agent/models_dev.py: Fetches and caches the models.dev registry
(3800+ models across 100+ providers with per-provider context windows).
In-memory cache (1hr TTL) + disk cache for cold starts.
- Rewritten get_model_context_length() resolution chain:
0. Config override (model.context_length)
1. Custom providers per-model context_length
2. Persistent disk cache
3. Endpoint /models (local servers)
4. Anthropic /v1/models API (max_input_tokens, API-key only)
5. OpenRouter live API (existing, unchanged)
6. Nous suffix-match via OpenRouter (dot/dash normalization)
7. models.dev registry lookup (provider-aware)
8. Thin hardcoded defaults (broad family patterns)
9. 128K fallback (was 2M)
- Provider-aware context: same model now correctly resolves to different
context windows per provider (e.g. claude-opus-4.6: 1M on Anthropic,
128K on GitHub Copilot). Provider name flows through ContextCompressor.
- DEFAULT_CONTEXT_LENGTHS shrunk from 80+ entries to ~16 broad patterns.
models.dev replaces the per-model hardcoding.
- CONTEXT_PROBE_TIERS changed from [2M, 1M, 512K, 200K, 128K, 64K, 32K]
to [128K, 64K, 32K, 16K, 8K]. Unknown models no longer start at 2M.
- hermes model: prompts for context_length when configuring custom
endpoints. Supports shorthand (32k, 128K). Saved to custom_providers
per-model config.
- custom_providers schema extended with optional models dict for
per-model context_length (backward compatible).
- Nous Portal: suffix-matches bare IDs (claude-opus-4-6) against
OpenRouter's prefixed IDs (anthropic/claude-opus-4.6) with dot/dash
normalization. Handles all 15 current Nous models.
- Anthropic direct: queries /v1/models for max_input_tokens. Only works
with regular API keys (sk-ant-api*), not OAuth tokens. Falls through
to models.dev for OAuth users.
Tests: 5574 passed (18 new tests for models_dev + updated probe tiers)
Docs: Updated configuration.md context length section, AGENTS.md
Co-authored-by: Test <test@test.com>
2026-03-20 06:04:33 -07:00
|
|
|
|
if resp.status_code != 200:
|
|
|
|
|
|
return None
|
|
|
|
|
|
data = resp.json()
|
|
|
|
|
|
for m in data.get("data", []):
|
|
|
|
|
|
if m.get("id") == model:
|
|
|
|
|
|
ctx = m.get("max_input_tokens")
|
|
|
|
|
|
if isinstance(ctx, int) and ctx > 0:
|
|
|
|
|
|
return ctx
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
logger.debug("Anthropic /v1/models query failed: %s", e)
|
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
|
|
|
fix(context): resolve real Codex OAuth context windows (272k, not 1M) (#14935)
On ChatGPT Codex OAuth every gpt-5.x slug actually caps at 272,000 tokens,
but Hermes was resolving gpt-5.5 / gpt-5.4 to 1,050,000 (from models.dev)
because openai-codex aliases to the openai entry there. At 1.05M the
compressor never fires and requests hard-fail with 'context window
exceeded' around the real 272k boundary.
Verified live against chatgpt.com/backend-api/codex/models:
gpt-5.5, gpt-5.4, gpt-5.4-mini, gpt-5.3-codex, gpt-5.2-codex,
gpt-5.2, gpt-5.1-codex-max → context_window = 272000
Changes:
- agent/model_metadata.py:
* _fetch_codex_oauth_context_lengths() — probe the Codex /models
endpoint with the OAuth bearer token and read context_window per
slug (1h in-memory TTL).
* _resolve_codex_oauth_context_length() — prefer the live probe,
fall back to hardcoded _CODEX_OAUTH_CONTEXT_FALLBACK (all 272k).
* Wire into get_model_context_length() when provider=='openai-codex',
running BEFORE the models.dev lookup (which returns 1.05M). Result
persists via save_context_length() so subsequent lookups skip the
probe entirely.
* Fixed the now-wrong comment on the DEFAULT_CONTEXT_LENGTHS gpt-5.5
entry (400k was never right for Codex; it's the catch-all for
providers we can't probe live).
Tests (4 new in TestCodexOAuthContextLength):
- fallback table used when no token is available (no models.dev leakage)
- live probe overrides the fallback
- probe failure (non-200) falls back to hardcoded 272k
- non-codex providers (openrouter, direct openai) unaffected
Non-codex context resolution is unchanged — the Codex branch only fires
when provider=='openai-codex'.
2026-04-23 22:39:47 -07:00
|
|
|
|
# Known ChatGPT Codex OAuth context windows (observed via live
|
|
|
|
|
|
# chatgpt.com/backend-api/codex/models probe, Apr 2026). These are the
|
|
|
|
|
|
# `context_window` values, which are what Codex actually enforces — the
|
|
|
|
|
|
# direct OpenAI API has larger limits for the same slugs, but Codex OAuth
|
|
|
|
|
|
# caps lower (e.g. gpt-5.5 is 1.05M on the API, 272K on Codex).
|
|
|
|
|
|
#
|
|
|
|
|
|
# Used as a fallback when the live probe fails (no token, network error).
|
|
|
|
|
|
# Longest keys first so substring match picks the most specific entry.
|
|
|
|
|
|
_CODEX_OAUTH_CONTEXT_FALLBACK: Dict[str, int] = {
|
|
|
|
|
|
"gpt-5.1-codex-max": 272_000,
|
|
|
|
|
|
"gpt-5.1-codex-mini": 272_000,
|
|
|
|
|
|
"gpt-5.3-codex": 272_000,
|
2026-05-10 10:41:24 +05:30
|
|
|
|
# Spark runs on specialised low-latency hardware and exposes a smaller
|
|
|
|
|
|
# 128k window than other Codex OAuth slugs. Listed explicitly so the
|
|
|
|
|
|
# longest-key-first fallback resolves it correctly — substring match
|
|
|
|
|
|
# on "gpt-5.3-codex" otherwise wins and reports 272k. Availability is
|
|
|
|
|
|
# gated by ChatGPT Pro entitlement on the Codex backend.
|
2026-05-01 11:16:49 +04:00
|
|
|
|
"gpt-5.3-codex-spark": 128_000,
|
fix(context): resolve real Codex OAuth context windows (272k, not 1M) (#14935)
On ChatGPT Codex OAuth every gpt-5.x slug actually caps at 272,000 tokens,
but Hermes was resolving gpt-5.5 / gpt-5.4 to 1,050,000 (from models.dev)
because openai-codex aliases to the openai entry there. At 1.05M the
compressor never fires and requests hard-fail with 'context window
exceeded' around the real 272k boundary.
Verified live against chatgpt.com/backend-api/codex/models:
gpt-5.5, gpt-5.4, gpt-5.4-mini, gpt-5.3-codex, gpt-5.2-codex,
gpt-5.2, gpt-5.1-codex-max → context_window = 272000
Changes:
- agent/model_metadata.py:
* _fetch_codex_oauth_context_lengths() — probe the Codex /models
endpoint with the OAuth bearer token and read context_window per
slug (1h in-memory TTL).
* _resolve_codex_oauth_context_length() — prefer the live probe,
fall back to hardcoded _CODEX_OAUTH_CONTEXT_FALLBACK (all 272k).
* Wire into get_model_context_length() when provider=='openai-codex',
running BEFORE the models.dev lookup (which returns 1.05M). Result
persists via save_context_length() so subsequent lookups skip the
probe entirely.
* Fixed the now-wrong comment on the DEFAULT_CONTEXT_LENGTHS gpt-5.5
entry (400k was never right for Codex; it's the catch-all for
providers we can't probe live).
Tests (4 new in TestCodexOAuthContextLength):
- fallback table used when no token is available (no models.dev leakage)
- live probe overrides the fallback
- probe failure (non-200) falls back to hardcoded 272k
- non-codex providers (openrouter, direct openai) unaffected
Non-codex context resolution is unchanged — the Codex branch only fires
when provider=='openai-codex'.
2026-04-23 22:39:47 -07:00
|
|
|
|
"gpt-5.2-codex": 272_000,
|
|
|
|
|
|
"gpt-5.4-mini": 272_000,
|
|
|
|
|
|
"gpt-5.5": 272_000,
|
|
|
|
|
|
"gpt-5.4": 272_000,
|
|
|
|
|
|
"gpt-5.2": 272_000,
|
|
|
|
|
|
"gpt-5": 272_000,
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
_codex_oauth_context_cache: Dict[str, int] = {}
|
|
|
|
|
|
_codex_oauth_context_cache_time: float = 0.0
|
|
|
|
|
|
_CODEX_OAUTH_CONTEXT_CACHE_TTL = 3600 # 1 hour
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _fetch_codex_oauth_context_lengths(access_token: str) -> Dict[str, int]:
|
|
|
|
|
|
"""Probe the ChatGPT Codex /models endpoint for per-slug context windows.
|
|
|
|
|
|
|
|
|
|
|
|
Codex OAuth imposes its own context limits that differ from the direct
|
|
|
|
|
|
OpenAI API (e.g. gpt-5.5 is 1.05M on the API, 272K on Codex). The
|
|
|
|
|
|
`context_window` field in each model entry is the authoritative source.
|
|
|
|
|
|
|
|
|
|
|
|
Returns a ``{slug: context_window}`` dict. Empty on failure.
|
|
|
|
|
|
"""
|
|
|
|
|
|
global _codex_oauth_context_cache, _codex_oauth_context_cache_time
|
|
|
|
|
|
now = time.time()
|
|
|
|
|
|
if (
|
|
|
|
|
|
_codex_oauth_context_cache
|
|
|
|
|
|
and now - _codex_oauth_context_cache_time < _CODEX_OAUTH_CONTEXT_CACHE_TTL
|
|
|
|
|
|
):
|
|
|
|
|
|
return _codex_oauth_context_cache
|
|
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
|
resp = requests.get(
|
|
|
|
|
|
"https://chatgpt.com/backend-api/codex/models?client_version=1.0.0",
|
|
|
|
|
|
headers={"Authorization": f"Bearer {access_token}"},
|
|
|
|
|
|
timeout=10,
|
2026-04-24 03:02:24 -07:00
|
|
|
|
verify=_resolve_requests_verify(),
|
fix(context): resolve real Codex OAuth context windows (272k, not 1M) (#14935)
On ChatGPT Codex OAuth every gpt-5.x slug actually caps at 272,000 tokens,
but Hermes was resolving gpt-5.5 / gpt-5.4 to 1,050,000 (from models.dev)
because openai-codex aliases to the openai entry there. At 1.05M the
compressor never fires and requests hard-fail with 'context window
exceeded' around the real 272k boundary.
Verified live against chatgpt.com/backend-api/codex/models:
gpt-5.5, gpt-5.4, gpt-5.4-mini, gpt-5.3-codex, gpt-5.2-codex,
gpt-5.2, gpt-5.1-codex-max → context_window = 272000
Changes:
- agent/model_metadata.py:
* _fetch_codex_oauth_context_lengths() — probe the Codex /models
endpoint with the OAuth bearer token and read context_window per
slug (1h in-memory TTL).
* _resolve_codex_oauth_context_length() — prefer the live probe,
fall back to hardcoded _CODEX_OAUTH_CONTEXT_FALLBACK (all 272k).
* Wire into get_model_context_length() when provider=='openai-codex',
running BEFORE the models.dev lookup (which returns 1.05M). Result
persists via save_context_length() so subsequent lookups skip the
probe entirely.
* Fixed the now-wrong comment on the DEFAULT_CONTEXT_LENGTHS gpt-5.5
entry (400k was never right for Codex; it's the catch-all for
providers we can't probe live).
Tests (4 new in TestCodexOAuthContextLength):
- fallback table used when no token is available (no models.dev leakage)
- live probe overrides the fallback
- probe failure (non-200) falls back to hardcoded 272k
- non-codex providers (openrouter, direct openai) unaffected
Non-codex context resolution is unchanged — the Codex branch only fires
when provider=='openai-codex'.
2026-04-23 22:39:47 -07:00
|
|
|
|
)
|
|
|
|
|
|
if resp.status_code != 200:
|
|
|
|
|
|
logger.debug(
|
|
|
|
|
|
"Codex /models probe returned HTTP %s; falling back to hardcoded defaults",
|
|
|
|
|
|
resp.status_code,
|
|
|
|
|
|
)
|
|
|
|
|
|
return {}
|
|
|
|
|
|
data = resp.json()
|
|
|
|
|
|
except Exception as exc:
|
|
|
|
|
|
logger.debug("Codex /models probe failed: %s", exc)
|
|
|
|
|
|
return {}
|
|
|
|
|
|
|
|
|
|
|
|
entries = data.get("models", []) if isinstance(data, dict) else []
|
|
|
|
|
|
result: Dict[str, int] = {}
|
|
|
|
|
|
for item in entries:
|
|
|
|
|
|
if not isinstance(item, dict):
|
|
|
|
|
|
continue
|
|
|
|
|
|
slug = item.get("slug")
|
|
|
|
|
|
ctx = item.get("context_window")
|
|
|
|
|
|
if isinstance(slug, str) and isinstance(ctx, int) and ctx > 0:
|
|
|
|
|
|
result[slug.strip()] = ctx
|
|
|
|
|
|
|
|
|
|
|
|
if result:
|
|
|
|
|
|
_codex_oauth_context_cache = result
|
|
|
|
|
|
_codex_oauth_context_cache_time = now
|
|
|
|
|
|
return result
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _resolve_codex_oauth_context_length(
|
|
|
|
|
|
model: str, access_token: str = ""
|
|
|
|
|
|
) -> Optional[int]:
|
|
|
|
|
|
"""Resolve a Codex OAuth model's real context window.
|
|
|
|
|
|
|
|
|
|
|
|
Prefers a live probe of chatgpt.com/backend-api/codex/models (when we
|
|
|
|
|
|
have a bearer token), then falls back to ``_CODEX_OAUTH_CONTEXT_FALLBACK``.
|
|
|
|
|
|
"""
|
|
|
|
|
|
model_bare = _strip_provider_prefix(model).strip()
|
|
|
|
|
|
if not model_bare:
|
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
|
|
if access_token:
|
|
|
|
|
|
live = _fetch_codex_oauth_context_lengths(access_token)
|
|
|
|
|
|
if model_bare in live:
|
|
|
|
|
|
return live[model_bare]
|
|
|
|
|
|
# Case-insensitive match in case casing drifts
|
|
|
|
|
|
model_lower = model_bare.lower()
|
|
|
|
|
|
for slug, ctx in live.items():
|
|
|
|
|
|
if slug.lower() == model_lower:
|
|
|
|
|
|
return ctx
|
|
|
|
|
|
|
|
|
|
|
|
# Fallback: longest-key-first substring match over hardcoded defaults.
|
|
|
|
|
|
model_lower = model_bare.lower()
|
|
|
|
|
|
for slug, ctx in sorted(
|
|
|
|
|
|
_CODEX_OAUTH_CONTEXT_FALLBACK.items(), key=lambda x: len(x[0]), reverse=True
|
|
|
|
|
|
):
|
|
|
|
|
|
if slug in model_lower:
|
|
|
|
|
|
return ctx
|
|
|
|
|
|
|
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-05-12 14:59:31 -04:00
|
|
|
|
def _resolve_nous_context_length(
|
|
|
|
|
|
model: str,
|
|
|
|
|
|
base_url: str = "",
|
|
|
|
|
|
api_key: str = "",
|
|
|
|
|
|
) -> Tuple[Optional[int], str]:
|
|
|
|
|
|
"""Resolve Nous Portal model context length.
|
|
|
|
|
|
|
|
|
|
|
|
Tries the live Nous inference endpoint first (authoritative), then falls
|
|
|
|
|
|
back to OpenRouter metadata with suffix/version matching.
|
|
|
|
|
|
|
|
|
|
|
|
Nous model IDs are bare after prefix-stripping (e.g. 'qwen3.6-plus',
|
|
|
|
|
|
'claude-opus-4-6') while OpenRouter uses prefixed IDs (e.g.
|
|
|
|
|
|
'qwen/qwen3.6-plus', 'anthropic/claude-opus-4.6'). Version
|
|
|
|
|
|
normalization (dot↔dash) is applied to handle name drifts.
|
|
|
|
|
|
|
|
|
|
|
|
Returns ``(context_length, source)`` where ``source`` is one of:
|
|
|
|
|
|
- ``"portal"`` — live /v1/models response (authoritative)
|
|
|
|
|
|
- ``"openrouter"`` — OpenRouter cache fallback (non-authoritative;
|
|
|
|
|
|
callers must NOT persist this to the on-disk cache or a single
|
|
|
|
|
|
portal blip will freeze the wrong value in forever)
|
|
|
|
|
|
- ``""`` — could not resolve
|
feat: overhaul context length detection with models.dev and provider-aware resolution (#2158)
Replace the fragile hardcoded context length system with a multi-source
resolution chain that correctly identifies context windows per provider.
Key changes:
- New agent/models_dev.py: Fetches and caches the models.dev registry
(3800+ models across 100+ providers with per-provider context windows).
In-memory cache (1hr TTL) + disk cache for cold starts.
- Rewritten get_model_context_length() resolution chain:
0. Config override (model.context_length)
1. Custom providers per-model context_length
2. Persistent disk cache
3. Endpoint /models (local servers)
4. Anthropic /v1/models API (max_input_tokens, API-key only)
5. OpenRouter live API (existing, unchanged)
6. Nous suffix-match via OpenRouter (dot/dash normalization)
7. models.dev registry lookup (provider-aware)
8. Thin hardcoded defaults (broad family patterns)
9. 128K fallback (was 2M)
- Provider-aware context: same model now correctly resolves to different
context windows per provider (e.g. claude-opus-4.6: 1M on Anthropic,
128K on GitHub Copilot). Provider name flows through ContextCompressor.
- DEFAULT_CONTEXT_LENGTHS shrunk from 80+ entries to ~16 broad patterns.
models.dev replaces the per-model hardcoding.
- CONTEXT_PROBE_TIERS changed from [2M, 1M, 512K, 200K, 128K, 64K, 32K]
to [128K, 64K, 32K, 16K, 8K]. Unknown models no longer start at 2M.
- hermes model: prompts for context_length when configuring custom
endpoints. Supports shorthand (32k, 128K). Saved to custom_providers
per-model config.
- custom_providers schema extended with optional models dict for
per-model context_length (backward compatible).
- Nous Portal: suffix-matches bare IDs (claude-opus-4-6) against
OpenRouter's prefixed IDs (anthropic/claude-opus-4.6) with dot/dash
normalization. Handles all 15 current Nous models.
- Anthropic direct: queries /v1/models for max_input_tokens. Only works
with regular API keys (sk-ant-api*), not OAuth tokens. Falls through
to models.dev for OAuth users.
Tests: 5574 passed (18 new tests for models_dev + updated probe tiers)
Docs: Updated configuration.md context length section, AGENTS.md
Co-authored-by: Test <test@test.com>
2026-03-20 06:04:33 -07:00
|
|
|
|
"""
|
2026-05-12 14:59:31 -04:00
|
|
|
|
# Portal first — the Nous /models endpoint is authoritative for what our
|
|
|
|
|
|
# infrastructure enforces and may differ from OR (e.g. OR reports 1M for
|
|
|
|
|
|
# qwen3.6-plus; the portal correctly says 262144). Fall back to the OR
|
|
|
|
|
|
# catalog only if the portal doesn't list the model.
|
|
|
|
|
|
if base_url:
|
|
|
|
|
|
portal_ctx = _resolve_endpoint_context_length(model, base_url, api_key=api_key)
|
|
|
|
|
|
if portal_ctx is not None:
|
|
|
|
|
|
return portal_ctx, "portal"
|
2026-05-12 00:24:01 -04:00
|
|
|
|
|
2026-05-12 14:59:31 -04:00
|
|
|
|
metadata = fetch_model_metadata()
|
2026-05-12 00:24:01 -04:00
|
|
|
|
|
2026-05-12 14:59:31 -04:00
|
|
|
|
def _safe_ctx(or_id: str, entry: dict) -> Optional[int]:
|
2026-05-12 00:24:01 -04:00
|
|
|
|
ctx = entry.get("context_length")
|
|
|
|
|
|
if ctx is None:
|
|
|
|
|
|
return None
|
2026-05-12 00:25:40 -04:00
|
|
|
|
if ctx <= 32768 and _model_name_suggests_kimi(or_id):
|
2026-05-12 00:24:01 -04:00
|
|
|
|
logger.info(
|
|
|
|
|
|
"Rejecting OpenRouter metadata context=%s for %r "
|
|
|
|
|
|
"(Kimi-family underreport, Nous path); falling through to hardcoded defaults",
|
|
|
|
|
|
ctx, or_id,
|
|
|
|
|
|
)
|
|
|
|
|
|
return None
|
|
|
|
|
|
return ctx
|
|
|
|
|
|
|
feat: overhaul context length detection with models.dev and provider-aware resolution (#2158)
Replace the fragile hardcoded context length system with a multi-source
resolution chain that correctly identifies context windows per provider.
Key changes:
- New agent/models_dev.py: Fetches and caches the models.dev registry
(3800+ models across 100+ providers with per-provider context windows).
In-memory cache (1hr TTL) + disk cache for cold starts.
- Rewritten get_model_context_length() resolution chain:
0. Config override (model.context_length)
1. Custom providers per-model context_length
2. Persistent disk cache
3. Endpoint /models (local servers)
4. Anthropic /v1/models API (max_input_tokens, API-key only)
5. OpenRouter live API (existing, unchanged)
6. Nous suffix-match via OpenRouter (dot/dash normalization)
7. models.dev registry lookup (provider-aware)
8. Thin hardcoded defaults (broad family patterns)
9. 128K fallback (was 2M)
- Provider-aware context: same model now correctly resolves to different
context windows per provider (e.g. claude-opus-4.6: 1M on Anthropic,
128K on GitHub Copilot). Provider name flows through ContextCompressor.
- DEFAULT_CONTEXT_LENGTHS shrunk from 80+ entries to ~16 broad patterns.
models.dev replaces the per-model hardcoding.
- CONTEXT_PROBE_TIERS changed from [2M, 1M, 512K, 200K, 128K, 64K, 32K]
to [128K, 64K, 32K, 16K, 8K]. Unknown models no longer start at 2M.
- hermes model: prompts for context_length when configuring custom
endpoints. Supports shorthand (32k, 128K). Saved to custom_providers
per-model config.
- custom_providers schema extended with optional models dict for
per-model context_length (backward compatible).
- Nous Portal: suffix-matches bare IDs (claude-opus-4-6) against
OpenRouter's prefixed IDs (anthropic/claude-opus-4.6) with dot/dash
normalization. Handles all 15 current Nous models.
- Anthropic direct: queries /v1/models for max_input_tokens. Only works
with regular API keys (sk-ant-api*), not OAuth tokens. Falls through
to models.dev for OAuth users.
Tests: 5574 passed (18 new tests for models_dev + updated probe tiers)
Docs: Updated configuration.md context length section, AGENTS.md
Co-authored-by: Test <test@test.com>
2026-03-20 06:04:33 -07:00
|
|
|
|
if model in metadata:
|
2026-05-12 14:59:31 -04:00
|
|
|
|
ctx = _safe_ctx(model, metadata[model])
|
|
|
|
|
|
if ctx is not None:
|
|
|
|
|
|
return ctx, "openrouter"
|
feat: overhaul context length detection with models.dev and provider-aware resolution (#2158)
Replace the fragile hardcoded context length system with a multi-source
resolution chain that correctly identifies context windows per provider.
Key changes:
- New agent/models_dev.py: Fetches and caches the models.dev registry
(3800+ models across 100+ providers with per-provider context windows).
In-memory cache (1hr TTL) + disk cache for cold starts.
- Rewritten get_model_context_length() resolution chain:
0. Config override (model.context_length)
1. Custom providers per-model context_length
2. Persistent disk cache
3. Endpoint /models (local servers)
4. Anthropic /v1/models API (max_input_tokens, API-key only)
5. OpenRouter live API (existing, unchanged)
6. Nous suffix-match via OpenRouter (dot/dash normalization)
7. models.dev registry lookup (provider-aware)
8. Thin hardcoded defaults (broad family patterns)
9. 128K fallback (was 2M)
- Provider-aware context: same model now correctly resolves to different
context windows per provider (e.g. claude-opus-4.6: 1M on Anthropic,
128K on GitHub Copilot). Provider name flows through ContextCompressor.
- DEFAULT_CONTEXT_LENGTHS shrunk from 80+ entries to ~16 broad patterns.
models.dev replaces the per-model hardcoding.
- CONTEXT_PROBE_TIERS changed from [2M, 1M, 512K, 200K, 128K, 64K, 32K]
to [128K, 64K, 32K, 16K, 8K]. Unknown models no longer start at 2M.
- hermes model: prompts for context_length when configuring custom
endpoints. Supports shorthand (32k, 128K). Saved to custom_providers
per-model config.
- custom_providers schema extended with optional models dict for
per-model context_length (backward compatible).
- Nous Portal: suffix-matches bare IDs (claude-opus-4-6) against
OpenRouter's prefixed IDs (anthropic/claude-opus-4.6) with dot/dash
normalization. Handles all 15 current Nous models.
- Anthropic direct: queries /v1/models for max_input_tokens. Only works
with regular API keys (sk-ant-api*), not OAuth tokens. Falls through
to models.dev for OAuth users.
Tests: 5574 passed (18 new tests for models_dev + updated probe tiers)
Docs: Updated configuration.md context length section, AGENTS.md
Co-authored-by: Test <test@test.com>
2026-03-20 06:04:33 -07:00
|
|
|
|
|
|
|
|
|
|
normalized = _normalize_model_version(model).lower()
|
|
|
|
|
|
|
|
|
|
|
|
for or_id, entry in metadata.items():
|
|
|
|
|
|
bare = or_id.split("/", 1)[1] if "/" in or_id else or_id
|
|
|
|
|
|
if bare.lower() == model.lower() or _normalize_model_version(bare).lower() == normalized:
|
2026-05-12 14:59:31 -04:00
|
|
|
|
ctx = _safe_ctx(or_id, entry)
|
|
|
|
|
|
if ctx is not None:
|
|
|
|
|
|
return ctx, "openrouter"
|
feat: overhaul context length detection with models.dev and provider-aware resolution (#2158)
Replace the fragile hardcoded context length system with a multi-source
resolution chain that correctly identifies context windows per provider.
Key changes:
- New agent/models_dev.py: Fetches and caches the models.dev registry
(3800+ models across 100+ providers with per-provider context windows).
In-memory cache (1hr TTL) + disk cache for cold starts.
- Rewritten get_model_context_length() resolution chain:
0. Config override (model.context_length)
1. Custom providers per-model context_length
2. Persistent disk cache
3. Endpoint /models (local servers)
4. Anthropic /v1/models API (max_input_tokens, API-key only)
5. OpenRouter live API (existing, unchanged)
6. Nous suffix-match via OpenRouter (dot/dash normalization)
7. models.dev registry lookup (provider-aware)
8. Thin hardcoded defaults (broad family patterns)
9. 128K fallback (was 2M)
- Provider-aware context: same model now correctly resolves to different
context windows per provider (e.g. claude-opus-4.6: 1M on Anthropic,
128K on GitHub Copilot). Provider name flows through ContextCompressor.
- DEFAULT_CONTEXT_LENGTHS shrunk from 80+ entries to ~16 broad patterns.
models.dev replaces the per-model hardcoding.
- CONTEXT_PROBE_TIERS changed from [2M, 1M, 512K, 200K, 128K, 64K, 32K]
to [128K, 64K, 32K, 16K, 8K]. Unknown models no longer start at 2M.
- hermes model: prompts for context_length when configuring custom
endpoints. Supports shorthand (32k, 128K). Saved to custom_providers
per-model config.
- custom_providers schema extended with optional models dict for
per-model context_length (backward compatible).
- Nous Portal: suffix-matches bare IDs (claude-opus-4-6) against
OpenRouter's prefixed IDs (anthropic/claude-opus-4.6) with dot/dash
normalization. Handles all 15 current Nous models.
- Anthropic direct: queries /v1/models for max_input_tokens. Only works
with regular API keys (sk-ant-api*), not OAuth tokens. Falls through
to models.dev for OAuth users.
Tests: 5574 passed (18 new tests for models_dev + updated probe tiers)
Docs: Updated configuration.md context length section, AGENTS.md
Co-authored-by: Test <test@test.com>
2026-03-20 06:04:33 -07:00
|
|
|
|
|
|
|
|
|
|
model_lower = model.lower()
|
|
|
|
|
|
for or_id, entry in metadata.items():
|
|
|
|
|
|
bare = or_id.split("/", 1)[1] if "/" in or_id else or_id
|
|
|
|
|
|
for candidate, query in [(bare.lower(), model_lower), (_normalize_model_version(bare).lower(), normalized)]:
|
|
|
|
|
|
if candidate.startswith(query) and (
|
|
|
|
|
|
len(candidate) == len(query) or candidate[len(query)] in "-:."
|
|
|
|
|
|
):
|
2026-05-12 14:59:31 -04:00
|
|
|
|
ctx = _safe_ctx(or_id, entry)
|
|
|
|
|
|
if ctx is not None:
|
|
|
|
|
|
return ctx, "openrouter"
|
feat: overhaul context length detection with models.dev and provider-aware resolution (#2158)
Replace the fragile hardcoded context length system with a multi-source
resolution chain that correctly identifies context windows per provider.
Key changes:
- New agent/models_dev.py: Fetches and caches the models.dev registry
(3800+ models across 100+ providers with per-provider context windows).
In-memory cache (1hr TTL) + disk cache for cold starts.
- Rewritten get_model_context_length() resolution chain:
0. Config override (model.context_length)
1. Custom providers per-model context_length
2. Persistent disk cache
3. Endpoint /models (local servers)
4. Anthropic /v1/models API (max_input_tokens, API-key only)
5. OpenRouter live API (existing, unchanged)
6. Nous suffix-match via OpenRouter (dot/dash normalization)
7. models.dev registry lookup (provider-aware)
8. Thin hardcoded defaults (broad family patterns)
9. 128K fallback (was 2M)
- Provider-aware context: same model now correctly resolves to different
context windows per provider (e.g. claude-opus-4.6: 1M on Anthropic,
128K on GitHub Copilot). Provider name flows through ContextCompressor.
- DEFAULT_CONTEXT_LENGTHS shrunk from 80+ entries to ~16 broad patterns.
models.dev replaces the per-model hardcoding.
- CONTEXT_PROBE_TIERS changed from [2M, 1M, 512K, 200K, 128K, 64K, 32K]
to [128K, 64K, 32K, 16K, 8K]. Unknown models no longer start at 2M.
- hermes model: prompts for context_length when configuring custom
endpoints. Supports shorthand (32k, 128K). Saved to custom_providers
per-model config.
- custom_providers schema extended with optional models dict for
per-model context_length (backward compatible).
- Nous Portal: suffix-matches bare IDs (claude-opus-4-6) against
OpenRouter's prefixed IDs (anthropic/claude-opus-4.6) with dot/dash
normalization. Handles all 15 current Nous models.
- Anthropic direct: queries /v1/models for max_input_tokens. Only works
with regular API keys (sk-ant-api*), not OAuth tokens. Falls through
to models.dev for OAuth users.
Tests: 5574 passed (18 new tests for models_dev + updated probe tiers)
Docs: Updated configuration.md context length section, AGENTS.md
Co-authored-by: Test <test@test.com>
2026-03-20 06:04:33 -07:00
|
|
|
|
|
2026-05-12 14:59:31 -04:00
|
|
|
|
return None, ""
|
feat: overhaul context length detection with models.dev and provider-aware resolution (#2158)
Replace the fragile hardcoded context length system with a multi-source
resolution chain that correctly identifies context windows per provider.
Key changes:
- New agent/models_dev.py: Fetches and caches the models.dev registry
(3800+ models across 100+ providers with per-provider context windows).
In-memory cache (1hr TTL) + disk cache for cold starts.
- Rewritten get_model_context_length() resolution chain:
0. Config override (model.context_length)
1. Custom providers per-model context_length
2. Persistent disk cache
3. Endpoint /models (local servers)
4. Anthropic /v1/models API (max_input_tokens, API-key only)
5. OpenRouter live API (existing, unchanged)
6. Nous suffix-match via OpenRouter (dot/dash normalization)
7. models.dev registry lookup (provider-aware)
8. Thin hardcoded defaults (broad family patterns)
9. 128K fallback (was 2M)
- Provider-aware context: same model now correctly resolves to different
context windows per provider (e.g. claude-opus-4.6: 1M on Anthropic,
128K on GitHub Copilot). Provider name flows through ContextCompressor.
- DEFAULT_CONTEXT_LENGTHS shrunk from 80+ entries to ~16 broad patterns.
models.dev replaces the per-model hardcoding.
- CONTEXT_PROBE_TIERS changed from [2M, 1M, 512K, 200K, 128K, 64K, 32K]
to [128K, 64K, 32K, 16K, 8K]. Unknown models no longer start at 2M.
- hermes model: prompts for context_length when configuring custom
endpoints. Supports shorthand (32k, 128K). Saved to custom_providers
per-model config.
- custom_providers schema extended with optional models dict for
per-model context_length (backward compatible).
- Nous Portal: suffix-matches bare IDs (claude-opus-4-6) against
OpenRouter's prefixed IDs (anthropic/claude-opus-4.6) with dot/dash
normalization. Handles all 15 current Nous models.
- Anthropic direct: queries /v1/models for max_input_tokens. Only works
with regular API keys (sk-ant-api*), not OAuth tokens. Falls through
to models.dev for OAuth users.
Tests: 5574 passed (18 new tests for models_dev + updated probe tiers)
Docs: Updated configuration.md context length section, AGENTS.md
Co-authored-by: Test <test@test.com>
2026-03-20 06:04:33 -07:00
|
|
|
|
|
|
|
|
|
|
|
2026-03-19 06:01:16 -07:00
|
|
|
|
def get_model_context_length(
|
|
|
|
|
|
model: str,
|
|
|
|
|
|
base_url: str = "",
|
|
|
|
|
|
api_key: str = "",
|
|
|
|
|
|
config_context_length: int | None = None,
|
feat: overhaul context length detection with models.dev and provider-aware resolution (#2158)
Replace the fragile hardcoded context length system with a multi-source
resolution chain that correctly identifies context windows per provider.
Key changes:
- New agent/models_dev.py: Fetches and caches the models.dev registry
(3800+ models across 100+ providers with per-provider context windows).
In-memory cache (1hr TTL) + disk cache for cold starts.
- Rewritten get_model_context_length() resolution chain:
0. Config override (model.context_length)
1. Custom providers per-model context_length
2. Persistent disk cache
3. Endpoint /models (local servers)
4. Anthropic /v1/models API (max_input_tokens, API-key only)
5. OpenRouter live API (existing, unchanged)
6. Nous suffix-match via OpenRouter (dot/dash normalization)
7. models.dev registry lookup (provider-aware)
8. Thin hardcoded defaults (broad family patterns)
9. 128K fallback (was 2M)
- Provider-aware context: same model now correctly resolves to different
context windows per provider (e.g. claude-opus-4.6: 1M on Anthropic,
128K on GitHub Copilot). Provider name flows through ContextCompressor.
- DEFAULT_CONTEXT_LENGTHS shrunk from 80+ entries to ~16 broad patterns.
models.dev replaces the per-model hardcoding.
- CONTEXT_PROBE_TIERS changed from [2M, 1M, 512K, 200K, 128K, 64K, 32K]
to [128K, 64K, 32K, 16K, 8K]. Unknown models no longer start at 2M.
- hermes model: prompts for context_length when configuring custom
endpoints. Supports shorthand (32k, 128K). Saved to custom_providers
per-model config.
- custom_providers schema extended with optional models dict for
per-model context_length (backward compatible).
- Nous Portal: suffix-matches bare IDs (claude-opus-4-6) against
OpenRouter's prefixed IDs (anthropic/claude-opus-4.6) with dot/dash
normalization. Handles all 15 current Nous models.
- Anthropic direct: queries /v1/models for max_input_tokens. Only works
with regular API keys (sk-ant-api*), not OAuth tokens. Falls through
to models.dev for OAuth users.
Tests: 5574 passed (18 new tests for models_dev + updated probe tiers)
Docs: Updated configuration.md context length section, AGENTS.md
Co-authored-by: Test <test@test.com>
2026-03-20 06:04:33 -07:00
|
|
|
|
provider: str = "",
|
fix(context): honor custom_providers context_length on /model switch + bump probe tier to 256K (#15844)
Fixes #15779. Custom-provider per-model context_length (`custom_providers[].models.<id>.context_length`) is now honored across every resolution path, not just agent startup. Also adds 256K as the top probe tier and default fallback.
## What changed
New helper `hermes_cli.config.get_custom_provider_context_length()` — single source of truth for the per-model override lookup, with trailing-slash-insensitive base-url matching.
`agent.model_metadata.get_model_context_length()` gains an optional `custom_providers=` kwarg (step 0b — runs after explicit `config_context_length` but before every other probe).
Wired through five call sites that previously either duplicated the lookup or ignored it entirely:
- `run_agent.py` startup — refactored to use the new helper (dedups legacy inline loop, keeps invalid-value warning)
- `AIAgent.switch_model()` — re-reads custom_providers from live config on every /model switch
- `hermes_cli.model_switch.resolve_display_context_length()` — new `custom_providers=` kwarg
- `gateway/run.py` /model confirmation (picker callback + text path)
- `gateway/run.py` `_format_session_info` (/info)
## Context probe tiers
`CONTEXT_PROBE_TIERS = [256_000, 128_000, 64_000, 32_000, 16_000, 8_000]` — was `[128_000, ...]`. `DEFAULT_FALLBACK_CONTEXT` follows tier[0], so unknown models now default to 256K. The stale `128000` literal in the OpenRouter metadata-miss path is replaced with `DEFAULT_FALLBACK_CONTEXT` for consistency.
## Repro (from #15779)
```yaml
custom_providers:
- name: my-custom-endpoint
base_url: https://example.invalid/v1
model: gpt-5.5
models:
gpt-5.5:
context_length: 1050000
```
`/model gpt-5.5 --provider custom:my-custom-endpoint` → previously "Context: 128,000", now "Context: 1,050,000".
## Tests
- `tests/hermes_cli/test_custom_provider_context_length.py` — new file, 19 tests covering the helper, step-0b integration, and the 256K tier invariants
- `tests/hermes_cli/test_model_switch_context_display.py` — added regression tests for #15779 through the display resolver
- `tests/gateway/test_session_info.py` — updated default-fallback assertion (128K → 256K)
- `tests/agent/test_model_metadata.py` — updated tier assertions for the new top tier
2026-04-25 18:47:53 -07:00
|
|
|
|
custom_providers: list | None = None,
|
2026-03-19 06:01:16 -07:00
|
|
|
|
) -> int:
|
2026-03-05 16:09:57 -08:00
|
|
|
|
"""Get the context length for a model.
|
|
|
|
|
|
|
|
|
|
|
|
Resolution order:
|
feat: overhaul context length detection with models.dev and provider-aware resolution (#2158)
Replace the fragile hardcoded context length system with a multi-source
resolution chain that correctly identifies context windows per provider.
Key changes:
- New agent/models_dev.py: Fetches and caches the models.dev registry
(3800+ models across 100+ providers with per-provider context windows).
In-memory cache (1hr TTL) + disk cache for cold starts.
- Rewritten get_model_context_length() resolution chain:
0. Config override (model.context_length)
1. Custom providers per-model context_length
2. Persistent disk cache
3. Endpoint /models (local servers)
4. Anthropic /v1/models API (max_input_tokens, API-key only)
5. OpenRouter live API (existing, unchanged)
6. Nous suffix-match via OpenRouter (dot/dash normalization)
7. models.dev registry lookup (provider-aware)
8. Thin hardcoded defaults (broad family patterns)
9. 128K fallback (was 2M)
- Provider-aware context: same model now correctly resolves to different
context windows per provider (e.g. claude-opus-4.6: 1M on Anthropic,
128K on GitHub Copilot). Provider name flows through ContextCompressor.
- DEFAULT_CONTEXT_LENGTHS shrunk from 80+ entries to ~16 broad patterns.
models.dev replaces the per-model hardcoding.
- CONTEXT_PROBE_TIERS changed from [2M, 1M, 512K, 200K, 128K, 64K, 32K]
to [128K, 64K, 32K, 16K, 8K]. Unknown models no longer start at 2M.
- hermes model: prompts for context_length when configuring custom
endpoints. Supports shorthand (32k, 128K). Saved to custom_providers
per-model config.
- custom_providers schema extended with optional models dict for
per-model context_length (backward compatible).
- Nous Portal: suffix-matches bare IDs (claude-opus-4-6) against
OpenRouter's prefixed IDs (anthropic/claude-opus-4.6) with dot/dash
normalization. Handles all 15 current Nous models.
- Anthropic direct: queries /v1/models for max_input_tokens. Only works
with regular API keys (sk-ant-api*), not OAuth tokens. Falls through
to models.dev for OAuth users.
Tests: 5574 passed (18 new tests for models_dev + updated probe tiers)
Docs: Updated configuration.md context length section, AGENTS.md
Co-authored-by: Test <test@test.com>
2026-03-20 06:04:33 -07:00
|
|
|
|
0. Explicit config override (model.context_length or custom_providers per-model)
|
2026-05-12 14:59:31 -04:00
|
|
|
|
1. Persistent cache (previously discovered via probing). Nous URLs
|
|
|
|
|
|
bypass the cache here so step 5b can always reconcile against
|
|
|
|
|
|
the authoritative portal /v1/models response.
|
fix(bedrock): resolve context length via static table before custom-endpoint probe
## Problem
`get_model_context_length()` in `agent/model_metadata.py` had a resolution
order bug that caused every Bedrock model to fall back to the 128K default
context length instead of reaching the static Bedrock table (200K for
Claude, etc.).
The root cause: `bedrock-runtime.<region>.amazonaws.com` is not listed in
`_URL_TO_PROVIDER`, so `_is_known_provider_base_url()` returned False.
The resolution order then ran the custom-endpoint probe (step 2) *before*
the Bedrock branch (step 4b), which:
1. Treated Bedrock as a custom endpoint (via `_is_custom_endpoint`).
2. Called `fetch_endpoint_model_metadata()` → `GET /models` on the
bedrock-runtime URL (Bedrock doesn't serve this shape).
3. Fell through to `return DEFAULT_FALLBACK_CONTEXT` (128K) at the
"probe-down" branch — never reaching the Bedrock static table.
Result: users on Bedrock saw 128K context for Claude models that
actually support 200K on Bedrock, causing premature auto-compression.
## Fix
Promote the Bedrock branch from step 4b to step 1b, so it runs *before*
the custom-endpoint probe at step 2. The static table in
`bedrock_adapter.py::get_bedrock_context_length()` is the authoritative
source for Bedrock (the ListFoundationModels API doesn't expose context
window sizes), so there's no reason to probe `/models` first.
The original step 4b is replaced with a one-line breadcrumb comment
pointing to the new location, to make the resolution-order docstring
accurate.
## Changes
- `agent/model_metadata.py`
- Add step 1b: Bedrock static-table branch (unchanged predicate, moved).
- Remove dead step 4b block, replace with breadcrumb comment.
- Update resolution-order docstring to include step 1b.
- `tests/agent/test_model_metadata.py`
- New `TestBedrockContextResolution` class (3 tests):
- `test_bedrock_provider_returns_static_table_before_probe`:
confirms `provider="bedrock"` hits the static table and does NOT
call `fetch_endpoint_model_metadata` (regression guard).
- `test_bedrock_url_without_provider_hint`: confirms the
`bedrock-runtime.*.amazonaws.com` host match works without an
explicit `provider=` hint.
- `test_non_bedrock_url_still_probes`: confirms the probe still
fires for genuinely-custom endpoints (no over-reach).
## Testing
pytest tests/agent/test_model_metadata.py -q
# 83 passed in 1.95s (3 new + 80 existing)
## Risk
Very low.
- Predicate is identical to the original step 4b — no behaviour change
for non-Bedrock paths.
- Original step 4b was dead code for the user-facing case (always hit
the 128K fallback first), so removing it cannot regress behaviour.
- Bedrock path now short-circuits before any network I/O — faster too.
- `ImportError` fall-through preserved so users without `boto3`
installed are unaffected.
## Related
- This is a prerequisite for accurate context-window accounting on
Bedrock — the fix for #14710 (stale-connection client eviction)
depends on correct context sizing to know when to compress.
Signed-off-by: Andre Kurait <andrekurait@gmail.com>
2026-04-23 20:33:09 +00:00
|
|
|
|
1b. AWS Bedrock static table (must precede custom-endpoint probe)
|
2026-03-18 03:04:07 -07:00
|
|
|
|
2. Active endpoint metadata (/models for explicit custom endpoints)
|
feat: overhaul context length detection with models.dev and provider-aware resolution (#2158)
Replace the fragile hardcoded context length system with a multi-source
resolution chain that correctly identifies context windows per provider.
Key changes:
- New agent/models_dev.py: Fetches and caches the models.dev registry
(3800+ models across 100+ providers with per-provider context windows).
In-memory cache (1hr TTL) + disk cache for cold starts.
- Rewritten get_model_context_length() resolution chain:
0. Config override (model.context_length)
1. Custom providers per-model context_length
2. Persistent disk cache
3. Endpoint /models (local servers)
4. Anthropic /v1/models API (max_input_tokens, API-key only)
5. OpenRouter live API (existing, unchanged)
6. Nous suffix-match via OpenRouter (dot/dash normalization)
7. models.dev registry lookup (provider-aware)
8. Thin hardcoded defaults (broad family patterns)
9. 128K fallback (was 2M)
- Provider-aware context: same model now correctly resolves to different
context windows per provider (e.g. claude-opus-4.6: 1M on Anthropic,
128K on GitHub Copilot). Provider name flows through ContextCompressor.
- DEFAULT_CONTEXT_LENGTHS shrunk from 80+ entries to ~16 broad patterns.
models.dev replaces the per-model hardcoding.
- CONTEXT_PROBE_TIERS changed from [2M, 1M, 512K, 200K, 128K, 64K, 32K]
to [128K, 64K, 32K, 16K, 8K]. Unknown models no longer start at 2M.
- hermes model: prompts for context_length when configuring custom
endpoints. Supports shorthand (32k, 128K). Saved to custom_providers
per-model config.
- custom_providers schema extended with optional models dict for
per-model context_length (backward compatible).
- Nous Portal: suffix-matches bare IDs (claude-opus-4-6) against
OpenRouter's prefixed IDs (anthropic/claude-opus-4.6) with dot/dash
normalization. Handles all 15 current Nous models.
- Anthropic direct: queries /v1/models for max_input_tokens. Only works
with regular API keys (sk-ant-api*), not OAuth tokens. Falls through
to models.dev for OAuth users.
Tests: 5574 passed (18 new tests for models_dev + updated probe tiers)
Docs: Updated configuration.md context length section, AGENTS.md
Co-authored-by: Test <test@test.com>
2026-03-20 06:04:33 -07:00
|
|
|
|
3. Local server query (for local endpoints)
|
|
|
|
|
|
4. Anthropic /v1/models API (API-key users only, not OAuth)
|
2026-05-12 01:05:25 +05:30
|
|
|
|
5. Provider-aware lookups (before generic OpenRouter cache):
|
|
|
|
|
|
a. Copilot live /models API
|
2026-05-12 14:59:31 -04:00
|
|
|
|
b. Nous: live /v1/models probe first (authoritative), then OR
|
|
|
|
|
|
cache fallback with suffix/version normalisation. Only
|
|
|
|
|
|
portal-derived values are persisted to disk.
|
2026-05-12 01:05:25 +05:30
|
|
|
|
c. Codex OAuth /models probe
|
|
|
|
|
|
d. GMI /models endpoint
|
|
|
|
|
|
e. Ollama native /api/show probe (any base_url, provider-agnostic)
|
|
|
|
|
|
f. models.dev registry lookup (with :cloud/-cloud suffix fallback)
|
|
|
|
|
|
6. OpenRouter live API metadata (Kimi-family 32k guard)
|
|
|
|
|
|
7. Hardcoded defaults (broad family patterns, longest-key-first)
|
|
|
|
|
|
8. Local server query (last resort)
|
|
|
|
|
|
9. Default fallback (256K)"""
|
2026-03-19 06:01:16 -07:00
|
|
|
|
# 0. Explicit config override — user knows best
|
|
|
|
|
|
if config_context_length is not None and isinstance(config_context_length, int) and config_context_length > 0:
|
|
|
|
|
|
return config_context_length
|
|
|
|
|
|
|
2026-06-27 12:08:09 -07:00
|
|
|
|
# 0a. MoA virtual provider — ``model`` is a preset name, not a real model,
|
|
|
|
|
|
# and ``base_url`` is the local virtual endpoint, so every probe below would
|
|
|
|
|
|
# miss and fall through to the 256K default. The aggregator is the acting
|
|
|
|
|
|
# model, so resolve the context window from the aggregator slot's real
|
|
|
|
|
|
# provider+model instead. References are advisory-only and never bound the
|
|
|
|
|
|
# acting context, so they're ignored here.
|
|
|
|
|
|
if (provider or "").strip().lower() == "moa":
|
|
|
|
|
|
try:
|
|
|
|
|
|
from hermes_cli.config import load_config
|
|
|
|
|
|
from hermes_cli.moa_config import resolve_moa_preset
|
|
|
|
|
|
from hermes_cli.runtime_provider import resolve_runtime_provider
|
|
|
|
|
|
|
|
|
|
|
|
preset = resolve_moa_preset(load_config().get("moa") or {}, model)
|
|
|
|
|
|
agg = preset.get("aggregator") or {}
|
|
|
|
|
|
agg_provider = str(agg.get("provider") or "").strip()
|
|
|
|
|
|
agg_model = str(agg.get("model") or "").strip()
|
|
|
|
|
|
if agg_model and agg_provider and agg_provider.lower() != "moa":
|
|
|
|
|
|
rt = resolve_runtime_provider(requested=agg_provider, target_model=agg_model)
|
|
|
|
|
|
return get_model_context_length(
|
|
|
|
|
|
agg_model,
|
|
|
|
|
|
base_url=rt.get("base_url", "") or "",
|
|
|
|
|
|
api_key=rt.get("api_key", "") or "",
|
|
|
|
|
|
provider=agg_provider,
|
|
|
|
|
|
)
|
|
|
|
|
|
except Exception:
|
|
|
|
|
|
logger.debug("MoA aggregator context-length resolution failed", exc_info=True)
|
|
|
|
|
|
# Fall through to the generic default if aggregator resolution failed.
|
|
|
|
|
|
|
fix(context): honor custom_providers context_length on /model switch + bump probe tier to 256K (#15844)
Fixes #15779. Custom-provider per-model context_length (`custom_providers[].models.<id>.context_length`) is now honored across every resolution path, not just agent startup. Also adds 256K as the top probe tier and default fallback.
## What changed
New helper `hermes_cli.config.get_custom_provider_context_length()` — single source of truth for the per-model override lookup, with trailing-slash-insensitive base-url matching.
`agent.model_metadata.get_model_context_length()` gains an optional `custom_providers=` kwarg (step 0b — runs after explicit `config_context_length` but before every other probe).
Wired through five call sites that previously either duplicated the lookup or ignored it entirely:
- `run_agent.py` startup — refactored to use the new helper (dedups legacy inline loop, keeps invalid-value warning)
- `AIAgent.switch_model()` — re-reads custom_providers from live config on every /model switch
- `hermes_cli.model_switch.resolve_display_context_length()` — new `custom_providers=` kwarg
- `gateway/run.py` /model confirmation (picker callback + text path)
- `gateway/run.py` `_format_session_info` (/info)
## Context probe tiers
`CONTEXT_PROBE_TIERS = [256_000, 128_000, 64_000, 32_000, 16_000, 8_000]` — was `[128_000, ...]`. `DEFAULT_FALLBACK_CONTEXT` follows tier[0], so unknown models now default to 256K. The stale `128000` literal in the OpenRouter metadata-miss path is replaced with `DEFAULT_FALLBACK_CONTEXT` for consistency.
## Repro (from #15779)
```yaml
custom_providers:
- name: my-custom-endpoint
base_url: https://example.invalid/v1
model: gpt-5.5
models:
gpt-5.5:
context_length: 1050000
```
`/model gpt-5.5 --provider custom:my-custom-endpoint` → previously "Context: 128,000", now "Context: 1,050,000".
## Tests
- `tests/hermes_cli/test_custom_provider_context_length.py` — new file, 19 tests covering the helper, step-0b integration, and the 256K tier invariants
- `tests/hermes_cli/test_model_switch_context_display.py` — added regression tests for #15779 through the display resolver
- `tests/gateway/test_session_info.py` — updated default-fallback assertion (128K → 256K)
- `tests/agent/test_model_metadata.py` — updated tier assertions for the new top tier
2026-04-25 18:47:53 -07:00
|
|
|
|
# 0b. custom_providers per-model override — check before any probe.
|
|
|
|
|
|
# This closes the gap where /model switch and display paths used to fall
|
|
|
|
|
|
# back to 128K despite the user having a per-model context_length set.
|
|
|
|
|
|
# See #15779.
|
|
|
|
|
|
if custom_providers and base_url and model:
|
|
|
|
|
|
try:
|
|
|
|
|
|
from hermes_cli.config import get_custom_provider_context_length
|
|
|
|
|
|
cp_ctx = get_custom_provider_context_length(
|
|
|
|
|
|
model=model,
|
|
|
|
|
|
base_url=base_url,
|
|
|
|
|
|
custom_providers=custom_providers,
|
|
|
|
|
|
)
|
|
|
|
|
|
if cp_ctx:
|
|
|
|
|
|
return cp_ctx
|
|
|
|
|
|
except Exception:
|
|
|
|
|
|
pass # fall through to probing
|
|
|
|
|
|
|
2026-03-18 22:00:53 +01:00
|
|
|
|
# Normalise provider-prefixed model names (e.g. "local:model-name" →
|
|
|
|
|
|
# "model-name") so cache lookups and server queries use the bare ID that
|
2026-03-20 03:19:31 -07:00
|
|
|
|
# local servers actually know about. Ollama "model:tag" colons are preserved.
|
|
|
|
|
|
model = _strip_provider_prefix(model)
|
2026-03-18 22:00:53 +01:00
|
|
|
|
|
2026-03-05 16:09:57 -08:00
|
|
|
|
# 1. Check persistent cache (model+provider)
|
2026-04-25 12:30:55 -04:00
|
|
|
|
# LM Studio is excluded — its loaded context length is transient (the
|
|
|
|
|
|
# user can reload the model with a different context_length at any time
|
|
|
|
|
|
# via /api/v1/models/load), so a stale cached value would mask reloads.
|
|
|
|
|
|
if base_url and provider != "lmstudio":
|
2026-03-05 16:09:57 -08:00
|
|
|
|
cached = get_cached_context_length(model, base_url)
|
|
|
|
|
|
if cached is not None:
|
fix(context): invalidate stale Codex OAuth cache entries >= 400k (#15078)
PR #14935 added a Codex-aware context resolver but only new lookups
hit the live /models probe. Users who had run Hermes on gpt-5.5 / 5.4
BEFORE that PR already had the wrong value (e.g. 1,050,000 from
models.dev) persisted in ~/.hermes/context_length_cache.yaml, and the
cache-first lookup in get_model_context_length() returns it forever.
Symptom (reported in the wild by Ludwig, min heo, Gaoge on current
main at 6051fba9d, which is AFTER #14935):
* Startup banner shows context usage against 1M
* Compression fires late and then OpenAI hard-rejects with
'context length will be reduced from 1,050,000 to 128,000'
around the real 272k boundary.
Fix: when the step-1 cache returns a value for an openai-codex lookup,
check whether it's >= 400k. Codex OAuth caps every slug at 272k (live
probe values) so anything at or above 400k is definitionally a
pre-#14935 leftover. Drop that entry from the on-disk cache and fall
through to step 5, which runs the live /models probe and repersists
the correct value (or 272k from the hardcoded fallback if the probe
fails). Non-Codex providers and legitimately-cached Codex entries at
272k are untouched.
Changes:
- agent/model_metadata.py:
* _invalidate_cached_context_length() — drop a single entry from
context_length_cache.yaml and rewrite the file.
* Step-1 cache check in get_model_context_length() now gates
provider=='openai-codex' entries >= 400k through invalidation
instead of returning them.
Tests (3 new in TestCodexOAuthContextLength):
- stale 1.05M Codex entry is dropped from disk AND re-resolved
through the live probe to 272k; unrelated cache entries survive.
- fresh 272k Codex entry is respected (no probe call, no invalidation).
- non-Codex 1M entries (e.g. anthropic/claude-opus-4.6 on OpenRouter)
are unaffected — the guard is strictly scoped to openai-codex.
Full tests/agent/test_model_metadata.py: 88 passed.
2026-04-24 04:46:07 -07:00
|
|
|
|
# Invalidate stale Codex OAuth cache entries: pre-PR #14935 builds
|
|
|
|
|
|
# resolved gpt-5.x to the direct-API value (e.g. 1.05M) via
|
|
|
|
|
|
# models.dev and persisted it. Codex OAuth caps at 272K for every
|
|
|
|
|
|
# slug, so any cached Codex entry at or above 400K is a leftover
|
|
|
|
|
|
# from the old resolution path. Drop it and fall through to the
|
|
|
|
|
|
# live /models probe in step 5 below.
|
|
|
|
|
|
if provider == "openai-codex" and cached >= 400_000:
|
|
|
|
|
|
logger.info(
|
|
|
|
|
|
"Dropping stale Codex cache entry %s@%s -> %s (pre-fix value); "
|
|
|
|
|
|
"re-resolving via live /models probe",
|
|
|
|
|
|
model, base_url, f"{cached:,}",
|
|
|
|
|
|
)
|
|
|
|
|
|
_invalidate_cached_context_length(model, base_url)
|
2026-05-12 00:24:01 -04:00
|
|
|
|
# Invalidate stale 32k cache entries for Kimi-family models.
|
|
|
|
|
|
elif cached <= 32768 and _model_name_suggests_kimi(model):
|
|
|
|
|
|
logger.info(
|
|
|
|
|
|
"Dropping stale Kimi cache entry %s@%s -> %s (OpenRouter underreport); "
|
|
|
|
|
|
"re-resolving via hardcoded defaults",
|
|
|
|
|
|
model, base_url, f"{cached:,}",
|
|
|
|
|
|
)
|
|
|
|
|
|
_invalidate_cached_context_length(model, base_url)
|
2026-06-01 14:59:07 -07:00
|
|
|
|
# Invalidate stale ≤204,800 cache entries for MiniMax-M3. Pre-catalog
|
|
|
|
|
|
# builds resolved M3 via the generic ``minimax`` catch-all (204,800)
|
|
|
|
|
|
# and persisted it before the ``minimax-m3`` (1M) entry existed; that
|
|
|
|
|
|
# stale value would otherwise stick forever here at step 1. M3 is 1M,
|
|
|
|
|
|
# so any sub-256K cached value for an M3 slug is a leftover — drop it
|
|
|
|
|
|
# and fall through to the hardcoded default.
|
|
|
|
|
|
elif cached <= 204_800 and _model_name_suggests_minimax_m3(model):
|
|
|
|
|
|
logger.info(
|
|
|
|
|
|
"Dropping stale MiniMax-M3 cache entry %s@%s -> %s (pre-catalog value); "
|
|
|
|
|
|
"re-resolving via hardcoded defaults",
|
|
|
|
|
|
model, base_url, f"{cached:,}",
|
|
|
|
|
|
)
|
|
|
|
|
|
_invalidate_cached_context_length(model, base_url)
|
2026-06-02 02:43:38 +03:00
|
|
|
|
# Invalidate stale ≤256,000 cache entries for Grok-4.3. The
|
|
|
|
|
|
# ``grok-4.3`` (1M) entry was added to DEFAULT_CONTEXT_LENGTHS on
|
|
|
|
|
|
# 2026-05-15; prior to that, grok-4.3 slugs resolved via the
|
|
|
|
|
|
# ``grok-4`` catch-all (256,000) and that value was persisted.
|
|
|
|
|
|
# grok-4.3 is 1M, so any sub-262K cached value is a pre-catalog
|
|
|
|
|
|
# leftover — drop it and fall through to the hardcoded default.
|
|
|
|
|
|
elif cached <= 256_000 and _model_name_suggests_grok_4_3(model):
|
|
|
|
|
|
logger.info(
|
|
|
|
|
|
"Dropping stale Grok-4.3 cache entry %s@%s -> %s (pre-catalog value); "
|
|
|
|
|
|
"re-resolving via hardcoded defaults",
|
|
|
|
|
|
model, base_url, f"{cached:,}",
|
|
|
|
|
|
)
|
|
|
|
|
|
_invalidate_cached_context_length(model, base_url)
|
2026-05-12 14:59:31 -04:00
|
|
|
|
# Nous Portal: the portal /v1/models endpoint is authoritative.
|
|
|
|
|
|
# Bypass the persistent cache so step 5b can always reconcile
|
|
|
|
|
|
# against it — this corrects pre-fix entries seeded from the
|
|
|
|
|
|
# OR catalog (the same OR underreport class that the Kimi/Qwen
|
|
|
|
|
|
# DEFAULT_CONTEXT_LENGTHS overrides exist to mitigate) without
|
|
|
|
|
|
# touching the on-disk file when the portal is unreachable.
|
|
|
|
|
|
# The in-memory 300s endpoint metadata cache makes the per-call
|
|
|
|
|
|
# cost amortise to ~0 within a process.
|
|
|
|
|
|
elif _infer_provider_from_url(base_url) == "nous":
|
|
|
|
|
|
logger.debug(
|
|
|
|
|
|
"Bypassing persistent cache for %s@%s (Nous portal authoritative)",
|
|
|
|
|
|
model, base_url,
|
|
|
|
|
|
)
|
|
|
|
|
|
# Fall through; step 5b reconciles and overwrites if portal responds.
|
fix(context): invalidate stale Codex OAuth cache entries >= 400k (#15078)
PR #14935 added a Codex-aware context resolver but only new lookups
hit the live /models probe. Users who had run Hermes on gpt-5.5 / 5.4
BEFORE that PR already had the wrong value (e.g. 1,050,000 from
models.dev) persisted in ~/.hermes/context_length_cache.yaml, and the
cache-first lookup in get_model_context_length() returns it forever.
Symptom (reported in the wild by Ludwig, min heo, Gaoge on current
main at 6051fba9d, which is AFTER #14935):
* Startup banner shows context usage against 1M
* Compression fires late and then OpenAI hard-rejects with
'context length will be reduced from 1,050,000 to 128,000'
around the real 272k boundary.
Fix: when the step-1 cache returns a value for an openai-codex lookup,
check whether it's >= 400k. Codex OAuth caps every slug at 272k (live
probe values) so anything at or above 400k is definitionally a
pre-#14935 leftover. Drop that entry from the on-disk cache and fall
through to step 5, which runs the live /models probe and repersists
the correct value (or 272k from the hardcoded fallback if the probe
fails). Non-Codex providers and legitimately-cached Codex entries at
272k are untouched.
Changes:
- agent/model_metadata.py:
* _invalidate_cached_context_length() — drop a single entry from
context_length_cache.yaml and rewrite the file.
* Step-1 cache check in get_model_context_length() now gates
provider=='openai-codex' entries >= 400k through invalidation
instead of returning them.
Tests (3 new in TestCodexOAuthContextLength):
- stale 1.05M Codex entry is dropped from disk AND re-resolved
through the live probe to 272k; unrelated cache entries survive.
- fresh 272k Codex entry is respected (no probe call, no invalidation).
- non-Codex 1M entries (e.g. anthropic/claude-opus-4.6 on OpenRouter)
are unaffected — the guard is strictly scoped to openai-codex.
Full tests/agent/test_model_metadata.py: 88 passed.
2026-04-24 04:46:07 -07:00
|
|
|
|
else:
|
|
|
|
|
|
return cached
|
2026-03-05 16:09:57 -08:00
|
|
|
|
|
fix(bedrock): resolve context length via static table before custom-endpoint probe
## Problem
`get_model_context_length()` in `agent/model_metadata.py` had a resolution
order bug that caused every Bedrock model to fall back to the 128K default
context length instead of reaching the static Bedrock table (200K for
Claude, etc.).
The root cause: `bedrock-runtime.<region>.amazonaws.com` is not listed in
`_URL_TO_PROVIDER`, so `_is_known_provider_base_url()` returned False.
The resolution order then ran the custom-endpoint probe (step 2) *before*
the Bedrock branch (step 4b), which:
1. Treated Bedrock as a custom endpoint (via `_is_custom_endpoint`).
2. Called `fetch_endpoint_model_metadata()` → `GET /models` on the
bedrock-runtime URL (Bedrock doesn't serve this shape).
3. Fell through to `return DEFAULT_FALLBACK_CONTEXT` (128K) at the
"probe-down" branch — never reaching the Bedrock static table.
Result: users on Bedrock saw 128K context for Claude models that
actually support 200K on Bedrock, causing premature auto-compression.
## Fix
Promote the Bedrock branch from step 4b to step 1b, so it runs *before*
the custom-endpoint probe at step 2. The static table in
`bedrock_adapter.py::get_bedrock_context_length()` is the authoritative
source for Bedrock (the ListFoundationModels API doesn't expose context
window sizes), so there's no reason to probe `/models` first.
The original step 4b is replaced with a one-line breadcrumb comment
pointing to the new location, to make the resolution-order docstring
accurate.
## Changes
- `agent/model_metadata.py`
- Add step 1b: Bedrock static-table branch (unchanged predicate, moved).
- Remove dead step 4b block, replace with breadcrumb comment.
- Update resolution-order docstring to include step 1b.
- `tests/agent/test_model_metadata.py`
- New `TestBedrockContextResolution` class (3 tests):
- `test_bedrock_provider_returns_static_table_before_probe`:
confirms `provider="bedrock"` hits the static table and does NOT
call `fetch_endpoint_model_metadata` (regression guard).
- `test_bedrock_url_without_provider_hint`: confirms the
`bedrock-runtime.*.amazonaws.com` host match works without an
explicit `provider=` hint.
- `test_non_bedrock_url_still_probes`: confirms the probe still
fires for genuinely-custom endpoints (no over-reach).
## Testing
pytest tests/agent/test_model_metadata.py -q
# 83 passed in 1.95s (3 new + 80 existing)
## Risk
Very low.
- Predicate is identical to the original step 4b — no behaviour change
for non-Bedrock paths.
- Original step 4b was dead code for the user-facing case (always hit
the 128K fallback first), so removing it cannot regress behaviour.
- Bedrock path now short-circuits before any network I/O — faster too.
- `ImportError` fall-through preserved so users without `boto3`
installed are unaffected.
## Related
- This is a prerequisite for accurate context-window accounting on
Bedrock — the fix for #14710 (stale-connection client eviction)
depends on correct context sizing to know when to compress.
Signed-off-by: Andre Kurait <andrekurait@gmail.com>
2026-04-23 20:33:09 +00:00
|
|
|
|
# 1b. AWS Bedrock — use static context length table.
|
|
|
|
|
|
# Bedrock's ListFoundationModels API doesn't expose context window sizes,
|
|
|
|
|
|
# so we maintain a curated table in bedrock_adapter.py that reflects
|
|
|
|
|
|
# AWS-imposed limits (e.g. 200K for Claude models vs 1M on the native
|
|
|
|
|
|
# Anthropic API). This must run BEFORE the custom-endpoint probe at
|
|
|
|
|
|
# step 2 — bedrock-runtime.<region>.amazonaws.com is not in
|
|
|
|
|
|
# _URL_TO_PROVIDER, so it would otherwise be treated as a custom endpoint,
|
|
|
|
|
|
# fail the /models probe (Bedrock doesn't expose that shape), and fall
|
|
|
|
|
|
# back to the 128K default before reaching the original step 4b branch.
|
|
|
|
|
|
if provider == "bedrock" or (
|
|
|
|
|
|
base_url
|
|
|
|
|
|
and base_url_hostname(base_url).startswith("bedrock-runtime.")
|
|
|
|
|
|
and base_url_host_matches(base_url, "amazonaws.com")
|
|
|
|
|
|
):
|
|
|
|
|
|
try:
|
|
|
|
|
|
from agent.bedrock_adapter import get_bedrock_context_length
|
|
|
|
|
|
return get_bedrock_context_length(model)
|
|
|
|
|
|
except ImportError:
|
|
|
|
|
|
pass # boto3 not installed — fall through to generic resolution
|
|
|
|
|
|
|
feat: add NovitaAI as LLM provider
Add NovitaAI as a first-class provider with dedicated model selection
flow, live pricing, and authoritative context length resolution.
- Register provider in PROVIDER_REGISTRY, HERMES_OVERLAYS, and all
alias/label maps (ID: novita, aliases: novita-ai, novitaai)
- Add dedicated _model_flow_novita() with 3-tier model list fallback:
Novita API → models.dev → static curated list
- Fetch live pricing from /v1/models with correct unit conversion
(input_token_price_per_m is 0.0001 USD per Mtok)
- Add Novita-specific context length resolution (step 4b) in
get_model_context_length(), prioritized over models.dev/OpenRouter
- Register api.novita.ai in _URL_TO_PROVIDER to prevent early return
from the custom-endpoint code path
- Add models.dev mapping (novita → novita-ai)
- Add default auxiliary model (deepseek/deepseek-v3-0324)
- Add NOVITA_API_KEY to test isolation (conftest.py)
- Update docs: providers page, env vars reference, CLI reference,
.env.example, README, and landing page
2026-04-10 22:22:47 +08:00
|
|
|
|
if provider == "novita" or (base_url and base_url_host_matches(base_url, "api.novita.ai")):
|
|
|
|
|
|
ctx = _resolve_endpoint_context_length(model, base_url or "https://api.novita.ai/openai/v1", api_key=api_key)
|
|
|
|
|
|
if ctx is not None:
|
|
|
|
|
|
if base_url:
|
|
|
|
|
|
save_context_length(model, base_url, ctx)
|
|
|
|
|
|
return ctx
|
|
|
|
|
|
|
2026-03-22 08:15:06 -07:00
|
|
|
|
# 2. Active endpoint metadata for truly custom/unknown endpoints.
|
|
|
|
|
|
# Known providers (Copilot, OpenAI, Anthropic, etc.) skip this — their
|
|
|
|
|
|
# /models endpoint may report a provider-imposed limit (e.g. Copilot
|
|
|
|
|
|
# returns 128k) instead of the model's full context (400k). models.dev
|
|
|
|
|
|
# has the correct per-provider values and is checked at step 5+.
|
|
|
|
|
|
if _is_custom_endpoint(base_url) and not _is_known_provider_base_url(base_url):
|
feat(providers): add GMI Cloud as a first-class API-key provider (#11955)
Add GMI Cloud (api.gmi-serving.com) as a full first-class API-key provider
with built-in auth, aliases, model catalog, CLI entry points, auxiliary client
routing, context length resolution, doctor checks, env var tracking, and docs.
- auth.py: ProviderConfig for 'gmi' (api_key, GMI_API_KEY / GMI_BASE_URL)
- providers.py: HermesOverlay with extra_env_vars for models.dev detection
- models.py: curated slash-form model catalog; live /v1/models fetch
- main.py: 'gmi' in _named_custom_provider_map and --provider choices
- model_metadata.py: _URL_TO_PROVIDER, _PROVIDER_PREFIXES, dedicated
context-length probe block (GMI's /models has authoritative data)
- auxiliary_client.py: alias entries; _compat_model fix for slash-form
models on cached aggregator-style clients; gmi aux default model
- doctor.py: GMI in provider connectivity checks
- config.py: GMI_API_KEY / GMI_BASE_URL in OPTIONAL_ENV_VARS
- conftest.py: explicit GMI_BASE_URL clearing (not caught by _API_KEY suffix)
- docs: providers.md, environment-variables.md, fallback-providers.md,
configuration.md, quickstart.md (expands provider table)
Co-authored-by: Isaac Huang <isaachuang@Isaacs-MacBook-Pro.local>
2026-04-17 11:19:56 -07:00
|
|
|
|
context_length = _resolve_endpoint_context_length(model, base_url, api_key=api_key)
|
|
|
|
|
|
if context_length is not None:
|
|
|
|
|
|
return context_length
|
2026-03-18 03:04:07 -07:00
|
|
|
|
if not _is_known_provider_base_url(base_url):
|
2026-05-12 01:05:25 +05:30
|
|
|
|
# 2b. Ollama native /api/show — any URL might be an Ollama server
|
|
|
|
|
|
# (local, cloud, or custom hosting). Non-Ollama servers return
|
|
|
|
|
|
# 404/405 quickly. Fall through on failure.
|
|
|
|
|
|
ctx = _query_ollama_api_show(model, base_url, api_key=api_key)
|
|
|
|
|
|
if ctx is not None:
|
|
|
|
|
|
save_context_length(model, base_url, ctx)
|
|
|
|
|
|
return ctx
|
feat: overhaul context length detection with models.dev and provider-aware resolution (#2158)
Replace the fragile hardcoded context length system with a multi-source
resolution chain that correctly identifies context windows per provider.
Key changes:
- New agent/models_dev.py: Fetches and caches the models.dev registry
(3800+ models across 100+ providers with per-provider context windows).
In-memory cache (1hr TTL) + disk cache for cold starts.
- Rewritten get_model_context_length() resolution chain:
0. Config override (model.context_length)
1. Custom providers per-model context_length
2. Persistent disk cache
3. Endpoint /models (local servers)
4. Anthropic /v1/models API (max_input_tokens, API-key only)
5. OpenRouter live API (existing, unchanged)
6. Nous suffix-match via OpenRouter (dot/dash normalization)
7. models.dev registry lookup (provider-aware)
8. Thin hardcoded defaults (broad family patterns)
9. 128K fallback (was 2M)
- Provider-aware context: same model now correctly resolves to different
context windows per provider (e.g. claude-opus-4.6: 1M on Anthropic,
128K on GitHub Copilot). Provider name flows through ContextCompressor.
- DEFAULT_CONTEXT_LENGTHS shrunk from 80+ entries to ~16 broad patterns.
models.dev replaces the per-model hardcoding.
- CONTEXT_PROBE_TIERS changed from [2M, 1M, 512K, 200K, 128K, 64K, 32K]
to [128K, 64K, 32K, 16K, 8K]. Unknown models no longer start at 2M.
- hermes model: prompts for context_length when configuring custom
endpoints. Supports shorthand (32k, 128K). Saved to custom_providers
per-model config.
- custom_providers schema extended with optional models dict for
per-model context_length (backward compatible).
- Nous Portal: suffix-matches bare IDs (claude-opus-4-6) against
OpenRouter's prefixed IDs (anthropic/claude-opus-4.6) with dot/dash
normalization. Handles all 15 current Nous models.
- Anthropic direct: queries /v1/models for max_input_tokens. Only works
with regular API keys (sk-ant-api*), not OAuth tokens. Falls through
to models.dev for OAuth users.
Tests: 5574 passed (18 new tests for models_dev + updated probe tiers)
Docs: Updated configuration.md context length section, AGENTS.md
Co-authored-by: Test <test@test.com>
2026-03-20 06:04:33 -07:00
|
|
|
|
# 3. Try querying local server directly
|
2026-03-18 21:38:41 +01:00
|
|
|
|
if is_local_endpoint(base_url):
|
2026-04-20 20:49:44 -07:00
|
|
|
|
local_ctx = _query_local_context_length(model, base_url, api_key=api_key)
|
2026-03-18 21:38:41 +01:00
|
|
|
|
if local_ctx and local_ctx > 0:
|
2026-04-25 12:30:55 -04:00
|
|
|
|
if provider != "lmstudio":
|
|
|
|
|
|
save_context_length(model, base_url, local_ctx)
|
2026-03-18 21:38:41 +01:00
|
|
|
|
return local_ctx
|
2026-03-19 06:01:16 -07:00
|
|
|
|
logger.info(
|
|
|
|
|
|
"Could not detect context length for model %r at %s — "
|
|
|
|
|
|
"defaulting to %s tokens (probe-down). Set model.context_length "
|
|
|
|
|
|
"in config.yaml to override.",
|
feat: overhaul context length detection with models.dev and provider-aware resolution (#2158)
Replace the fragile hardcoded context length system with a multi-source
resolution chain that correctly identifies context windows per provider.
Key changes:
- New agent/models_dev.py: Fetches and caches the models.dev registry
(3800+ models across 100+ providers with per-provider context windows).
In-memory cache (1hr TTL) + disk cache for cold starts.
- Rewritten get_model_context_length() resolution chain:
0. Config override (model.context_length)
1. Custom providers per-model context_length
2. Persistent disk cache
3. Endpoint /models (local servers)
4. Anthropic /v1/models API (max_input_tokens, API-key only)
5. OpenRouter live API (existing, unchanged)
6. Nous suffix-match via OpenRouter (dot/dash normalization)
7. models.dev registry lookup (provider-aware)
8. Thin hardcoded defaults (broad family patterns)
9. 128K fallback (was 2M)
- Provider-aware context: same model now correctly resolves to different
context windows per provider (e.g. claude-opus-4.6: 1M on Anthropic,
128K on GitHub Copilot). Provider name flows through ContextCompressor.
- DEFAULT_CONTEXT_LENGTHS shrunk from 80+ entries to ~16 broad patterns.
models.dev replaces the per-model hardcoding.
- CONTEXT_PROBE_TIERS changed from [2M, 1M, 512K, 200K, 128K, 64K, 32K]
to [128K, 64K, 32K, 16K, 8K]. Unknown models no longer start at 2M.
- hermes model: prompts for context_length when configuring custom
endpoints. Supports shorthand (32k, 128K). Saved to custom_providers
per-model config.
- custom_providers schema extended with optional models dict for
per-model context_length (backward compatible).
- Nous Portal: suffix-matches bare IDs (claude-opus-4-6) against
OpenRouter's prefixed IDs (anthropic/claude-opus-4.6) with dot/dash
normalization. Handles all 15 current Nous models.
- Anthropic direct: queries /v1/models for max_input_tokens. Only works
with regular API keys (sk-ant-api*), not OAuth tokens. Falls through
to models.dev for OAuth users.
Tests: 5574 passed (18 new tests for models_dev + updated probe tiers)
Docs: Updated configuration.md context length section, AGENTS.md
Co-authored-by: Test <test@test.com>
2026-03-20 06:04:33 -07:00
|
|
|
|
model, base_url, f"{DEFAULT_FALLBACK_CONTEXT:,}",
|
2026-03-19 06:01:16 -07:00
|
|
|
|
)
|
2026-06-04 16:19:24 +00:00
|
|
|
|
# 3b. Before falling back to the hard 256K default, consult the
|
|
|
|
|
|
# hardcoded catalog as a last resort. A proxied/custom Anthropic
|
|
|
|
|
|
# gateway (e.g. corporate proxy) fails the Ollama/local probes
|
|
|
|
|
|
# above, but the model name may still match an entry in
|
|
|
|
|
|
# DEFAULT_CONTEXT_LENGTHS (e.g. "claude-opus-4-8" → 1M).
|
|
|
|
|
|
# Without this, the early return here short-circuits the catalog
|
|
|
|
|
|
# lookup at step 8 and silently caps context at 256K.
|
|
|
|
|
|
model_lower = model.lower()
|
|
|
|
|
|
for default_model, length in sorted(
|
|
|
|
|
|
DEFAULT_CONTEXT_LENGTHS.items(),
|
|
|
|
|
|
key=lambda x: len(x[0]),
|
|
|
|
|
|
reverse=True,
|
|
|
|
|
|
):
|
|
|
|
|
|
if default_model in model_lower:
|
|
|
|
|
|
logger.info(
|
|
|
|
|
|
"Using hardcoded context length %s for model %r "
|
|
|
|
|
|
"(custom endpoint, catalog match on %r)",
|
|
|
|
|
|
f"{length:,}", model, default_model,
|
|
|
|
|
|
)
|
|
|
|
|
|
return length
|
feat: overhaul context length detection with models.dev and provider-aware resolution (#2158)
Replace the fragile hardcoded context length system with a multi-source
resolution chain that correctly identifies context windows per provider.
Key changes:
- New agent/models_dev.py: Fetches and caches the models.dev registry
(3800+ models across 100+ providers with per-provider context windows).
In-memory cache (1hr TTL) + disk cache for cold starts.
- Rewritten get_model_context_length() resolution chain:
0. Config override (model.context_length)
1. Custom providers per-model context_length
2. Persistent disk cache
3. Endpoint /models (local servers)
4. Anthropic /v1/models API (max_input_tokens, API-key only)
5. OpenRouter live API (existing, unchanged)
6. Nous suffix-match via OpenRouter (dot/dash normalization)
7. models.dev registry lookup (provider-aware)
8. Thin hardcoded defaults (broad family patterns)
9. 128K fallback (was 2M)
- Provider-aware context: same model now correctly resolves to different
context windows per provider (e.g. claude-opus-4.6: 1M on Anthropic,
128K on GitHub Copilot). Provider name flows through ContextCompressor.
- DEFAULT_CONTEXT_LENGTHS shrunk from 80+ entries to ~16 broad patterns.
models.dev replaces the per-model hardcoding.
- CONTEXT_PROBE_TIERS changed from [2M, 1M, 512K, 200K, 128K, 64K, 32K]
to [128K, 64K, 32K, 16K, 8K]. Unknown models no longer start at 2M.
- hermes model: prompts for context_length when configuring custom
endpoints. Supports shorthand (32k, 128K). Saved to custom_providers
per-model config.
- custom_providers schema extended with optional models dict for
per-model context_length (backward compatible).
- Nous Portal: suffix-matches bare IDs (claude-opus-4-6) against
OpenRouter's prefixed IDs (anthropic/claude-opus-4.6) with dot/dash
normalization. Handles all 15 current Nous models.
- Anthropic direct: queries /v1/models for max_input_tokens. Only works
with regular API keys (sk-ant-api*), not OAuth tokens. Falls through
to models.dev for OAuth users.
Tests: 5574 passed (18 new tests for models_dev + updated probe tiers)
Docs: Updated configuration.md context length section, AGENTS.md
Co-authored-by: Test <test@test.com>
2026-03-20 06:04:33 -07:00
|
|
|
|
return DEFAULT_FALLBACK_CONTEXT
|
2026-03-18 03:04:07 -07:00
|
|
|
|
|
feat: overhaul context length detection with models.dev and provider-aware resolution (#2158)
Replace the fragile hardcoded context length system with a multi-source
resolution chain that correctly identifies context windows per provider.
Key changes:
- New agent/models_dev.py: Fetches and caches the models.dev registry
(3800+ models across 100+ providers with per-provider context windows).
In-memory cache (1hr TTL) + disk cache for cold starts.
- Rewritten get_model_context_length() resolution chain:
0. Config override (model.context_length)
1. Custom providers per-model context_length
2. Persistent disk cache
3. Endpoint /models (local servers)
4. Anthropic /v1/models API (max_input_tokens, API-key only)
5. OpenRouter live API (existing, unchanged)
6. Nous suffix-match via OpenRouter (dot/dash normalization)
7. models.dev registry lookup (provider-aware)
8. Thin hardcoded defaults (broad family patterns)
9. 128K fallback (was 2M)
- Provider-aware context: same model now correctly resolves to different
context windows per provider (e.g. claude-opus-4.6: 1M on Anthropic,
128K on GitHub Copilot). Provider name flows through ContextCompressor.
- DEFAULT_CONTEXT_LENGTHS shrunk from 80+ entries to ~16 broad patterns.
models.dev replaces the per-model hardcoding.
- CONTEXT_PROBE_TIERS changed from [2M, 1M, 512K, 200K, 128K, 64K, 32K]
to [128K, 64K, 32K, 16K, 8K]. Unknown models no longer start at 2M.
- hermes model: prompts for context_length when configuring custom
endpoints. Supports shorthand (32k, 128K). Saved to custom_providers
per-model config.
- custom_providers schema extended with optional models dict for
per-model context_length (backward compatible).
- Nous Portal: suffix-matches bare IDs (claude-opus-4-6) against
OpenRouter's prefixed IDs (anthropic/claude-opus-4.6) with dot/dash
normalization. Handles all 15 current Nous models.
- Anthropic direct: queries /v1/models for max_input_tokens. Only works
with regular API keys (sk-ant-api*), not OAuth tokens. Falls through
to models.dev for OAuth users.
Tests: 5574 passed (18 new tests for models_dev + updated probe tiers)
Docs: Updated configuration.md context length section, AGENTS.md
Co-authored-by: Test <test@test.com>
2026-03-20 06:04:33 -07:00
|
|
|
|
# 4. Anthropic /v1/models API (only for regular API keys, not OAuth)
|
|
|
|
|
|
if provider == "anthropic" or (
|
fix: extend hostname-match provider detection across remaining call sites
Aslaaen's fix in the original PR covered _detect_api_mode_for_url and the
two openai/xai sites in run_agent.py. This finishes the sweep: the same
substring-match false-positive class (e.g. https://api.openai.com.evil/v1,
https://proxy/api.openai.com/v1, https://api.anthropic.com.example/v1)
existed in eight more call sites, and the hostname helper was duplicated
in two modules.
- utils: add shared base_url_hostname() (single source of truth).
- hermes_cli/runtime_provider, run_agent: drop local duplicates, import
from utils. Reuse the cached AIAgent._base_url_hostname attribute
everywhere it's already populated.
- agent/auxiliary_client: switch codex-wrap auto-detect, max_completion_tokens
gate (auxiliary_max_tokens_param), and custom-endpoint max_tokens kwarg
selection to hostname equality.
- run_agent: native-anthropic check in the Claude-style model branch
and in the AIAgent init provider-auto-detect branch.
- agent/model_metadata: Anthropic /v1/models context-length lookup.
- hermes_cli/providers.determine_api_mode: anthropic / openai URL
heuristics for custom/unknown providers (the /anthropic path-suffix
convention for third-party gateways is preserved).
- tools/delegate_tool: anthropic detection for delegated subagent
runtimes.
- hermes_cli/setup, hermes_cli/tools_config: setup-wizard vision-endpoint
native-OpenAI detection (paired with deduping the repeated check into
a single is_native_openai boolean per branch).
Tests:
- tests/test_base_url_hostname.py covers the helper directly
(path-containing-host, host-suffix, trailing dot, port, case).
- tests/hermes_cli/test_determine_api_mode_hostname.py adds the same
regression class for determine_api_mode, plus a test that the
/anthropic third-party gateway convention still wins.
Also: add asslaenn5@gmail.com → Aslaaen to scripts/release.py AUTHOR_MAP.
2026-04-20 20:58:01 -07:00
|
|
|
|
base_url and base_url_hostname(base_url) == "api.anthropic.com"
|
feat: overhaul context length detection with models.dev and provider-aware resolution (#2158)
Replace the fragile hardcoded context length system with a multi-source
resolution chain that correctly identifies context windows per provider.
Key changes:
- New agent/models_dev.py: Fetches and caches the models.dev registry
(3800+ models across 100+ providers with per-provider context windows).
In-memory cache (1hr TTL) + disk cache for cold starts.
- Rewritten get_model_context_length() resolution chain:
0. Config override (model.context_length)
1. Custom providers per-model context_length
2. Persistent disk cache
3. Endpoint /models (local servers)
4. Anthropic /v1/models API (max_input_tokens, API-key only)
5. OpenRouter live API (existing, unchanged)
6. Nous suffix-match via OpenRouter (dot/dash normalization)
7. models.dev registry lookup (provider-aware)
8. Thin hardcoded defaults (broad family patterns)
9. 128K fallback (was 2M)
- Provider-aware context: same model now correctly resolves to different
context windows per provider (e.g. claude-opus-4.6: 1M on Anthropic,
128K on GitHub Copilot). Provider name flows through ContextCompressor.
- DEFAULT_CONTEXT_LENGTHS shrunk from 80+ entries to ~16 broad patterns.
models.dev replaces the per-model hardcoding.
- CONTEXT_PROBE_TIERS changed from [2M, 1M, 512K, 200K, 128K, 64K, 32K]
to [128K, 64K, 32K, 16K, 8K]. Unknown models no longer start at 2M.
- hermes model: prompts for context_length when configuring custom
endpoints. Supports shorthand (32k, 128K). Saved to custom_providers
per-model config.
- custom_providers schema extended with optional models dict for
per-model context_length (backward compatible).
- Nous Portal: suffix-matches bare IDs (claude-opus-4-6) against
OpenRouter's prefixed IDs (anthropic/claude-opus-4.6) with dot/dash
normalization. Handles all 15 current Nous models.
- Anthropic direct: queries /v1/models for max_input_tokens. Only works
with regular API keys (sk-ant-api*), not OAuth tokens. Falls through
to models.dev for OAuth users.
Tests: 5574 passed (18 new tests for models_dev + updated probe tiers)
Docs: Updated configuration.md context length section, AGENTS.md
Co-authored-by: Test <test@test.com>
2026-03-20 06:04:33 -07:00
|
|
|
|
):
|
|
|
|
|
|
ctx = _query_anthropic_context_length(model, base_url or "https://api.anthropic.com", api_key)
|
|
|
|
|
|
if ctx:
|
|
|
|
|
|
return ctx
|
|
|
|
|
|
|
fix(bedrock): resolve context length via static table before custom-endpoint probe
## Problem
`get_model_context_length()` in `agent/model_metadata.py` had a resolution
order bug that caused every Bedrock model to fall back to the 128K default
context length instead of reaching the static Bedrock table (200K for
Claude, etc.).
The root cause: `bedrock-runtime.<region>.amazonaws.com` is not listed in
`_URL_TO_PROVIDER`, so `_is_known_provider_base_url()` returned False.
The resolution order then ran the custom-endpoint probe (step 2) *before*
the Bedrock branch (step 4b), which:
1. Treated Bedrock as a custom endpoint (via `_is_custom_endpoint`).
2. Called `fetch_endpoint_model_metadata()` → `GET /models` on the
bedrock-runtime URL (Bedrock doesn't serve this shape).
3. Fell through to `return DEFAULT_FALLBACK_CONTEXT` (128K) at the
"probe-down" branch — never reaching the Bedrock static table.
Result: users on Bedrock saw 128K context for Claude models that
actually support 200K on Bedrock, causing premature auto-compression.
## Fix
Promote the Bedrock branch from step 4b to step 1b, so it runs *before*
the custom-endpoint probe at step 2. The static table in
`bedrock_adapter.py::get_bedrock_context_length()` is the authoritative
source for Bedrock (the ListFoundationModels API doesn't expose context
window sizes), so there's no reason to probe `/models` first.
The original step 4b is replaced with a one-line breadcrumb comment
pointing to the new location, to make the resolution-order docstring
accurate.
## Changes
- `agent/model_metadata.py`
- Add step 1b: Bedrock static-table branch (unchanged predicate, moved).
- Remove dead step 4b block, replace with breadcrumb comment.
- Update resolution-order docstring to include step 1b.
- `tests/agent/test_model_metadata.py`
- New `TestBedrockContextResolution` class (3 tests):
- `test_bedrock_provider_returns_static_table_before_probe`:
confirms `provider="bedrock"` hits the static table and does NOT
call `fetch_endpoint_model_metadata` (regression guard).
- `test_bedrock_url_without_provider_hint`: confirms the
`bedrock-runtime.*.amazonaws.com` host match works without an
explicit `provider=` hint.
- `test_non_bedrock_url_still_probes`: confirms the probe still
fires for genuinely-custom endpoints (no over-reach).
## Testing
pytest tests/agent/test_model_metadata.py -q
# 83 passed in 1.95s (3 new + 80 existing)
## Risk
Very low.
- Predicate is identical to the original step 4b — no behaviour change
for non-Bedrock paths.
- Original step 4b was dead code for the user-facing case (always hit
the 128K fallback first), so removing it cannot regress behaviour.
- Bedrock path now short-circuits before any network I/O — faster too.
- `ImportError` fall-through preserved so users without `boto3`
installed are unaffected.
## Related
- This is a prerequisite for accurate context-window accounting on
Bedrock — the fix for #14710 (stale-connection client eviction)
depends on correct context sizing to know when to compress.
Signed-off-by: Andre Kurait <andrekurait@gmail.com>
2026-04-23 20:33:09 +00:00
|
|
|
|
# 4b. (Bedrock handled earlier at step 1b — before custom-endpoint probe.)
|
feat: native AWS Bedrock provider via Converse API
Salvaged from PR #7920 by JiaDe-Wu — cherry-picked Bedrock-specific
additions onto current main, skipping stale-branch reverts (293 commits
behind).
Dual-path architecture:
- Claude models → AnthropicBedrock SDK (prompt caching, thinking budgets)
- Non-Claude models → Converse API via boto3 (Nova, DeepSeek, Llama, Mistral)
Includes:
- Core adapter (agent/bedrock_adapter.py, 1098 lines)
- Full provider registration (auth, models, providers, config, runtime, main)
- IAM credential chain + Bedrock API Key auth modes
- Dynamic model discovery via ListFoundationModels + ListInferenceProfiles
- Streaming with delta callbacks, error classification, guardrails
- hermes doctor + hermes auth integration
- /usage pricing for 7 Bedrock models
- 130 automated tests (79 unit + 28 integration + follow-up fixes)
- Documentation (website/docs/guides/aws-bedrock.md)
- boto3 optional dependency (pip install hermes-agent[bedrock])
Co-authored-by: JiaDe WU <40445668+JiaDe-Wu@users.noreply.github.com>
2026-04-15 15:18:01 -07:00
|
|
|
|
|
feat: overhaul context length detection with models.dev and provider-aware resolution (#2158)
Replace the fragile hardcoded context length system with a multi-source
resolution chain that correctly identifies context windows per provider.
Key changes:
- New agent/models_dev.py: Fetches and caches the models.dev registry
(3800+ models across 100+ providers with per-provider context windows).
In-memory cache (1hr TTL) + disk cache for cold starts.
- Rewritten get_model_context_length() resolution chain:
0. Config override (model.context_length)
1. Custom providers per-model context_length
2. Persistent disk cache
3. Endpoint /models (local servers)
4. Anthropic /v1/models API (max_input_tokens, API-key only)
5. OpenRouter live API (existing, unchanged)
6. Nous suffix-match via OpenRouter (dot/dash normalization)
7. models.dev registry lookup (provider-aware)
8. Thin hardcoded defaults (broad family patterns)
9. 128K fallback (was 2M)
- Provider-aware context: same model now correctly resolves to different
context windows per provider (e.g. claude-opus-4.6: 1M on Anthropic,
128K on GitHub Copilot). Provider name flows through ContextCompressor.
- DEFAULT_CONTEXT_LENGTHS shrunk from 80+ entries to ~16 broad patterns.
models.dev replaces the per-model hardcoding.
- CONTEXT_PROBE_TIERS changed from [2M, 1M, 512K, 200K, 128K, 64K, 32K]
to [128K, 64K, 32K, 16K, 8K]. Unknown models no longer start at 2M.
- hermes model: prompts for context_length when configuring custom
endpoints. Supports shorthand (32k, 128K). Saved to custom_providers
per-model config.
- custom_providers schema extended with optional models dict for
per-model context_length (backward compatible).
- Nous Portal: suffix-matches bare IDs (claude-opus-4-6) against
OpenRouter's prefixed IDs (anthropic/claude-opus-4.6) with dot/dash
normalization. Handles all 15 current Nous models.
- Anthropic direct: queries /v1/models for max_input_tokens. Only works
with regular API keys (sk-ant-api*), not OAuth tokens. Falls through
to models.dev for OAuth users.
Tests: 5574 passed (18 new tests for models_dev + updated probe tiers)
Docs: Updated configuration.md context length section, AGENTS.md
Co-authored-by: Test <test@test.com>
2026-03-20 06:04:33 -07:00
|
|
|
|
# 5. Provider-aware lookups (before generic OpenRouter cache)
|
|
|
|
|
|
# These are provider-specific and take priority over the generic OR cache,
|
|
|
|
|
|
# since the same model can have different context limits per provider
|
|
|
|
|
|
# (e.g. claude-opus-4.6 is 1M on Anthropic but 128K on GitHub Copilot).
|
fix: infer provider from base URL for models.dev context length lookup
Custom endpoint users (DashScope/Alibaba, Z.AI, Kimi, DeepSeek, etc.)
get wrong context lengths because their provider resolves as "openrouter"
or "custom", skipping the models.dev lookup entirely. For example,
qwen3.5-plus on DashScope falls to the generic "qwen" hardcoded default
(131K) instead of the correct 1M.
Add _infer_provider_from_url() that maps known API hostnames to their
models.dev provider IDs. When the explicit provider is generic
(openrouter/custom/empty), infer from the base URL before the models.dev
lookup. This resolves context lengths correctly for DashScope, Z.AI,
Kimi, MiniMax, DeepSeek, and Nous endpoints without requiring users to
manually set context_length in config.
Also refactors _is_known_provider_base_url() to use the same URL mapping,
removing the duplicated hostname list.
2026-03-20 11:57:24 -07:00
|
|
|
|
# If provider is generic (openrouter/custom/empty), try to infer from URL.
|
|
|
|
|
|
effective_provider = provider
|
2026-05-11 11:13:25 -07:00
|
|
|
|
if not effective_provider or effective_provider in {"openrouter", "custom"}:
|
fix: infer provider from base URL for models.dev context length lookup
Custom endpoint users (DashScope/Alibaba, Z.AI, Kimi, DeepSeek, etc.)
get wrong context lengths because their provider resolves as "openrouter"
or "custom", skipping the models.dev lookup entirely. For example,
qwen3.5-plus on DashScope falls to the generic "qwen" hardcoded default
(131K) instead of the correct 1M.
Add _infer_provider_from_url() that maps known API hostnames to their
models.dev provider IDs. When the explicit provider is generic
(openrouter/custom/empty), infer from the base URL before the models.dev
lookup. This resolves context lengths correctly for DashScope, Z.AI,
Kimi, MiniMax, DeepSeek, and Nous endpoints without requiring users to
manually set context_length in config.
Also refactors _is_known_provider_base_url() to use the same URL mapping,
removing the duplicated hostname list.
2026-03-20 11:57:24 -07:00
|
|
|
|
if base_url:
|
|
|
|
|
|
inferred = _infer_provider_from_url(base_url)
|
|
|
|
|
|
if inferred:
|
|
|
|
|
|
effective_provider = inferred
|
|
|
|
|
|
|
2026-04-20 05:20:55 +00:00
|
|
|
|
# 5a. Copilot live /models API — max_prompt_tokens from the user's account.
|
|
|
|
|
|
# This catches account-specific models (e.g. claude-opus-4.6-1m) that
|
|
|
|
|
|
# don't exist in models.dev. For models that ARE in models.dev, this
|
|
|
|
|
|
# returns the provider-enforced limit which is what users can actually use.
|
2026-05-11 11:13:25 -07:00
|
|
|
|
if effective_provider in {"copilot", "copilot-acp", "github-copilot"}:
|
2026-04-20 05:20:55 +00:00
|
|
|
|
try:
|
|
|
|
|
|
from hermes_cli.models import get_copilot_model_context
|
|
|
|
|
|
ctx = get_copilot_model_context(model, api_key=api_key)
|
|
|
|
|
|
if ctx:
|
|
|
|
|
|
return ctx
|
|
|
|
|
|
except Exception:
|
|
|
|
|
|
pass # Fall through to models.dev
|
|
|
|
|
|
|
fix: infer provider from base URL for models.dev context length lookup
Custom endpoint users (DashScope/Alibaba, Z.AI, Kimi, DeepSeek, etc.)
get wrong context lengths because their provider resolves as "openrouter"
or "custom", skipping the models.dev lookup entirely. For example,
qwen3.5-plus on DashScope falls to the generic "qwen" hardcoded default
(131K) instead of the correct 1M.
Add _infer_provider_from_url() that maps known API hostnames to their
models.dev provider IDs. When the explicit provider is generic
(openrouter/custom/empty), infer from the base URL before the models.dev
lookup. This resolves context lengths correctly for DashScope, Z.AI,
Kimi, MiniMax, DeepSeek, and Nous endpoints without requiring users to
manually set context_length in config.
Also refactors _is_known_provider_base_url() to use the same URL mapping,
removing the duplicated hostname list.
2026-03-20 11:57:24 -07:00
|
|
|
|
if effective_provider == "nous":
|
2026-05-12 14:59:31 -04:00
|
|
|
|
ctx, source = _resolve_nous_context_length(
|
|
|
|
|
|
model, base_url=base_url or "", api_key=api_key or ""
|
|
|
|
|
|
)
|
feat: overhaul context length detection with models.dev and provider-aware resolution (#2158)
Replace the fragile hardcoded context length system with a multi-source
resolution chain that correctly identifies context windows per provider.
Key changes:
- New agent/models_dev.py: Fetches and caches the models.dev registry
(3800+ models across 100+ providers with per-provider context windows).
In-memory cache (1hr TTL) + disk cache for cold starts.
- Rewritten get_model_context_length() resolution chain:
0. Config override (model.context_length)
1. Custom providers per-model context_length
2. Persistent disk cache
3. Endpoint /models (local servers)
4. Anthropic /v1/models API (max_input_tokens, API-key only)
5. OpenRouter live API (existing, unchanged)
6. Nous suffix-match via OpenRouter (dot/dash normalization)
7. models.dev registry lookup (provider-aware)
8. Thin hardcoded defaults (broad family patterns)
9. 128K fallback (was 2M)
- Provider-aware context: same model now correctly resolves to different
context windows per provider (e.g. claude-opus-4.6: 1M on Anthropic,
128K on GitHub Copilot). Provider name flows through ContextCompressor.
- DEFAULT_CONTEXT_LENGTHS shrunk from 80+ entries to ~16 broad patterns.
models.dev replaces the per-model hardcoding.
- CONTEXT_PROBE_TIERS changed from [2M, 1M, 512K, 200K, 128K, 64K, 32K]
to [128K, 64K, 32K, 16K, 8K]. Unknown models no longer start at 2M.
- hermes model: prompts for context_length when configuring custom
endpoints. Supports shorthand (32k, 128K). Saved to custom_providers
per-model config.
- custom_providers schema extended with optional models dict for
per-model context_length (backward compatible).
- Nous Portal: suffix-matches bare IDs (claude-opus-4-6) against
OpenRouter's prefixed IDs (anthropic/claude-opus-4.6) with dot/dash
normalization. Handles all 15 current Nous models.
- Anthropic direct: queries /v1/models for max_input_tokens. Only works
with regular API keys (sk-ant-api*), not OAuth tokens. Falls through
to models.dev for OAuth users.
Tests: 5574 passed (18 new tests for models_dev + updated probe tiers)
Docs: Updated configuration.md context length section, AGENTS.md
Co-authored-by: Test <test@test.com>
2026-03-20 06:04:33 -07:00
|
|
|
|
if ctx:
|
2026-05-12 14:59:31 -04:00
|
|
|
|
# Persist ONLY portal-derived values. Caching an OR-fallback
|
|
|
|
|
|
# value here would freeze in a wrong number on the first portal
|
|
|
|
|
|
# blip / auth glitch and step-1 would short-circuit it forever.
|
|
|
|
|
|
# OR's catalog is community-maintained and is precisely why the
|
|
|
|
|
|
# Kimi/Qwen DEFAULT_CONTEXT_LENGTHS overrides exist — we don't
|
|
|
|
|
|
# want it leaking into the persistent cache for Nous URLs.
|
|
|
|
|
|
if base_url and source == "portal":
|
|
|
|
|
|
save_context_length(model, base_url, ctx)
|
feat: overhaul context length detection with models.dev and provider-aware resolution (#2158)
Replace the fragile hardcoded context length system with a multi-source
resolution chain that correctly identifies context windows per provider.
Key changes:
- New agent/models_dev.py: Fetches and caches the models.dev registry
(3800+ models across 100+ providers with per-provider context windows).
In-memory cache (1hr TTL) + disk cache for cold starts.
- Rewritten get_model_context_length() resolution chain:
0. Config override (model.context_length)
1. Custom providers per-model context_length
2. Persistent disk cache
3. Endpoint /models (local servers)
4. Anthropic /v1/models API (max_input_tokens, API-key only)
5. OpenRouter live API (existing, unchanged)
6. Nous suffix-match via OpenRouter (dot/dash normalization)
7. models.dev registry lookup (provider-aware)
8. Thin hardcoded defaults (broad family patterns)
9. 128K fallback (was 2M)
- Provider-aware context: same model now correctly resolves to different
context windows per provider (e.g. claude-opus-4.6: 1M on Anthropic,
128K on GitHub Copilot). Provider name flows through ContextCompressor.
- DEFAULT_CONTEXT_LENGTHS shrunk from 80+ entries to ~16 broad patterns.
models.dev replaces the per-model hardcoding.
- CONTEXT_PROBE_TIERS changed from [2M, 1M, 512K, 200K, 128K, 64K, 32K]
to [128K, 64K, 32K, 16K, 8K]. Unknown models no longer start at 2M.
- hermes model: prompts for context_length when configuring custom
endpoints. Supports shorthand (32k, 128K). Saved to custom_providers
per-model config.
- custom_providers schema extended with optional models dict for
per-model context_length (backward compatible).
- Nous Portal: suffix-matches bare IDs (claude-opus-4-6) against
OpenRouter's prefixed IDs (anthropic/claude-opus-4.6) with dot/dash
normalization. Handles all 15 current Nous models.
- Anthropic direct: queries /v1/models for max_input_tokens. Only works
with regular API keys (sk-ant-api*), not OAuth tokens. Falls through
to models.dev for OAuth users.
Tests: 5574 passed (18 new tests for models_dev + updated probe tiers)
Docs: Updated configuration.md context length section, AGENTS.md
Co-authored-by: Test <test@test.com>
2026-03-20 06:04:33 -07:00
|
|
|
|
return ctx
|
fix(context): resolve real Codex OAuth context windows (272k, not 1M) (#14935)
On ChatGPT Codex OAuth every gpt-5.x slug actually caps at 272,000 tokens,
but Hermes was resolving gpt-5.5 / gpt-5.4 to 1,050,000 (from models.dev)
because openai-codex aliases to the openai entry there. At 1.05M the
compressor never fires and requests hard-fail with 'context window
exceeded' around the real 272k boundary.
Verified live against chatgpt.com/backend-api/codex/models:
gpt-5.5, gpt-5.4, gpt-5.4-mini, gpt-5.3-codex, gpt-5.2-codex,
gpt-5.2, gpt-5.1-codex-max → context_window = 272000
Changes:
- agent/model_metadata.py:
* _fetch_codex_oauth_context_lengths() — probe the Codex /models
endpoint with the OAuth bearer token and read context_window per
slug (1h in-memory TTL).
* _resolve_codex_oauth_context_length() — prefer the live probe,
fall back to hardcoded _CODEX_OAUTH_CONTEXT_FALLBACK (all 272k).
* Wire into get_model_context_length() when provider=='openai-codex',
running BEFORE the models.dev lookup (which returns 1.05M). Result
persists via save_context_length() so subsequent lookups skip the
probe entirely.
* Fixed the now-wrong comment on the DEFAULT_CONTEXT_LENGTHS gpt-5.5
entry (400k was never right for Codex; it's the catch-all for
providers we can't probe live).
Tests (4 new in TestCodexOAuthContextLength):
- fallback table used when no token is available (no models.dev leakage)
- live probe overrides the fallback
- probe failure (non-200) falls back to hardcoded 272k
- non-codex providers (openrouter, direct openai) unaffected
Non-codex context resolution is unchanged — the Codex branch only fires
when provider=='openai-codex'.
2026-04-23 22:39:47 -07:00
|
|
|
|
if effective_provider == "openai-codex":
|
|
|
|
|
|
# Codex OAuth enforces lower context limits than the direct OpenAI
|
|
|
|
|
|
# API for the same slug (e.g. gpt-5.5 is 1.05M on the API but 272K
|
|
|
|
|
|
# on Codex). Authoritative source is Codex's own /models endpoint.
|
|
|
|
|
|
codex_ctx = _resolve_codex_oauth_context_length(model, access_token=api_key or "")
|
|
|
|
|
|
if codex_ctx:
|
|
|
|
|
|
if base_url:
|
|
|
|
|
|
save_context_length(model, base_url, codex_ctx)
|
|
|
|
|
|
return codex_ctx
|
feat(providers): add GMI Cloud as a first-class API-key provider (#11955)
Add GMI Cloud (api.gmi-serving.com) as a full first-class API-key provider
with built-in auth, aliases, model catalog, CLI entry points, auxiliary client
routing, context length resolution, doctor checks, env var tracking, and docs.
- auth.py: ProviderConfig for 'gmi' (api_key, GMI_API_KEY / GMI_BASE_URL)
- providers.py: HermesOverlay with extra_env_vars for models.dev detection
- models.py: curated slash-form model catalog; live /v1/models fetch
- main.py: 'gmi' in _named_custom_provider_map and --provider choices
- model_metadata.py: _URL_TO_PROVIDER, _PROVIDER_PREFIXES, dedicated
context-length probe block (GMI's /models has authoritative data)
- auxiliary_client.py: alias entries; _compat_model fix for slash-form
models on cached aggregator-style clients; gmi aux default model
- doctor.py: GMI in provider connectivity checks
- config.py: GMI_API_KEY / GMI_BASE_URL in OPTIONAL_ENV_VARS
- conftest.py: explicit GMI_BASE_URL clearing (not caught by _API_KEY suffix)
- docs: providers.md, environment-variables.md, fallback-providers.md,
configuration.md, quickstart.md (expands provider table)
Co-authored-by: Isaac Huang <isaachuang@Isaacs-MacBook-Pro.local>
2026-04-17 11:19:56 -07:00
|
|
|
|
if effective_provider == "gmi" and base_url:
|
|
|
|
|
|
# GMI exposes authoritative context_length via /models, but it is not
|
|
|
|
|
|
# in models.dev yet. Preserve that higher-fidelity endpoint lookup.
|
|
|
|
|
|
ctx = _resolve_endpoint_context_length(model, base_url, api_key=api_key)
|
|
|
|
|
|
if ctx is not None:
|
|
|
|
|
|
return ctx
|
2026-05-12 01:05:25 +05:30
|
|
|
|
# 5e. Ollama native /api/show probe — runs for ANY provider with a
|
|
|
|
|
|
# base_url, not just ollama-cloud. Ollama-compatible servers expose
|
|
|
|
|
|
# this endpoint regardless of hostname (local Ollama, Ollama Cloud,
|
|
|
|
|
|
# custom Ollama hosting). The OpenAI-compat /v1/models endpoint
|
|
|
|
|
|
# correctly omits context_length per the OpenAI schema, but /api/show
|
|
|
|
|
|
# returns the authoritative GGUF model_info.context_length.
|
|
|
|
|
|
# For non-Ollama servers (OpenAI, Anthropic, etc.), the POST returns
|
|
|
|
|
|
# 404/405 quickly. Results are cached, so the hit is per-model+URL,
|
|
|
|
|
|
# once per hour.
|
|
|
|
|
|
if base_url:
|
|
|
|
|
|
ctx = _query_ollama_api_show(model, base_url, api_key=api_key)
|
|
|
|
|
|
if ctx is not None:
|
|
|
|
|
|
save_context_length(model, base_url, ctx)
|
|
|
|
|
|
return ctx
|
2026-06-09 10:49:32 -07:00
|
|
|
|
# 5f. OpenRouter live /models metadata — authoritative for OpenRouter-routed
|
|
|
|
|
|
# models. OpenRouter's catalog carries per-model context_length (e.g.
|
|
|
|
|
|
# anthropic/claude-fable-5 -> 1M) and refreshes as new slugs ship, so it
|
|
|
|
|
|
# must win over both models.dev (step 5g) and the hardcoded family catch-all
|
|
|
|
|
|
# (step 8). Before this branch, an OpenRouter selection set
|
|
|
|
|
|
# effective_provider="openrouter", which (a) made the models.dev lookup miss
|
|
|
|
|
|
# brand-new slugs and (b) skipped the step-6 OR fallback (gated on `not
|
|
|
|
|
|
# effective_provider`), so a fresh slug like claude-fable-5 fell through to
|
|
|
|
|
|
# the generic "claude": 200K entry and under-reported a 1M window. Mirrors
|
|
|
|
|
|
# the dedicated Nous/Copilot/GMI branches above.
|
|
|
|
|
|
if effective_provider == "openrouter":
|
|
|
|
|
|
metadata = fetch_model_metadata()
|
|
|
|
|
|
entry = metadata.get(model)
|
|
|
|
|
|
if entry:
|
|
|
|
|
|
or_ctx = entry.get("context_length")
|
|
|
|
|
|
# Guard against the known OpenRouter Kimi-family 32k underreport
|
|
|
|
|
|
# (same class the hardcoded overrides exist to mitigate).
|
|
|
|
|
|
if isinstance(or_ctx, int) and or_ctx > 0 and not (
|
|
|
|
|
|
or_ctx == 32768 and _model_name_suggests_kimi(model)
|
|
|
|
|
|
):
|
|
|
|
|
|
return or_ctx
|
|
|
|
|
|
|
fix: infer provider from base URL for models.dev context length lookup
Custom endpoint users (DashScope/Alibaba, Z.AI, Kimi, DeepSeek, etc.)
get wrong context lengths because their provider resolves as "openrouter"
or "custom", skipping the models.dev lookup entirely. For example,
qwen3.5-plus on DashScope falls to the generic "qwen" hardcoded default
(131K) instead of the correct 1M.
Add _infer_provider_from_url() that maps known API hostnames to their
models.dev provider IDs. When the explicit provider is generic
(openrouter/custom/empty), infer from the base URL before the models.dev
lookup. This resolves context lengths correctly for DashScope, Z.AI,
Kimi, MiniMax, DeepSeek, and Nous endpoints without requiring users to
manually set context_length in config.
Also refactors _is_known_provider_base_url() to use the same URL mapping,
removing the duplicated hostname list.
2026-03-20 11:57:24 -07:00
|
|
|
|
if effective_provider:
|
feat: overhaul context length detection with models.dev and provider-aware resolution (#2158)
Replace the fragile hardcoded context length system with a multi-source
resolution chain that correctly identifies context windows per provider.
Key changes:
- New agent/models_dev.py: Fetches and caches the models.dev registry
(3800+ models across 100+ providers with per-provider context windows).
In-memory cache (1hr TTL) + disk cache for cold starts.
- Rewritten get_model_context_length() resolution chain:
0. Config override (model.context_length)
1. Custom providers per-model context_length
2. Persistent disk cache
3. Endpoint /models (local servers)
4. Anthropic /v1/models API (max_input_tokens, API-key only)
5. OpenRouter live API (existing, unchanged)
6. Nous suffix-match via OpenRouter (dot/dash normalization)
7. models.dev registry lookup (provider-aware)
8. Thin hardcoded defaults (broad family patterns)
9. 128K fallback (was 2M)
- Provider-aware context: same model now correctly resolves to different
context windows per provider (e.g. claude-opus-4.6: 1M on Anthropic,
128K on GitHub Copilot). Provider name flows through ContextCompressor.
- DEFAULT_CONTEXT_LENGTHS shrunk from 80+ entries to ~16 broad patterns.
models.dev replaces the per-model hardcoding.
- CONTEXT_PROBE_TIERS changed from [2M, 1M, 512K, 200K, 128K, 64K, 32K]
to [128K, 64K, 32K, 16K, 8K]. Unknown models no longer start at 2M.
- hermes model: prompts for context_length when configuring custom
endpoints. Supports shorthand (32k, 128K). Saved to custom_providers
per-model config.
- custom_providers schema extended with optional models dict for
per-model context_length (backward compatible).
- Nous Portal: suffix-matches bare IDs (claude-opus-4-6) against
OpenRouter's prefixed IDs (anthropic/claude-opus-4.6) with dot/dash
normalization. Handles all 15 current Nous models.
- Anthropic direct: queries /v1/models for max_input_tokens. Only works
with regular API keys (sk-ant-api*), not OAuth tokens. Falls through
to models.dev for OAuth users.
Tests: 5574 passed (18 new tests for models_dev + updated probe tiers)
Docs: Updated configuration.md context length section, AGENTS.md
Co-authored-by: Test <test@test.com>
2026-03-20 06:04:33 -07:00
|
|
|
|
from agent.models_dev import lookup_models_dev_context
|
fix: infer provider from base URL for models.dev context length lookup
Custom endpoint users (DashScope/Alibaba, Z.AI, Kimi, DeepSeek, etc.)
get wrong context lengths because their provider resolves as "openrouter"
or "custom", skipping the models.dev lookup entirely. For example,
qwen3.5-plus on DashScope falls to the generic "qwen" hardcoded default
(131K) instead of the correct 1M.
Add _infer_provider_from_url() that maps known API hostnames to their
models.dev provider IDs. When the explicit provider is generic
(openrouter/custom/empty), infer from the base URL before the models.dev
lookup. This resolves context lengths correctly for DashScope, Z.AI,
Kimi, MiniMax, DeepSeek, and Nous endpoints without requiring users to
manually set context_length in config.
Also refactors _is_known_provider_base_url() to use the same URL mapping,
removing the duplicated hostname list.
2026-03-20 11:57:24 -07:00
|
|
|
|
ctx = lookup_models_dev_context(effective_provider, model)
|
feat: overhaul context length detection with models.dev and provider-aware resolution (#2158)
Replace the fragile hardcoded context length system with a multi-source
resolution chain that correctly identifies context windows per provider.
Key changes:
- New agent/models_dev.py: Fetches and caches the models.dev registry
(3800+ models across 100+ providers with per-provider context windows).
In-memory cache (1hr TTL) + disk cache for cold starts.
- Rewritten get_model_context_length() resolution chain:
0. Config override (model.context_length)
1. Custom providers per-model context_length
2. Persistent disk cache
3. Endpoint /models (local servers)
4. Anthropic /v1/models API (max_input_tokens, API-key only)
5. OpenRouter live API (existing, unchanged)
6. Nous suffix-match via OpenRouter (dot/dash normalization)
7. models.dev registry lookup (provider-aware)
8. Thin hardcoded defaults (broad family patterns)
9. 128K fallback (was 2M)
- Provider-aware context: same model now correctly resolves to different
context windows per provider (e.g. claude-opus-4.6: 1M on Anthropic,
128K on GitHub Copilot). Provider name flows through ContextCompressor.
- DEFAULT_CONTEXT_LENGTHS shrunk from 80+ entries to ~16 broad patterns.
models.dev replaces the per-model hardcoding.
- CONTEXT_PROBE_TIERS changed from [2M, 1M, 512K, 200K, 128K, 64K, 32K]
to [128K, 64K, 32K, 16K, 8K]. Unknown models no longer start at 2M.
- hermes model: prompts for context_length when configuring custom
endpoints. Supports shorthand (32k, 128K). Saved to custom_providers
per-model config.
- custom_providers schema extended with optional models dict for
per-model context_length (backward compatible).
- Nous Portal: suffix-matches bare IDs (claude-opus-4-6) against
OpenRouter's prefixed IDs (anthropic/claude-opus-4.6) with dot/dash
normalization. Handles all 15 current Nous models.
- Anthropic direct: queries /v1/models for max_input_tokens. Only works
with regular API keys (sk-ant-api*), not OAuth tokens. Falls through
to models.dev for OAuth users.
Tests: 5574 passed (18 new tests for models_dev + updated probe tiers)
Docs: Updated configuration.md context length section, AGENTS.md
Co-authored-by: Test <test@test.com>
2026-03-20 06:04:33 -07:00
|
|
|
|
if ctx:
|
2026-06-09 03:25:05 +08:00
|
|
|
|
# MiniMax M3: models.dev reports 512K but actual context is 1M.
|
|
|
|
|
|
# Prefer hardcoded catalog over stale probe value.
|
|
|
|
|
|
if _model_name_suggests_minimax_m3(model):
|
|
|
|
|
|
catalog = DEFAULT_CONTEXT_LENGTHS.get("minimax-m3")
|
|
|
|
|
|
if catalog and ctx < catalog:
|
|
|
|
|
|
logger.info(
|
|
|
|
|
|
"Rejecting models.dev context=%s for %r "
|
|
|
|
|
|
"(MiniMax-M3 underreport); using hardcoded default %s",
|
|
|
|
|
|
ctx, model, f"{catalog:,}",
|
|
|
|
|
|
)
|
|
|
|
|
|
ctx = catalog
|
feat: overhaul context length detection with models.dev and provider-aware resolution (#2158)
Replace the fragile hardcoded context length system with a multi-source
resolution chain that correctly identifies context windows per provider.
Key changes:
- New agent/models_dev.py: Fetches and caches the models.dev registry
(3800+ models across 100+ providers with per-provider context windows).
In-memory cache (1hr TTL) + disk cache for cold starts.
- Rewritten get_model_context_length() resolution chain:
0. Config override (model.context_length)
1. Custom providers per-model context_length
2. Persistent disk cache
3. Endpoint /models (local servers)
4. Anthropic /v1/models API (max_input_tokens, API-key only)
5. OpenRouter live API (existing, unchanged)
6. Nous suffix-match via OpenRouter (dot/dash normalization)
7. models.dev registry lookup (provider-aware)
8. Thin hardcoded defaults (broad family patterns)
9. 128K fallback (was 2M)
- Provider-aware context: same model now correctly resolves to different
context windows per provider (e.g. claude-opus-4.6: 1M on Anthropic,
128K on GitHub Copilot). Provider name flows through ContextCompressor.
- DEFAULT_CONTEXT_LENGTHS shrunk from 80+ entries to ~16 broad patterns.
models.dev replaces the per-model hardcoding.
- CONTEXT_PROBE_TIERS changed from [2M, 1M, 512K, 200K, 128K, 64K, 32K]
to [128K, 64K, 32K, 16K, 8K]. Unknown models no longer start at 2M.
- hermes model: prompts for context_length when configuring custom
endpoints. Supports shorthand (32k, 128K). Saved to custom_providers
per-model config.
- custom_providers schema extended with optional models dict for
per-model context_length (backward compatible).
- Nous Portal: suffix-matches bare IDs (claude-opus-4-6) against
OpenRouter's prefixed IDs (anthropic/claude-opus-4.6) with dot/dash
normalization. Handles all 15 current Nous models.
- Anthropic direct: queries /v1/models for max_input_tokens. Only works
with regular API keys (sk-ant-api*), not OAuth tokens. Falls through
to models.dev for OAuth users.
Tests: 5574 passed (18 new tests for models_dev + updated probe tiers)
Docs: Updated configuration.md context length section, AGENTS.md
Co-authored-by: Test <test@test.com>
2026-03-20 06:04:33 -07:00
|
|
|
|
return ctx
|
|
|
|
|
|
|
2026-05-12 01:38:43 +05:30
|
|
|
|
# 6. OpenRouter live API metadata — provider-unaware fallback.
|
|
|
|
|
|
# Only consulted when the provider is unknown (no effective_provider),
|
|
|
|
|
|
# because OpenRouter data is community-maintained and can be incorrect
|
|
|
|
|
|
# for models that belong to known providers with curated defaults.
|
|
|
|
|
|
if not effective_provider:
|
|
|
|
|
|
metadata = fetch_model_metadata()
|
|
|
|
|
|
if model in metadata:
|
|
|
|
|
|
or_ctx = metadata[model].get("context_length", DEFAULT_FALLBACK_CONTEXT)
|
|
|
|
|
|
# Guard against stale OpenRouter metadata for Kimi-family models.
|
|
|
|
|
|
if or_ctx == 32768 and _model_name_suggests_kimi(model):
|
|
|
|
|
|
logger.info(
|
|
|
|
|
|
"Rejecting OpenRouter metadata context=%s for %r "
|
|
|
|
|
|
"(Kimi-family underreport); falling through to hardcoded defaults",
|
|
|
|
|
|
or_ctx, model,
|
|
|
|
|
|
)
|
|
|
|
|
|
else:
|
|
|
|
|
|
return or_ctx
|
|
|
|
|
|
|
|
|
|
|
|
# 7. (reserved)
|
2026-02-21 22:31:43 -08:00
|
|
|
|
|
feat: overhaul context length detection with models.dev and provider-aware resolution (#2158)
Replace the fragile hardcoded context length system with a multi-source
resolution chain that correctly identifies context windows per provider.
Key changes:
- New agent/models_dev.py: Fetches and caches the models.dev registry
(3800+ models across 100+ providers with per-provider context windows).
In-memory cache (1hr TTL) + disk cache for cold starts.
- Rewritten get_model_context_length() resolution chain:
0. Config override (model.context_length)
1. Custom providers per-model context_length
2. Persistent disk cache
3. Endpoint /models (local servers)
4. Anthropic /v1/models API (max_input_tokens, API-key only)
5. OpenRouter live API (existing, unchanged)
6. Nous suffix-match via OpenRouter (dot/dash normalization)
7. models.dev registry lookup (provider-aware)
8. Thin hardcoded defaults (broad family patterns)
9. 128K fallback (was 2M)
- Provider-aware context: same model now correctly resolves to different
context windows per provider (e.g. claude-opus-4.6: 1M on Anthropic,
128K on GitHub Copilot). Provider name flows through ContextCompressor.
- DEFAULT_CONTEXT_LENGTHS shrunk from 80+ entries to ~16 broad patterns.
models.dev replaces the per-model hardcoding.
- CONTEXT_PROBE_TIERS changed from [2M, 1M, 512K, 200K, 128K, 64K, 32K]
to [128K, 64K, 32K, 16K, 8K]. Unknown models no longer start at 2M.
- hermes model: prompts for context_length when configuring custom
endpoints. Supports shorthand (32k, 128K). Saved to custom_providers
per-model config.
- custom_providers schema extended with optional models dict for
per-model context_length (backward compatible).
- Nous Portal: suffix-matches bare IDs (claude-opus-4-6) against
OpenRouter's prefixed IDs (anthropic/claude-opus-4.6) with dot/dash
normalization. Handles all 15 current Nous models.
- Anthropic direct: queries /v1/models for max_input_tokens. Only works
with regular API keys (sk-ant-api*), not OAuth tokens. Falls through
to models.dev for OAuth users.
Tests: 5574 passed (18 new tests for models_dev + updated probe tiers)
Docs: Updated configuration.md context length section, AGENTS.md
Co-authored-by: Test <test@test.com>
2026-03-20 06:04:33 -07:00
|
|
|
|
# 8. Hardcoded defaults (fuzzy match — longest key first for specificity)
|
2026-03-20 08:52:37 -07:00
|
|
|
|
# Only check `default_model in model` (is the key a substring of the input).
|
|
|
|
|
|
# The reverse (`model in default_model`) causes shorter names like
|
|
|
|
|
|
# "claude-sonnet-4" to incorrectly match "claude-sonnet-4-6" and return 1M.
|
2026-03-21 10:47:44 -07:00
|
|
|
|
model_lower = model.lower()
|
2026-03-17 04:12:08 -07:00
|
|
|
|
for default_model, length in sorted(
|
|
|
|
|
|
DEFAULT_CONTEXT_LENGTHS.items(), key=lambda x: len(x[0]), reverse=True
|
|
|
|
|
|
):
|
2026-03-21 10:47:44 -07:00
|
|
|
|
if default_model in model_lower:
|
2026-02-21 22:31:43 -08:00
|
|
|
|
return length
|
|
|
|
|
|
|
feat: overhaul context length detection with models.dev and provider-aware resolution (#2158)
Replace the fragile hardcoded context length system with a multi-source
resolution chain that correctly identifies context windows per provider.
Key changes:
- New agent/models_dev.py: Fetches and caches the models.dev registry
(3800+ models across 100+ providers with per-provider context windows).
In-memory cache (1hr TTL) + disk cache for cold starts.
- Rewritten get_model_context_length() resolution chain:
0. Config override (model.context_length)
1. Custom providers per-model context_length
2. Persistent disk cache
3. Endpoint /models (local servers)
4. Anthropic /v1/models API (max_input_tokens, API-key only)
5. OpenRouter live API (existing, unchanged)
6. Nous suffix-match via OpenRouter (dot/dash normalization)
7. models.dev registry lookup (provider-aware)
8. Thin hardcoded defaults (broad family patterns)
9. 128K fallback (was 2M)
- Provider-aware context: same model now correctly resolves to different
context windows per provider (e.g. claude-opus-4.6: 1M on Anthropic,
128K on GitHub Copilot). Provider name flows through ContextCompressor.
- DEFAULT_CONTEXT_LENGTHS shrunk from 80+ entries to ~16 broad patterns.
models.dev replaces the per-model hardcoding.
- CONTEXT_PROBE_TIERS changed from [2M, 1M, 512K, 200K, 128K, 64K, 32K]
to [128K, 64K, 32K, 16K, 8K]. Unknown models no longer start at 2M.
- hermes model: prompts for context_length when configuring custom
endpoints. Supports shorthand (32k, 128K). Saved to custom_providers
per-model config.
- custom_providers schema extended with optional models dict for
per-model context_length (backward compatible).
- Nous Portal: suffix-matches bare IDs (claude-opus-4-6) against
OpenRouter's prefixed IDs (anthropic/claude-opus-4.6) with dot/dash
normalization. Handles all 15 current Nous models.
- Anthropic direct: queries /v1/models for max_input_tokens. Only works
with regular API keys (sk-ant-api*), not OAuth tokens. Falls through
to models.dev for OAuth users.
Tests: 5574 passed (18 new tests for models_dev + updated probe tiers)
Docs: Updated configuration.md context length section, AGENTS.md
Co-authored-by: Test <test@test.com>
2026-03-20 06:04:33 -07:00
|
|
|
|
# 9. Query local server as last resort
|
2026-03-18 21:38:41 +01:00
|
|
|
|
if base_url and is_local_endpoint(base_url):
|
2026-04-20 20:49:44 -07:00
|
|
|
|
local_ctx = _query_local_context_length(model, base_url, api_key=api_key)
|
2026-03-18 21:38:41 +01:00
|
|
|
|
if local_ctx and local_ctx > 0:
|
2026-04-25 12:30:55 -04:00
|
|
|
|
if provider != "lmstudio":
|
|
|
|
|
|
save_context_length(model, base_url, local_ctx)
|
2026-03-18 21:38:41 +01:00
|
|
|
|
return local_ctx
|
|
|
|
|
|
|
2026-04-29 20:18:08 -07:00
|
|
|
|
# 10. Default fallback — 256K
|
feat: overhaul context length detection with models.dev and provider-aware resolution (#2158)
Replace the fragile hardcoded context length system with a multi-source
resolution chain that correctly identifies context windows per provider.
Key changes:
- New agent/models_dev.py: Fetches and caches the models.dev registry
(3800+ models across 100+ providers with per-provider context windows).
In-memory cache (1hr TTL) + disk cache for cold starts.
- Rewritten get_model_context_length() resolution chain:
0. Config override (model.context_length)
1. Custom providers per-model context_length
2. Persistent disk cache
3. Endpoint /models (local servers)
4. Anthropic /v1/models API (max_input_tokens, API-key only)
5. OpenRouter live API (existing, unchanged)
6. Nous suffix-match via OpenRouter (dot/dash normalization)
7. models.dev registry lookup (provider-aware)
8. Thin hardcoded defaults (broad family patterns)
9. 128K fallback (was 2M)
- Provider-aware context: same model now correctly resolves to different
context windows per provider (e.g. claude-opus-4.6: 1M on Anthropic,
128K on GitHub Copilot). Provider name flows through ContextCompressor.
- DEFAULT_CONTEXT_LENGTHS shrunk from 80+ entries to ~16 broad patterns.
models.dev replaces the per-model hardcoding.
- CONTEXT_PROBE_TIERS changed from [2M, 1M, 512K, 200K, 128K, 64K, 32K]
to [128K, 64K, 32K, 16K, 8K]. Unknown models no longer start at 2M.
- hermes model: prompts for context_length when configuring custom
endpoints. Supports shorthand (32k, 128K). Saved to custom_providers
per-model config.
- custom_providers schema extended with optional models dict for
per-model context_length (backward compatible).
- Nous Portal: suffix-matches bare IDs (claude-opus-4-6) against
OpenRouter's prefixed IDs (anthropic/claude-opus-4.6) with dot/dash
normalization. Handles all 15 current Nous models.
- Anthropic direct: queries /v1/models for max_input_tokens. Only works
with regular API keys (sk-ant-api*), not OAuth tokens. Falls through
to models.dev for OAuth users.
Tests: 5574 passed (18 new tests for models_dev + updated probe tiers)
Docs: Updated configuration.md context length section, AGENTS.md
Co-authored-by: Test <test@test.com>
2026-03-20 06:04:33 -07:00
|
|
|
|
return DEFAULT_FALLBACK_CONTEXT
|
2026-02-21 22:31:43 -08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def estimate_tokens_rough(text: str) -> int:
|
2026-04-11 16:33:35 -07:00
|
|
|
|
"""Rough token estimate (~4 chars/token) for pre-flight checks.
|
|
|
|
|
|
|
|
|
|
|
|
Uses ceiling division so short texts (1-3 chars) never estimate as
|
|
|
|
|
|
0 tokens, which would cause the compressor and pre-flight checks to
|
|
|
|
|
|
systematically undercount when many short tool results are present.
|
|
|
|
|
|
"""
|
2026-02-21 22:31:43 -08:00
|
|
|
|
if not text:
|
|
|
|
|
|
return 0
|
2026-04-11 16:33:35 -07:00
|
|
|
|
return (len(text) + 3) // 4
|
2026-02-21 22:31:43 -08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def estimate_messages_tokens_rough(messages: List[Dict[str, Any]]) -> int:
|
feat(computer-use): cua-driver backend, universal any-model schema
Background macOS desktop control via cua-driver MCP — does NOT steal the
user's cursor or keyboard focus, works with any tool-capable model.
Replaces the Anthropic-native `computer_20251124` approach from the
abandoned #4562 with a generic OpenAI function-calling schema plus SOM
(set-of-mark) captures so Claude, GPT, Gemini, and open models can all
drive the desktop via numbered element indices.
- `tools/computer_use/` package — swappable ComputerUseBackend ABC +
CuaDriverBackend (stdio MCP client to trycua/cua's cua-driver binary).
- Universal `computer_use` tool with one schema for all providers.
Actions: capture (som/vision/ax), click, double_click, right_click,
middle_click, drag, scroll, type, key, wait, list_apps, focus_app.
- Multimodal tool-result envelope (`_multimodal=True`, OpenAI-style
`content: [text, image_url]` parts) that flows through
handle_function_call into the tool message. Anthropic adapter converts
into native `tool_result` image blocks; OpenAI-compatible providers
get the parts list directly.
- Image eviction in convert_messages_to_anthropic: only the 3 most
recent screenshots carry real image data; older ones become text
placeholders to cap per-turn token cost.
- Context compressor image pruning: old multimodal tool results have
their image parts stripped instead of being skipped.
- Image-aware token estimation: each image counts as a flat 1500 tokens
instead of its base64 char length (~1MB would have registered as
~250K tokens before).
- COMPUTER_USE_GUIDANCE system-prompt block — injected when the toolset
is active.
- Session DB persistence strips base64 from multimodal tool messages.
- Trajectory saver normalises multimodal messages to text-only.
- `hermes tools` post-setup installs cua-driver via the upstream script
and prints permission-grant instructions.
- CLI approval callback wired so destructive computer_use actions go
through the same prompt_toolkit approval dialog as terminal commands.
- Hard safety guards at the tool level: blocked type patterns
(curl|bash, sudo rm -rf, fork bomb), blocked key combos (empty trash,
force delete, lock screen, log out).
- Skill `apple/macos-computer-use/SKILL.md` — universal (model-agnostic)
workflow guide.
- Docs: `user-guide/features/computer-use.md` plus reference catalog
entries.
44 new tests in tests/tools/test_computer_use.py covering schema
shape (universal, not Anthropic-native), dispatch routing, safety
guards, multimodal envelope, Anthropic adapter conversion, screenshot
eviction, context compressor pruning, image-aware token estimation,
run_agent helpers, and universality guarantees.
469/469 pass across tests/tools/test_computer_use.py + the affected
agent/ test suites.
- `model_tools.py` provider-gating: the tool is available to every
provider. Providers without multi-part tool message support will see
text-only tool results (graceful degradation via `text_summary`).
- Anthropic server-side `clear_tool_uses_20250919` — deferred;
client-side eviction + compressor pruning cover the same cost ceiling
without a beta header.
- macOS only. cua-driver uses private SkyLight SPIs
(SLEventPostToPid, SLPSPostEventRecordTo,
_AXObserverAddNotificationAndCheckRemote) that can break on any macOS
update. Pin with HERMES_CUA_DRIVER_VERSION.
- Requires Accessibility + Screen Recording permissions — the post-setup
prints the Settings path.
Supersedes PR #4562 (pyautogui/Quartz foreground backend, Anthropic-
native schema). Credit @0xbyt4 for the original #3816 groundwork whose
context/eviction/token design is preserved here in generic form.
2026-04-23 16:44:24 -07:00
|
|
|
|
"""Rough token estimate for a message list (pre-flight only).
|
|
|
|
|
|
|
|
|
|
|
|
Image parts (base64 PNG/JPEG) are counted as a flat ~1500 tokens per
|
|
|
|
|
|
image — the Anthropic pricing model — instead of counting raw base64
|
|
|
|
|
|
character length. Without this, a single ~1MB screenshot would be
|
|
|
|
|
|
estimated at ~250K tokens and trigger premature context compression.
|
|
|
|
|
|
"""
|
|
|
|
|
|
_IMAGE_TOKEN_COST = 1500
|
|
|
|
|
|
total_chars = 0
|
|
|
|
|
|
image_tokens = 0
|
|
|
|
|
|
for msg in messages:
|
|
|
|
|
|
total_chars += _estimate_message_chars(msg)
|
|
|
|
|
|
image_tokens += _count_image_tokens(msg, _IMAGE_TOKEN_COST)
|
|
|
|
|
|
return ((total_chars + 3) // 4) + image_tokens
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _count_image_tokens(msg: Dict[str, Any], cost_per_image: int) -> int:
|
|
|
|
|
|
"""Count image-like content parts in a message; return their token cost."""
|
|
|
|
|
|
count = 0
|
|
|
|
|
|
content = msg.get("content") if isinstance(msg, dict) else None
|
|
|
|
|
|
if isinstance(content, list):
|
|
|
|
|
|
for part in content:
|
|
|
|
|
|
if not isinstance(part, dict):
|
|
|
|
|
|
continue
|
|
|
|
|
|
ptype = part.get("type")
|
2026-05-11 11:13:25 -07:00
|
|
|
|
if ptype in {"image", "image_url", "input_image"}:
|
feat(computer-use): cua-driver backend, universal any-model schema
Background macOS desktop control via cua-driver MCP — does NOT steal the
user's cursor or keyboard focus, works with any tool-capable model.
Replaces the Anthropic-native `computer_20251124` approach from the
abandoned #4562 with a generic OpenAI function-calling schema plus SOM
(set-of-mark) captures so Claude, GPT, Gemini, and open models can all
drive the desktop via numbered element indices.
- `tools/computer_use/` package — swappable ComputerUseBackend ABC +
CuaDriverBackend (stdio MCP client to trycua/cua's cua-driver binary).
- Universal `computer_use` tool with one schema for all providers.
Actions: capture (som/vision/ax), click, double_click, right_click,
middle_click, drag, scroll, type, key, wait, list_apps, focus_app.
- Multimodal tool-result envelope (`_multimodal=True`, OpenAI-style
`content: [text, image_url]` parts) that flows through
handle_function_call into the tool message. Anthropic adapter converts
into native `tool_result` image blocks; OpenAI-compatible providers
get the parts list directly.
- Image eviction in convert_messages_to_anthropic: only the 3 most
recent screenshots carry real image data; older ones become text
placeholders to cap per-turn token cost.
- Context compressor image pruning: old multimodal tool results have
their image parts stripped instead of being skipped.
- Image-aware token estimation: each image counts as a flat 1500 tokens
instead of its base64 char length (~1MB would have registered as
~250K tokens before).
- COMPUTER_USE_GUIDANCE system-prompt block — injected when the toolset
is active.
- Session DB persistence strips base64 from multimodal tool messages.
- Trajectory saver normalises multimodal messages to text-only.
- `hermes tools` post-setup installs cua-driver via the upstream script
and prints permission-grant instructions.
- CLI approval callback wired so destructive computer_use actions go
through the same prompt_toolkit approval dialog as terminal commands.
- Hard safety guards at the tool level: blocked type patterns
(curl|bash, sudo rm -rf, fork bomb), blocked key combos (empty trash,
force delete, lock screen, log out).
- Skill `apple/macos-computer-use/SKILL.md` — universal (model-agnostic)
workflow guide.
- Docs: `user-guide/features/computer-use.md` plus reference catalog
entries.
44 new tests in tests/tools/test_computer_use.py covering schema
shape (universal, not Anthropic-native), dispatch routing, safety
guards, multimodal envelope, Anthropic adapter conversion, screenshot
eviction, context compressor pruning, image-aware token estimation,
run_agent helpers, and universality guarantees.
469/469 pass across tests/tools/test_computer_use.py + the affected
agent/ test suites.
- `model_tools.py` provider-gating: the tool is available to every
provider. Providers without multi-part tool message support will see
text-only tool results (graceful degradation via `text_summary`).
- Anthropic server-side `clear_tool_uses_20250919` — deferred;
client-side eviction + compressor pruning cover the same cost ceiling
without a beta header.
- macOS only. cua-driver uses private SkyLight SPIs
(SLEventPostToPid, SLPSPostEventRecordTo,
_AXObserverAddNotificationAndCheckRemote) that can break on any macOS
update. Pin with HERMES_CUA_DRIVER_VERSION.
- Requires Accessibility + Screen Recording permissions — the post-setup
prints the Settings path.
Supersedes PR #4562 (pyautogui/Quartz foreground backend, Anthropic-
native schema). Credit @0xbyt4 for the original #3816 groundwork whose
context/eviction/token design is preserved here in generic form.
2026-04-23 16:44:24 -07:00
|
|
|
|
count += 1
|
|
|
|
|
|
stashed = msg.get("_anthropic_content_blocks") if isinstance(msg, dict) else None
|
|
|
|
|
|
if isinstance(stashed, list):
|
|
|
|
|
|
for part in stashed:
|
|
|
|
|
|
if isinstance(part, dict) and part.get("type") == "image":
|
|
|
|
|
|
count += 1
|
|
|
|
|
|
# Multimodal tool results that haven't been converted yet.
|
|
|
|
|
|
if isinstance(content, dict) and content.get("_multimodal"):
|
|
|
|
|
|
inner = content.get("content")
|
|
|
|
|
|
if isinstance(inner, list):
|
|
|
|
|
|
for part in inner:
|
2026-05-11 11:13:25 -07:00
|
|
|
|
if isinstance(part, dict) and part.get("type") in {"image", "image_url"}:
|
feat(computer-use): cua-driver backend, universal any-model schema
Background macOS desktop control via cua-driver MCP — does NOT steal the
user's cursor or keyboard focus, works with any tool-capable model.
Replaces the Anthropic-native `computer_20251124` approach from the
abandoned #4562 with a generic OpenAI function-calling schema plus SOM
(set-of-mark) captures so Claude, GPT, Gemini, and open models can all
drive the desktop via numbered element indices.
- `tools/computer_use/` package — swappable ComputerUseBackend ABC +
CuaDriverBackend (stdio MCP client to trycua/cua's cua-driver binary).
- Universal `computer_use` tool with one schema for all providers.
Actions: capture (som/vision/ax), click, double_click, right_click,
middle_click, drag, scroll, type, key, wait, list_apps, focus_app.
- Multimodal tool-result envelope (`_multimodal=True`, OpenAI-style
`content: [text, image_url]` parts) that flows through
handle_function_call into the tool message. Anthropic adapter converts
into native `tool_result` image blocks; OpenAI-compatible providers
get the parts list directly.
- Image eviction in convert_messages_to_anthropic: only the 3 most
recent screenshots carry real image data; older ones become text
placeholders to cap per-turn token cost.
- Context compressor image pruning: old multimodal tool results have
their image parts stripped instead of being skipped.
- Image-aware token estimation: each image counts as a flat 1500 tokens
instead of its base64 char length (~1MB would have registered as
~250K tokens before).
- COMPUTER_USE_GUIDANCE system-prompt block — injected when the toolset
is active.
- Session DB persistence strips base64 from multimodal tool messages.
- Trajectory saver normalises multimodal messages to text-only.
- `hermes tools` post-setup installs cua-driver via the upstream script
and prints permission-grant instructions.
- CLI approval callback wired so destructive computer_use actions go
through the same prompt_toolkit approval dialog as terminal commands.
- Hard safety guards at the tool level: blocked type patterns
(curl|bash, sudo rm -rf, fork bomb), blocked key combos (empty trash,
force delete, lock screen, log out).
- Skill `apple/macos-computer-use/SKILL.md` — universal (model-agnostic)
workflow guide.
- Docs: `user-guide/features/computer-use.md` plus reference catalog
entries.
44 new tests in tests/tools/test_computer_use.py covering schema
shape (universal, not Anthropic-native), dispatch routing, safety
guards, multimodal envelope, Anthropic adapter conversion, screenshot
eviction, context compressor pruning, image-aware token estimation,
run_agent helpers, and universality guarantees.
469/469 pass across tests/tools/test_computer_use.py + the affected
agent/ test suites.
- `model_tools.py` provider-gating: the tool is available to every
provider. Providers without multi-part tool message support will see
text-only tool results (graceful degradation via `text_summary`).
- Anthropic server-side `clear_tool_uses_20250919` — deferred;
client-side eviction + compressor pruning cover the same cost ceiling
without a beta header.
- macOS only. cua-driver uses private SkyLight SPIs
(SLEventPostToPid, SLPSPostEventRecordTo,
_AXObserverAddNotificationAndCheckRemote) that can break on any macOS
update. Pin with HERMES_CUA_DRIVER_VERSION.
- Requires Accessibility + Screen Recording permissions — the post-setup
prints the Settings path.
Supersedes PR #4562 (pyautogui/Quartz foreground backend, Anthropic-
native schema). Credit @0xbyt4 for the original #3816 groundwork whose
context/eviction/token design is preserved here in generic form.
2026-04-23 16:44:24 -07:00
|
|
|
|
count += 1
|
|
|
|
|
|
return count * cost_per_image
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _estimate_message_chars(msg: Dict[str, Any]) -> int:
|
|
|
|
|
|
"""Char count for token estimation, excluding base64 image data.
|
|
|
|
|
|
|
|
|
|
|
|
Base64 images are counted via `_count_image_tokens` instead; including
|
|
|
|
|
|
their raw chars here would massively overestimate token usage.
|
|
|
|
|
|
"""
|
|
|
|
|
|
if not isinstance(msg, dict):
|
|
|
|
|
|
return len(str(msg))
|
|
|
|
|
|
shadow: Dict[str, Any] = {}
|
|
|
|
|
|
for k, v in msg.items():
|
|
|
|
|
|
if k == "_anthropic_content_blocks":
|
|
|
|
|
|
continue
|
|
|
|
|
|
if k == "content":
|
|
|
|
|
|
if isinstance(v, list):
|
|
|
|
|
|
cleaned = []
|
|
|
|
|
|
for part in v:
|
|
|
|
|
|
if isinstance(part, dict):
|
2026-05-11 11:13:25 -07:00
|
|
|
|
if part.get("type") in {"image", "image_url", "input_image"}:
|
feat(computer-use): cua-driver backend, universal any-model schema
Background macOS desktop control via cua-driver MCP — does NOT steal the
user's cursor or keyboard focus, works with any tool-capable model.
Replaces the Anthropic-native `computer_20251124` approach from the
abandoned #4562 with a generic OpenAI function-calling schema plus SOM
(set-of-mark) captures so Claude, GPT, Gemini, and open models can all
drive the desktop via numbered element indices.
- `tools/computer_use/` package — swappable ComputerUseBackend ABC +
CuaDriverBackend (stdio MCP client to trycua/cua's cua-driver binary).
- Universal `computer_use` tool with one schema for all providers.
Actions: capture (som/vision/ax), click, double_click, right_click,
middle_click, drag, scroll, type, key, wait, list_apps, focus_app.
- Multimodal tool-result envelope (`_multimodal=True`, OpenAI-style
`content: [text, image_url]` parts) that flows through
handle_function_call into the tool message. Anthropic adapter converts
into native `tool_result` image blocks; OpenAI-compatible providers
get the parts list directly.
- Image eviction in convert_messages_to_anthropic: only the 3 most
recent screenshots carry real image data; older ones become text
placeholders to cap per-turn token cost.
- Context compressor image pruning: old multimodal tool results have
their image parts stripped instead of being skipped.
- Image-aware token estimation: each image counts as a flat 1500 tokens
instead of its base64 char length (~1MB would have registered as
~250K tokens before).
- COMPUTER_USE_GUIDANCE system-prompt block — injected when the toolset
is active.
- Session DB persistence strips base64 from multimodal tool messages.
- Trajectory saver normalises multimodal messages to text-only.
- `hermes tools` post-setup installs cua-driver via the upstream script
and prints permission-grant instructions.
- CLI approval callback wired so destructive computer_use actions go
through the same prompt_toolkit approval dialog as terminal commands.
- Hard safety guards at the tool level: blocked type patterns
(curl|bash, sudo rm -rf, fork bomb), blocked key combos (empty trash,
force delete, lock screen, log out).
- Skill `apple/macos-computer-use/SKILL.md` — universal (model-agnostic)
workflow guide.
- Docs: `user-guide/features/computer-use.md` plus reference catalog
entries.
44 new tests in tests/tools/test_computer_use.py covering schema
shape (universal, not Anthropic-native), dispatch routing, safety
guards, multimodal envelope, Anthropic adapter conversion, screenshot
eviction, context compressor pruning, image-aware token estimation,
run_agent helpers, and universality guarantees.
469/469 pass across tests/tools/test_computer_use.py + the affected
agent/ test suites.
- `model_tools.py` provider-gating: the tool is available to every
provider. Providers without multi-part tool message support will see
text-only tool results (graceful degradation via `text_summary`).
- Anthropic server-side `clear_tool_uses_20250919` — deferred;
client-side eviction + compressor pruning cover the same cost ceiling
without a beta header.
- macOS only. cua-driver uses private SkyLight SPIs
(SLEventPostToPid, SLPSPostEventRecordTo,
_AXObserverAddNotificationAndCheckRemote) that can break on any macOS
update. Pin with HERMES_CUA_DRIVER_VERSION.
- Requires Accessibility + Screen Recording permissions — the post-setup
prints the Settings path.
Supersedes PR #4562 (pyautogui/Quartz foreground backend, Anthropic-
native schema). Credit @0xbyt4 for the original #3816 groundwork whose
context/eviction/token design is preserved here in generic form.
2026-04-23 16:44:24 -07:00
|
|
|
|
cleaned.append({"type": part.get("type"), "image": "[stripped]"})
|
|
|
|
|
|
else:
|
|
|
|
|
|
cleaned.append(part)
|
|
|
|
|
|
else:
|
|
|
|
|
|
cleaned.append(part)
|
|
|
|
|
|
shadow[k] = cleaned
|
|
|
|
|
|
elif isinstance(v, dict) and v.get("_multimodal"):
|
|
|
|
|
|
shadow[k] = v.get("text_summary", "")
|
|
|
|
|
|
else:
|
|
|
|
|
|
shadow[k] = v
|
|
|
|
|
|
else:
|
|
|
|
|
|
shadow[k] = v
|
|
|
|
|
|
return len(str(shadow))
|
2026-03-26 02:00:50 -07:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def estimate_request_tokens_rough(
|
|
|
|
|
|
messages: List[Dict[str, Any]],
|
|
|
|
|
|
*,
|
|
|
|
|
|
system_prompt: str = "",
|
|
|
|
|
|
tools: Optional[List[Dict[str, Any]]] = None,
|
|
|
|
|
|
) -> int:
|
|
|
|
|
|
"""Rough token estimate for a full chat-completions request.
|
|
|
|
|
|
|
|
|
|
|
|
Includes the major payload buckets Hermes sends to providers:
|
|
|
|
|
|
system prompt, conversation messages, and tool schemas. With 50+
|
|
|
|
|
|
tools enabled, schemas alone can add 20-30K tokens — a significant
|
feat(computer-use): cua-driver backend, universal any-model schema
Background macOS desktop control via cua-driver MCP — does NOT steal the
user's cursor or keyboard focus, works with any tool-capable model.
Replaces the Anthropic-native `computer_20251124` approach from the
abandoned #4562 with a generic OpenAI function-calling schema plus SOM
(set-of-mark) captures so Claude, GPT, Gemini, and open models can all
drive the desktop via numbered element indices.
- `tools/computer_use/` package — swappable ComputerUseBackend ABC +
CuaDriverBackend (stdio MCP client to trycua/cua's cua-driver binary).
- Universal `computer_use` tool with one schema for all providers.
Actions: capture (som/vision/ax), click, double_click, right_click,
middle_click, drag, scroll, type, key, wait, list_apps, focus_app.
- Multimodal tool-result envelope (`_multimodal=True`, OpenAI-style
`content: [text, image_url]` parts) that flows through
handle_function_call into the tool message. Anthropic adapter converts
into native `tool_result` image blocks; OpenAI-compatible providers
get the parts list directly.
- Image eviction in convert_messages_to_anthropic: only the 3 most
recent screenshots carry real image data; older ones become text
placeholders to cap per-turn token cost.
- Context compressor image pruning: old multimodal tool results have
their image parts stripped instead of being skipped.
- Image-aware token estimation: each image counts as a flat 1500 tokens
instead of its base64 char length (~1MB would have registered as
~250K tokens before).
- COMPUTER_USE_GUIDANCE system-prompt block — injected when the toolset
is active.
- Session DB persistence strips base64 from multimodal tool messages.
- Trajectory saver normalises multimodal messages to text-only.
- `hermes tools` post-setup installs cua-driver via the upstream script
and prints permission-grant instructions.
- CLI approval callback wired so destructive computer_use actions go
through the same prompt_toolkit approval dialog as terminal commands.
- Hard safety guards at the tool level: blocked type patterns
(curl|bash, sudo rm -rf, fork bomb), blocked key combos (empty trash,
force delete, lock screen, log out).
- Skill `apple/macos-computer-use/SKILL.md` — universal (model-agnostic)
workflow guide.
- Docs: `user-guide/features/computer-use.md` plus reference catalog
entries.
44 new tests in tests/tools/test_computer_use.py covering schema
shape (universal, not Anthropic-native), dispatch routing, safety
guards, multimodal envelope, Anthropic adapter conversion, screenshot
eviction, context compressor pruning, image-aware token estimation,
run_agent helpers, and universality guarantees.
469/469 pass across tests/tools/test_computer_use.py + the affected
agent/ test suites.
- `model_tools.py` provider-gating: the tool is available to every
provider. Providers without multi-part tool message support will see
text-only tool results (graceful degradation via `text_summary`).
- Anthropic server-side `clear_tool_uses_20250919` — deferred;
client-side eviction + compressor pruning cover the same cost ceiling
without a beta header.
- macOS only. cua-driver uses private SkyLight SPIs
(SLEventPostToPid, SLPSPostEventRecordTo,
_AXObserverAddNotificationAndCheckRemote) that can break on any macOS
update. Pin with HERMES_CUA_DRIVER_VERSION.
- Requires Accessibility + Screen Recording permissions — the post-setup
prints the Settings path.
Supersedes PR #4562 (pyautogui/Quartz foreground backend, Anthropic-
native schema). Credit @0xbyt4 for the original #3816 groundwork whose
context/eviction/token design is preserved here in generic form.
2026-04-23 16:44:24 -07:00
|
|
|
|
blind spot when only counting messages. Image content is counted
|
|
|
|
|
|
at a flat per-image cost (see estimate_messages_tokens_rough).
|
2026-03-26 02:00:50 -07:00
|
|
|
|
"""
|
feat(computer-use): cua-driver backend, universal any-model schema
Background macOS desktop control via cua-driver MCP — does NOT steal the
user's cursor or keyboard focus, works with any tool-capable model.
Replaces the Anthropic-native `computer_20251124` approach from the
abandoned #4562 with a generic OpenAI function-calling schema plus SOM
(set-of-mark) captures so Claude, GPT, Gemini, and open models can all
drive the desktop via numbered element indices.
- `tools/computer_use/` package — swappable ComputerUseBackend ABC +
CuaDriverBackend (stdio MCP client to trycua/cua's cua-driver binary).
- Universal `computer_use` tool with one schema for all providers.
Actions: capture (som/vision/ax), click, double_click, right_click,
middle_click, drag, scroll, type, key, wait, list_apps, focus_app.
- Multimodal tool-result envelope (`_multimodal=True`, OpenAI-style
`content: [text, image_url]` parts) that flows through
handle_function_call into the tool message. Anthropic adapter converts
into native `tool_result` image blocks; OpenAI-compatible providers
get the parts list directly.
- Image eviction in convert_messages_to_anthropic: only the 3 most
recent screenshots carry real image data; older ones become text
placeholders to cap per-turn token cost.
- Context compressor image pruning: old multimodal tool results have
their image parts stripped instead of being skipped.
- Image-aware token estimation: each image counts as a flat 1500 tokens
instead of its base64 char length (~1MB would have registered as
~250K tokens before).
- COMPUTER_USE_GUIDANCE system-prompt block — injected when the toolset
is active.
- Session DB persistence strips base64 from multimodal tool messages.
- Trajectory saver normalises multimodal messages to text-only.
- `hermes tools` post-setup installs cua-driver via the upstream script
and prints permission-grant instructions.
- CLI approval callback wired so destructive computer_use actions go
through the same prompt_toolkit approval dialog as terminal commands.
- Hard safety guards at the tool level: blocked type patterns
(curl|bash, sudo rm -rf, fork bomb), blocked key combos (empty trash,
force delete, lock screen, log out).
- Skill `apple/macos-computer-use/SKILL.md` — universal (model-agnostic)
workflow guide.
- Docs: `user-guide/features/computer-use.md` plus reference catalog
entries.
44 new tests in tests/tools/test_computer_use.py covering schema
shape (universal, not Anthropic-native), dispatch routing, safety
guards, multimodal envelope, Anthropic adapter conversion, screenshot
eviction, context compressor pruning, image-aware token estimation,
run_agent helpers, and universality guarantees.
469/469 pass across tests/tools/test_computer_use.py + the affected
agent/ test suites.
- `model_tools.py` provider-gating: the tool is available to every
provider. Providers without multi-part tool message support will see
text-only tool results (graceful degradation via `text_summary`).
- Anthropic server-side `clear_tool_uses_20250919` — deferred;
client-side eviction + compressor pruning cover the same cost ceiling
without a beta header.
- macOS only. cua-driver uses private SkyLight SPIs
(SLEventPostToPid, SLPSPostEventRecordTo,
_AXObserverAddNotificationAndCheckRemote) that can break on any macOS
update. Pin with HERMES_CUA_DRIVER_VERSION.
- Requires Accessibility + Screen Recording permissions — the post-setup
prints the Settings path.
Supersedes PR #4562 (pyautogui/Quartz foreground backend, Anthropic-
native schema). Credit @0xbyt4 for the original #3816 groundwork whose
context/eviction/token design is preserved here in generic form.
2026-04-23 16:44:24 -07:00
|
|
|
|
total = 0
|
2026-03-26 02:00:50 -07:00
|
|
|
|
if system_prompt:
|
feat(computer-use): cua-driver backend, universal any-model schema
Background macOS desktop control via cua-driver MCP — does NOT steal the
user's cursor or keyboard focus, works with any tool-capable model.
Replaces the Anthropic-native `computer_20251124` approach from the
abandoned #4562 with a generic OpenAI function-calling schema plus SOM
(set-of-mark) captures so Claude, GPT, Gemini, and open models can all
drive the desktop via numbered element indices.
- `tools/computer_use/` package — swappable ComputerUseBackend ABC +
CuaDriverBackend (stdio MCP client to trycua/cua's cua-driver binary).
- Universal `computer_use` tool with one schema for all providers.
Actions: capture (som/vision/ax), click, double_click, right_click,
middle_click, drag, scroll, type, key, wait, list_apps, focus_app.
- Multimodal tool-result envelope (`_multimodal=True`, OpenAI-style
`content: [text, image_url]` parts) that flows through
handle_function_call into the tool message. Anthropic adapter converts
into native `tool_result` image blocks; OpenAI-compatible providers
get the parts list directly.
- Image eviction in convert_messages_to_anthropic: only the 3 most
recent screenshots carry real image data; older ones become text
placeholders to cap per-turn token cost.
- Context compressor image pruning: old multimodal tool results have
their image parts stripped instead of being skipped.
- Image-aware token estimation: each image counts as a flat 1500 tokens
instead of its base64 char length (~1MB would have registered as
~250K tokens before).
- COMPUTER_USE_GUIDANCE system-prompt block — injected when the toolset
is active.
- Session DB persistence strips base64 from multimodal tool messages.
- Trajectory saver normalises multimodal messages to text-only.
- `hermes tools` post-setup installs cua-driver via the upstream script
and prints permission-grant instructions.
- CLI approval callback wired so destructive computer_use actions go
through the same prompt_toolkit approval dialog as terminal commands.
- Hard safety guards at the tool level: blocked type patterns
(curl|bash, sudo rm -rf, fork bomb), blocked key combos (empty trash,
force delete, lock screen, log out).
- Skill `apple/macos-computer-use/SKILL.md` — universal (model-agnostic)
workflow guide.
- Docs: `user-guide/features/computer-use.md` plus reference catalog
entries.
44 new tests in tests/tools/test_computer_use.py covering schema
shape (universal, not Anthropic-native), dispatch routing, safety
guards, multimodal envelope, Anthropic adapter conversion, screenshot
eviction, context compressor pruning, image-aware token estimation,
run_agent helpers, and universality guarantees.
469/469 pass across tests/tools/test_computer_use.py + the affected
agent/ test suites.
- `model_tools.py` provider-gating: the tool is available to every
provider. Providers without multi-part tool message support will see
text-only tool results (graceful degradation via `text_summary`).
- Anthropic server-side `clear_tool_uses_20250919` — deferred;
client-side eviction + compressor pruning cover the same cost ceiling
without a beta header.
- macOS only. cua-driver uses private SkyLight SPIs
(SLEventPostToPid, SLPSPostEventRecordTo,
_AXObserverAddNotificationAndCheckRemote) that can break on any macOS
update. Pin with HERMES_CUA_DRIVER_VERSION.
- Requires Accessibility + Screen Recording permissions — the post-setup
prints the Settings path.
Supersedes PR #4562 (pyautogui/Quartz foreground backend, Anthropic-
native schema). Credit @0xbyt4 for the original #3816 groundwork whose
context/eviction/token design is preserved here in generic form.
2026-04-23 16:44:24 -07:00
|
|
|
|
total += (len(system_prompt) + 3) // 4
|
2026-03-26 02:00:50 -07:00
|
|
|
|
if messages:
|
feat(computer-use): cua-driver backend, universal any-model schema
Background macOS desktop control via cua-driver MCP — does NOT steal the
user's cursor or keyboard focus, works with any tool-capable model.
Replaces the Anthropic-native `computer_20251124` approach from the
abandoned #4562 with a generic OpenAI function-calling schema plus SOM
(set-of-mark) captures so Claude, GPT, Gemini, and open models can all
drive the desktop via numbered element indices.
- `tools/computer_use/` package — swappable ComputerUseBackend ABC +
CuaDriverBackend (stdio MCP client to trycua/cua's cua-driver binary).
- Universal `computer_use` tool with one schema for all providers.
Actions: capture (som/vision/ax), click, double_click, right_click,
middle_click, drag, scroll, type, key, wait, list_apps, focus_app.
- Multimodal tool-result envelope (`_multimodal=True`, OpenAI-style
`content: [text, image_url]` parts) that flows through
handle_function_call into the tool message. Anthropic adapter converts
into native `tool_result` image blocks; OpenAI-compatible providers
get the parts list directly.
- Image eviction in convert_messages_to_anthropic: only the 3 most
recent screenshots carry real image data; older ones become text
placeholders to cap per-turn token cost.
- Context compressor image pruning: old multimodal tool results have
their image parts stripped instead of being skipped.
- Image-aware token estimation: each image counts as a flat 1500 tokens
instead of its base64 char length (~1MB would have registered as
~250K tokens before).
- COMPUTER_USE_GUIDANCE system-prompt block — injected when the toolset
is active.
- Session DB persistence strips base64 from multimodal tool messages.
- Trajectory saver normalises multimodal messages to text-only.
- `hermes tools` post-setup installs cua-driver via the upstream script
and prints permission-grant instructions.
- CLI approval callback wired so destructive computer_use actions go
through the same prompt_toolkit approval dialog as terminal commands.
- Hard safety guards at the tool level: blocked type patterns
(curl|bash, sudo rm -rf, fork bomb), blocked key combos (empty trash,
force delete, lock screen, log out).
- Skill `apple/macos-computer-use/SKILL.md` — universal (model-agnostic)
workflow guide.
- Docs: `user-guide/features/computer-use.md` plus reference catalog
entries.
44 new tests in tests/tools/test_computer_use.py covering schema
shape (universal, not Anthropic-native), dispatch routing, safety
guards, multimodal envelope, Anthropic adapter conversion, screenshot
eviction, context compressor pruning, image-aware token estimation,
run_agent helpers, and universality guarantees.
469/469 pass across tests/tools/test_computer_use.py + the affected
agent/ test suites.
- `model_tools.py` provider-gating: the tool is available to every
provider. Providers without multi-part tool message support will see
text-only tool results (graceful degradation via `text_summary`).
- Anthropic server-side `clear_tool_uses_20250919` — deferred;
client-side eviction + compressor pruning cover the same cost ceiling
without a beta header.
- macOS only. cua-driver uses private SkyLight SPIs
(SLEventPostToPid, SLPSPostEventRecordTo,
_AXObserverAddNotificationAndCheckRemote) that can break on any macOS
update. Pin with HERMES_CUA_DRIVER_VERSION.
- Requires Accessibility + Screen Recording permissions — the post-setup
prints the Settings path.
Supersedes PR #4562 (pyautogui/Quartz foreground backend, Anthropic-
native schema). Credit @0xbyt4 for the original #3816 groundwork whose
context/eviction/token design is preserved here in generic form.
2026-04-23 16:44:24 -07:00
|
|
|
|
total += estimate_messages_tokens_rough(messages)
|
2026-03-26 02:00:50 -07:00
|
|
|
|
if tools:
|
feat(computer-use): cua-driver backend, universal any-model schema
Background macOS desktop control via cua-driver MCP — does NOT steal the
user's cursor or keyboard focus, works with any tool-capable model.
Replaces the Anthropic-native `computer_20251124` approach from the
abandoned #4562 with a generic OpenAI function-calling schema plus SOM
(set-of-mark) captures so Claude, GPT, Gemini, and open models can all
drive the desktop via numbered element indices.
- `tools/computer_use/` package — swappable ComputerUseBackend ABC +
CuaDriverBackend (stdio MCP client to trycua/cua's cua-driver binary).
- Universal `computer_use` tool with one schema for all providers.
Actions: capture (som/vision/ax), click, double_click, right_click,
middle_click, drag, scroll, type, key, wait, list_apps, focus_app.
- Multimodal tool-result envelope (`_multimodal=True`, OpenAI-style
`content: [text, image_url]` parts) that flows through
handle_function_call into the tool message. Anthropic adapter converts
into native `tool_result` image blocks; OpenAI-compatible providers
get the parts list directly.
- Image eviction in convert_messages_to_anthropic: only the 3 most
recent screenshots carry real image data; older ones become text
placeholders to cap per-turn token cost.
- Context compressor image pruning: old multimodal tool results have
their image parts stripped instead of being skipped.
- Image-aware token estimation: each image counts as a flat 1500 tokens
instead of its base64 char length (~1MB would have registered as
~250K tokens before).
- COMPUTER_USE_GUIDANCE system-prompt block — injected when the toolset
is active.
- Session DB persistence strips base64 from multimodal tool messages.
- Trajectory saver normalises multimodal messages to text-only.
- `hermes tools` post-setup installs cua-driver via the upstream script
and prints permission-grant instructions.
- CLI approval callback wired so destructive computer_use actions go
through the same prompt_toolkit approval dialog as terminal commands.
- Hard safety guards at the tool level: blocked type patterns
(curl|bash, sudo rm -rf, fork bomb), blocked key combos (empty trash,
force delete, lock screen, log out).
- Skill `apple/macos-computer-use/SKILL.md` — universal (model-agnostic)
workflow guide.
- Docs: `user-guide/features/computer-use.md` plus reference catalog
entries.
44 new tests in tests/tools/test_computer_use.py covering schema
shape (universal, not Anthropic-native), dispatch routing, safety
guards, multimodal envelope, Anthropic adapter conversion, screenshot
eviction, context compressor pruning, image-aware token estimation,
run_agent helpers, and universality guarantees.
469/469 pass across tests/tools/test_computer_use.py + the affected
agent/ test suites.
- `model_tools.py` provider-gating: the tool is available to every
provider. Providers without multi-part tool message support will see
text-only tool results (graceful degradation via `text_summary`).
- Anthropic server-side `clear_tool_uses_20250919` — deferred;
client-side eviction + compressor pruning cover the same cost ceiling
without a beta header.
- macOS only. cua-driver uses private SkyLight SPIs
(SLEventPostToPid, SLPSPostEventRecordTo,
_AXObserverAddNotificationAndCheckRemote) that can break on any macOS
update. Pin with HERMES_CUA_DRIVER_VERSION.
- Requires Accessibility + Screen Recording permissions — the post-setup
prints the Settings path.
Supersedes PR #4562 (pyautogui/Quartz foreground backend, Anthropic-
native schema). Credit @0xbyt4 for the original #3816 groundwork whose
context/eviction/token design is preserved here in generic form.
2026-04-23 16:44:24 -07:00
|
|
|
|
total += (len(str(tools)) + 3) // 4
|
|
|
|
|
|
return total
|