fix: OpenRouter models stripped of prefix, causing 404 (#116)
When config has provider=openrouter and model=openrouter/free, resolve_model_provider() stripped the 'openrouter/' prefix because prefix == config_provider. This sent 'free' to OpenRouter's API, which returned 404 (model not found). OpenRouter always needs the full provider/model path (e.g. openrouter/free, anthropic/claude-sonnet-4.6). The prefix-stripping logic is only correct for direct-API providers. Fix: skip prefix stripping entirely when config_provider is 'openrouter'. Return the full model_id with provider='openrouter'. Added 7 unit tests for resolve_model_provider() covering: - openrouter/free keeps full path (the bug) - openrouter cross-provider models keep full path - direct API providers still strip prefix correctly - cross-provider routing to openrouter - bare model names use config provider - empty model returns defaults Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -391,6 +391,10 @@ def resolve_model_provider(model_id: str) -> tuple:
|
||||
|
||||
if '/' in model_id:
|
||||
prefix, bare = model_id.split('/', 1)
|
||||
# OpenRouter always needs the full provider/model path (e.g. openrouter/free,
|
||||
# anthropic/claude-sonnet-4.6). Never strip the prefix for OpenRouter.
|
||||
if config_provider == 'openrouter':
|
||||
return model_id, 'openrouter', config_base_url
|
||||
# If prefix matches config provider exactly, strip it and use that provider directly.
|
||||
# e.g. config=anthropic, model=anthropic/claude-... → bare name to anthropic API
|
||||
if config_provider and prefix == config_provider:
|
||||
|
||||
100
tests/test_model_resolver.py
Normal file
100
tests/test_model_resolver.py
Normal file
@@ -0,0 +1,100 @@
|
||||
"""
|
||||
Tests for resolve_model_provider() model routing logic.
|
||||
Verifies that model IDs are correctly resolved to (model, provider, base_url)
|
||||
tuples for different provider configurations.
|
||||
"""
|
||||
import api.config as config
|
||||
|
||||
|
||||
def _resolve_with_config(model_id, provider=None, base_url=None, default=None):
|
||||
"""Helper: temporarily set config.cfg model section, call resolve, restore."""
|
||||
old_cfg = dict(config.cfg)
|
||||
model_cfg = {}
|
||||
if provider:
|
||||
model_cfg['provider'] = provider
|
||||
if base_url:
|
||||
model_cfg['base_url'] = base_url
|
||||
if default:
|
||||
model_cfg['default'] = default
|
||||
config.cfg['model'] = model_cfg if model_cfg else {}
|
||||
try:
|
||||
return config.resolve_model_provider(model_id)
|
||||
finally:
|
||||
config.cfg.clear()
|
||||
config.cfg.update(old_cfg)
|
||||
|
||||
|
||||
# ── OpenRouter prefix handling ────────────────────────────────────────────
|
||||
|
||||
def test_openrouter_free_keeps_full_path():
|
||||
"""openrouter/free must NOT be stripped to 'free' when provider is openrouter."""
|
||||
model, provider, base_url = _resolve_with_config(
|
||||
'openrouter/free', provider='openrouter',
|
||||
base_url='https://openrouter.ai/api/v1',
|
||||
)
|
||||
assert model == 'openrouter/free', f"Expected 'openrouter/free', got '{model}'"
|
||||
assert provider == 'openrouter'
|
||||
|
||||
|
||||
def test_openrouter_model_with_provider_prefix():
|
||||
"""anthropic/claude-sonnet-4.6 via openrouter keeps full path."""
|
||||
model, provider, base_url = _resolve_with_config(
|
||||
'anthropic/claude-sonnet-4.6', provider='openrouter',
|
||||
base_url='https://openrouter.ai/api/v1',
|
||||
)
|
||||
assert model == 'anthropic/claude-sonnet-4.6'
|
||||
assert provider == 'openrouter'
|
||||
|
||||
|
||||
# ── Direct provider prefix stripping ─────────────────────────────────────
|
||||
|
||||
def test_anthropic_prefix_stripped_for_direct_api():
|
||||
"""anthropic/claude-sonnet-4.6 strips prefix when provider is anthropic."""
|
||||
model, provider, base_url = _resolve_with_config(
|
||||
'anthropic/claude-sonnet-4.6', provider='anthropic',
|
||||
)
|
||||
assert model == 'claude-sonnet-4.6'
|
||||
assert provider == 'anthropic'
|
||||
|
||||
|
||||
def test_openai_prefix_stripped_for_direct_api():
|
||||
"""openai/gpt-5.4-mini strips prefix when provider is openai."""
|
||||
model, provider, base_url = _resolve_with_config(
|
||||
'openai/gpt-5.4-mini', provider='openai',
|
||||
)
|
||||
assert model == 'gpt-5.4-mini'
|
||||
assert provider == 'openai'
|
||||
|
||||
|
||||
# ── Cross-provider routing ───────────────────────────────────────────────
|
||||
|
||||
def test_cross_provider_routes_through_openrouter():
|
||||
"""Picking openai model when config is anthropic routes via openrouter."""
|
||||
model, provider, base_url = _resolve_with_config(
|
||||
'openai/gpt-5.4-mini', provider='anthropic',
|
||||
)
|
||||
assert model == 'openai/gpt-5.4-mini'
|
||||
assert provider == 'openrouter'
|
||||
assert base_url is None # openrouter uses its own endpoint
|
||||
|
||||
|
||||
# ── Bare model names ─────────────────────────────────────────────────────
|
||||
|
||||
def test_bare_model_uses_config_provider():
|
||||
"""A model name without / uses the config provider and base_url."""
|
||||
model, provider, base_url = _resolve_with_config(
|
||||
'gemma-4-26B', provider='custom',
|
||||
base_url='http://192.168.1.160:4000',
|
||||
)
|
||||
assert model == 'gemma-4-26B'
|
||||
assert provider == 'custom'
|
||||
assert base_url == 'http://192.168.1.160:4000'
|
||||
|
||||
|
||||
def test_empty_model_returns_config_defaults():
|
||||
"""Empty model string returns config provider and base_url."""
|
||||
model, provider, base_url = _resolve_with_config(
|
||||
'', provider='anthropic',
|
||||
)
|
||||
assert model == ''
|
||||
assert provider == 'anthropic'
|
||||
Reference in New Issue
Block a user