From 2864c2b6917f3bcaa2676dad6f95404422f1f14b Mon Sep 17 00:00:00 2001 From: deboste Date: Tue, 31 Mar 2026 14:35:54 +0000 Subject: [PATCH 1/2] fix(api): resolve model provider from config to prevent misrouting When the model dropdown sends a prefixed ID like "anthropic/claude-xxx", AIAgent interprets the provider/model format as an OpenRouter path and routes through OpenRouter instead of the direct Anthropic API. Fix: read the configured provider from config.yaml model section. If the model ID starts with the configured provider name followed by "/", strip that prefix and pass the provider explicitly to AIAgent. This ensures direct API providers (Anthropic, OpenAI, etc.) are used when configured, regardless of the model ID format from the dropdown. --- api/routes.py | 10 +++++++++- api/streaming.py | 11 +++++++++++ 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/api/routes.py b/api/routes.py index 190bcd7..0dc968e 100644 --- a/api/routes.py +++ b/api/routes.py @@ -629,7 +629,15 @@ def _handle_chat_sync(handler, body): try: from run_agent import AIAgent with CHAT_LOCK: - agent = AIAgent(model=s.model, platform='cli', quiet_mode=True, + from api.config import cfg as _hcfg + _model = s.model or '' + _prov = None + _mc = _hcfg.get('model', {}) + if isinstance(_mc, dict): + _prov = _mc.get('provider') + if _prov and '/' in _model and _model.startswith(_prov + '/'): + _model = _model.split('/', 1)[1] + agent = AIAgent(model=_model, provider=_prov, platform='cli', quiet_mode=True, enabled_toolsets=CLI_TOOLSETS, session_id=s.session_id) workspace_ctx = f"[Workspace: {s.workspace}]\n" workspace_system_msg = ( diff --git a/api/streaming.py b/api/streaming.py index 25a66a1..be42a10 100644 --- a/api/streaming.py +++ b/api/streaming.py @@ -13,6 +13,7 @@ from pathlib import Path from api.config import ( STREAMS, STREAMS_LOCK, CANCEL_FLAGS, CLI_TOOLSETS, _get_session_agent_lock, _set_thread_env, _clear_thread_env, + cfg as _hermes_cfg, ) # Lazy import to avoid circular deps -- hermes-agent is on sys.path via api/config.py @@ -99,8 +100,18 @@ def _run_agent_streaming(session_id, msg_text, model, workspace, stream_id, atta if AIAgent is None: raise ImportError("AIAgent not available -- check that hermes-agent is on sys.path") + # Resolve provider from config so agent routes to the right API + _provider = None + model_cfg = _hermes_cfg.get('model', {}) + if isinstance(model_cfg, dict): + _provider = model_cfg.get('provider') + # If model has provider/ prefix matching config provider, strip it + # so AIAgent doesn't misroute to OpenRouter + if _provider and '/' in model and model.startswith(_provider + '/'): + model = model.split('/', 1)[1] agent = AIAgent( model=model, + provider=_provider, platform='cli', quiet_mode=True, enabled_toolsets=CLI_TOOLSETS, From 241357595d5f2f7b700f86d6c4bc6904a80b2abb Mon Sep 17 00:00:00 2001 From: Nathan Esquenazi Date: Wed, 1 Apr 2026 22:56:34 -0700 Subject: [PATCH 2/2] refactor: extract resolve_model_provider helper, fix cross-provider routing Replace duplicated inline provider resolution in routes.py and streaming.py with a shared resolve_model_provider() helper in config.py. Improvements over original: - If model ID has a prefix matching any known direct-API provider (not just the config provider), strip it and route correctly. This handles edge cases like localStorage restoring a model from a different provider group. - Single source of truth for the resolution logic. Co-Authored-By: Claude Opus 4.6 (1M context) --- api/config.py | 30 ++++++++++++++++++++++++++++++ api/routes.py | 12 +++--------- api/streaming.py | 16 ++++------------ 3 files changed, 37 insertions(+), 21 deletions(-) diff --git a/api/config.py b/api/config.py index b78522d..2bc126e 100644 --- a/api/config.py +++ b/api/config.py @@ -312,6 +312,36 @@ _PROVIDER_MODELS = { } +def resolve_model_provider(model_id: str): + """Resolve bare model name and provider for AIAgent. + + Model IDs from the dropdown may include a provider prefix + (e.g. 'anthropic/claude-sonnet-4.6'). Direct-API providers expect + bare model names, while OpenRouter expects the full provider/model path. + + Returns (model, provider) where provider may be None. + """ + config_provider = None + model_cfg = cfg.get('model', {}) + if isinstance(model_cfg, dict): + config_provider = model_cfg.get('provider') + + model_id = (model_id or '').strip() + if not model_id: + return model_id, config_provider + + if '/' in model_id: + prefix, bare = model_id.split('/', 1) + # If prefix matches config provider, strip it + if config_provider and prefix == config_provider: + return bare, config_provider + # If prefix is a known direct-API provider, use it + if prefix in _PROVIDER_MODELS: + return bare, prefix + + return model_id, config_provider + + def get_available_models() -> dict: """ Return available models grouped by provider. diff --git a/api/routes.py b/api/routes.py index 0dc968e..3c55f4e 100644 --- a/api/routes.py +++ b/api/routes.py @@ -629,15 +629,9 @@ def _handle_chat_sync(handler, body): try: from run_agent import AIAgent with CHAT_LOCK: - from api.config import cfg as _hcfg - _model = s.model or '' - _prov = None - _mc = _hcfg.get('model', {}) - if isinstance(_mc, dict): - _prov = _mc.get('provider') - if _prov and '/' in _model and _model.startswith(_prov + '/'): - _model = _model.split('/', 1)[1] - agent = AIAgent(model=_model, provider=_prov, platform='cli', quiet_mode=True, + from api.config import resolve_model_provider + _model, _provider = resolve_model_provider(s.model) + agent = AIAgent(model=_model, provider=_provider, platform='cli', quiet_mode=True, enabled_toolsets=CLI_TOOLSETS, session_id=s.session_id) workspace_ctx = f"[Workspace: {s.workspace}]\n" workspace_system_msg = ( diff --git a/api/streaming.py b/api/streaming.py index be42a10..ec0ea5f 100644 --- a/api/streaming.py +++ b/api/streaming.py @@ -13,7 +13,7 @@ from pathlib import Path from api.config import ( STREAMS, STREAMS_LOCK, CANCEL_FLAGS, CLI_TOOLSETS, _get_session_agent_lock, _set_thread_env, _clear_thread_env, - cfg as _hermes_cfg, + resolve_model_provider, ) # Lazy import to avoid circular deps -- hermes-agent is on sys.path via api/config.py @@ -100,18 +100,10 @@ def _run_agent_streaming(session_id, msg_text, model, workspace, stream_id, atta if AIAgent is None: raise ImportError("AIAgent not available -- check that hermes-agent is on sys.path") - # Resolve provider from config so agent routes to the right API - _provider = None - model_cfg = _hermes_cfg.get('model', {}) - if isinstance(model_cfg, dict): - _provider = model_cfg.get('provider') - # If model has provider/ prefix matching config provider, strip it - # so AIAgent doesn't misroute to OpenRouter - if _provider and '/' in model and model.startswith(_provider + '/'): - model = model.split('/', 1)[1] + resolved_model, resolved_provider = resolve_model_provider(model) agent = AIAgent( - model=model, - provider=_provider, + model=resolved_model, + provider=resolved_provider, platform='cli', quiet_mode=True, enabled_toolsets=CLI_TOOLSETS,