-
Notifications
You must be signed in to change notification settings - Fork 52.4k
feat: per-provider max_tokens via custom_providers models.<model>.max_tokens #28786
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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.<model>.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: | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Because |
||
| 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") | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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 | ||
|
|
||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This helper mirrors context_length lookup, but max_tokens has a stronger non-leakage requirement across fallback/switch paths. It should probably be part of a shared max_tokens resolution path rather than only called from startup. |
||
|
|
||
| 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.<model>.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. | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Resolving this only during init makes
agent.max_tokensa sticky session-wide value; fallback activation and/modelswitching later change provider/model without recomputing this cap, so the primary provider's cap can still leak onto fallback providers.