Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions agent/agent_init.py
Original file line number Diff line number Diff line change
Expand Up @@ -1165,6 +1165,23 @@ def init_agent(
)

Copy link
Copy Markdown
Collaborator

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_tokens a sticky session-wide value; fallback activation and /model switching later change provider/model without recomputing this cap, so the primary provider's cap can still leak onto fallback providers.

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:

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because _session_init_model_config["max_tokens"] was written just above this block, the session metadata will still record None when this nested provider cap is applied. Either resolve before recording the metadata or update the metadata after assigning agent.max_tokens.

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")
Expand Down
62 changes: 61 additions & 1 deletion hermes_cli/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
}
Expand Down Expand Up @@ -3229,6 +3229,66 @@ def get_custom_provider_context_length(
return None

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The 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.
Expand Down
Loading