From e873dbafb30da2fd3d140bef1830e9c884deebdf Mon Sep 17 00:00:00 2001 From: liyifan Date: Tue, 19 May 2026 22:49:18 +0800 Subject: [PATCH] feat: support per-provider max_tokens via custom_providers[].models..max_tokens Adds a new lookup function get_custom_provider_max_tokens() parallel to the existing get_custom_provider_context_length(). When model.max_tokens is unset globally, the agent init path now falls back to checking custom_providers for a per-provider max_tokens override, matched by base_url + model name. This allows users to set a provider-scoped output-token cap without affecting fallback providers in the chain. Closes #28782 --- agent/agent_init.py | 17 ++++++++++++ hermes_cli/config.py | 62 +++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 78 insertions(+), 1 deletion(-) diff --git a/agent/agent_init.py b/agent/agent_init.py index a5d27c0b73d05..9391e4ddf8e09 100644 --- a/agent/agent_init.py +++ b/agent/agent_init.py @@ -1165,6 +1165,23 @@ def init_agent( ) agent._session_init_model_config["max_tokens"] = agent.max_tokens + # If max_tokens is still unset, check custom_providers for a per-provider + # override (e.g. custom_providers[].models..max_tokens). + # This allows provider-scoped output-token caps without the global + # model.max_tokens affecting fallback providers. + if agent.max_tokens is None: + try: + from hermes_cli.config import get_compatible_custom_providers, get_custom_provider_max_tokens + _cp_max_tokens = get_custom_provider_max_tokens( + model=agent.model, + base_url=agent.base_url, + custom_providers=get_compatible_custom_providers(_agent_cfg), + ) + if _cp_max_tokens is not None: + agent.max_tokens = int(_cp_max_tokens) + except Exception: + pass + # Read explicit context_length override from model config if isinstance(_model_cfg, dict): _config_context_length = _model_cfg.get("context_length") diff --git a/hermes_cli/config.py b/hermes_cli/config.py index ce3ddd54108aa..a3b06527eb0e8 100644 --- a/hermes_cli/config.py +++ b/hermes_cli/config.py @@ -3000,7 +3000,7 @@ def _normalize_custom_provider_entry( _KNOWN_KEYS = { "name", "api", "url", "base_url", "api_key", "key_env", "api_key_env", "api_mode", "transport", "model", "default_model", "models", - "context_length", "rate_limit_delay", + "context_length", "max_tokens", "rate_limit_delay", "request_timeout_seconds", "stale_timeout_seconds", "discover_models", } @@ -3229,6 +3229,66 @@ def get_custom_provider_context_length( return None +def get_custom_provider_max_tokens( + model: str, + base_url: str, + custom_providers: Optional[List[Dict[str, Any]]] = None, + config: Optional[Dict[str, Any]] = None, +) -> Optional[int]: + """Look up a per-model ``max_tokens`` override from ``custom_providers``. + + Matches any entry whose ``base_url`` equals ``base_url`` (trailing-slash + insensitive) and returns ``custom_providers[i].models..max_tokens`` + if present and valid. Returns ``None`` when no override applies. + + Mirrors ``get_custom_provider_context_length`` — the same pattern for + output-token caps that already exists for context windows. + + Used by: + * ``AIAgent.__init__`` (startup resolution, after the global + ``model.max_tokens`` fallback) + """ + if not model or not base_url: + return None + if custom_providers is None: + try: + custom_providers = get_compatible_custom_providers(config) + except Exception: + if config is None: + return None + raw = config.get("custom_providers") + custom_providers = raw if isinstance(raw, list) else [] + if not isinstance(custom_providers, list): + return None + + target_url = (base_url or "").rstrip("/") + if not target_url: + return None + + for entry in custom_providers: + if not isinstance(entry, dict): + continue + entry_url = (entry.get("base_url") or "").rstrip("/") + if not entry_url or entry_url != target_url: + continue + models = entry.get("models") + if not isinstance(models, dict): + continue + model_cfg = models.get(model) + if not isinstance(model_cfg, dict): + continue + raw_mt = model_cfg.get("max_tokens") + if raw_mt is None: + continue + try: + mt = int(raw_mt) + except (TypeError, ValueError): + continue + if mt > 0: + return mt + return None + + def check_config_version() -> Tuple[int, int]: """ Check config version.