diff --git a/api/config.py b/api/config.py index 6742652..ba6b5de 100644 --- a/api/config.py +++ b/api/config.py @@ -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: diff --git a/tests/test_model_resolver.py b/tests/test_model_resolver.py new file mode 100644 index 0000000..a80ff62 --- /dev/null +++ b/tests/test_model_resolver.py @@ -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'