{"repo": "brosax/webui", "files": {"tests/test_model_resolver.py": {"e1a27ebf8a13d6b8fa3b8edaa87f00c553e6c262": {"t": 1785846972.918118, "n": 24429, "h": "16b27a1142db3d90", "c": "\"\"\"\nTests for resolve_model_provider() model routing logic.\nVerifies that model IDs are correctly resolved to (model, provider, base_url)\ntuples for different provider configurations.\n\"\"\"\nimport pytest\nimport api.config as config\n\n\ndef _resolve_with_config(model_id, provider=None, base_url=None, default=None, custom_providers=None):\n \"\"\"Helper: temporarily set config.cfg model/custom provider sections, call resolve, restore.\"\"\"\n old_cfg = dict(config.cfg)\n model_cfg = {}\n if provider:\n model_cfg['provider'] = provider\n if base_url:\n model_cfg['base_url'] = base_url\n if default:\n model_cfg['default'] = default\n config.cfg['model'] = model_cfg if model_cfg else {}\n if custom_providers is not None:\n config.cfg['custom_providers'] = custom_providers\n try:\n return config.resolve_model_provider(model_id)\n finally:\n config.cfg.clear()\n config.cfg.update(old_cfg)\n\n\n# ── OpenRouter prefix handling ────────────────────────────────────────────\n\ndef test_openrouter_free_keeps_full_path():\n \"\"\"openrouter/free must NOT be stripped to 'free' when provider is openrouter.\"\"\"\n model, provider, base_url = _resolve_with_config(\n 'openrouter/free', provider='openrouter',\n base_url='https://openrouter.ai/api/v1',\n )\n assert model == 'openrouter/free', f\"Expected 'openrouter/free', got '{model}'\"\n assert provider == 'openrouter'\n\n\ndef test_openrouter_model_with_provider_prefix():\n \"\"\"anthropic/claude-sonnet-4.6 via openrouter keeps full path.\"\"\"\n model, provider, base_url = _resolve_with_config(\n 'anthropic/claude-sonnet-4.6', provider='openrouter',\n base_url='https://openrouter.ai/api/v1',\n )\n assert model == 'anthropic/claude-sonnet-4.6'\n assert provider == 'openrouter'\n\n\n# ── Direct provider prefix stripping ─────────────────────────────────────\n\ndef test_anthropic_prefix_stripped_for_direct_api():\n \"\"\"anthropic/claude-sonnet-4.6 strips prefix when provider is anthropic.\"\"\"\n model, provider, base_url = _resolve_with_config(\n 'anthropic/claude-sonnet-4.6', provider='anthropic',\n )\n assert model == 'claude-sonnet-4.6'\n assert provider == 'anthropic'\n\n\ndef test_openai_prefix_stripped_for_direct_api():\n \"\"\"openai/gpt-5.4-mini strips prefix when provider is openai.\"\"\"\n model, provider, base_url = _resolve_with_config(\n 'openai/gpt-5.4-mini', provider='openai',\n )\n assert model == 'gpt-5.4-mini'\n assert provider == 'openai'\n\n\n# ── Cross-provider routing ───────────────────────────────────────────────\n\ndef test_cross_provider_routes_through_openrouter():\n \"\"\"Picking openai model when config is anthropic routes via openrouter.\"\"\"\n model, provider, base_url = _resolve_with_config(\n 'openai/gpt-5.4-mini', provider='anthropic',\n )\n assert model == 'openai/gpt-5.4-mini'\n assert provider == 'openrouter'\n assert base_url is None # openrouter uses its own endpoint\n\n\n# ── Bare model names ─────────────────────────────────────────────────────\n\ndef test_bare_model_uses_config_provider():\n \"\"\"A model name without / uses the config provider and base_url.\"\"\"\n model, provider, base_url = _resolve_with_config(\n 'gemma-4-26B', provider='custom',\n base_url='http://192.168.1.160:4000',\n )\n assert model == 'gemma-4-26B'\n assert provider == 'custom'\n assert base_url == 'http://192.168.1.160:4000'\n\n\ndef test_empty_model_returns_config_defaults():\n \"\"\"Empty model string returns config provider and base_url.\"\"\"\n model, provider, base_url = _resolve_with_config(\n '', provider='anthropic',\n )\n assert model == ''\n assert provider == 'anthropic'\n\n\n# ── @provider:model hint routing (Issue #138 v2) ────────────────────────\n\ndef test_provider_hint_routes_to_specific_provider():\n \"\"\"@minimax:MiniMax-M2.7 routes to minimax provider directly.\"\"\"\n model, provider, base_url = _resolve_with_config(\n '@minimax:MiniMax-M2.7', provider='anthropic',\n )\n assert model == 'MiniMax-M2.7'\n assert provider == 'minimax'\n assert base_url is None # resolve_runtime_provider will fill this\n\n\ndef test_provider_hint_zai():\n \"\"\"@zai:GLM-5 routes to zai provider directly.\"\"\"\n model, provider, base_url = _resolve_with_config(\n '@zai:GLM-5', provider='openai',\n )\n assert model == 'GLM-5'\n assert provider == 'zai'\n\n\ndef test_provider_hint_deepseek():\n \"\"\"@deepseek:deepseek-chat routes to deepseek provider.\"\"\"\n model, provider, base_url = _resolve_with_config(\n '@deepseek:deepseek-chat', provider='anthropic',\n )\n assert model == 'deepseek-chat'\n assert provider == 'deepseek'\n\n\ndef test_slash_prefix_non_default_still_routes_openrouter():\n \"\"\"minimax/MiniMax-M2.7 (old format) still routes through openrouter.\"\"\"\n model, provider, base_url = _resolve_with_config(\n 'minimax/MiniMax-M2.7', provider='anthropic',\n )\n assert model == 'minimax/MiniMax-M2.7'\n assert provider == 'openrouter'\n\n\ndef test_custom_provider_model_with_slash_routes_to_named_custom_provider():\n \"\"\"Slash-containing custom endpoint model IDs must not be mistaken for OpenRouter models.\"\"\"\n model, provider, base_url = _resolve_with_config(\n 'google/gemma-4-26b-a4b',\n provider='openrouter',\n base_url='https://openrouter.ai/api/v1',\n custom_providers=[{\n 'name': 'Local LM Studio',\n 'base_url': 'http://lmstudio.local:1234/v1',\n 'model': 'google/gemma-4-26b-a4b',\n }],\n )\n assert model == 'google/gemma-4-26b-a4b'\n assert provider == 'custom:local-lm-studio'\n assert base_url == 'http://lmstudio.local:1234/v1'\n\n\ndef test_custom_provider_models_dict_routes_to_named_custom_provider():\n \"\"\"Models listed only under custom_providers[].models still route to that endpoint.\"\"\"\n model, provider, base_url = _resolve_with_config(\n 'sensenova-6.7-flash-lite',\n provider='xiaomi',\n custom_providers=[{\n 'name': 'LiteLLM Proxy',\n 'base_url': 'http://127.0.0.1:8080/v1',\n 'model': 'deepseek-v4-flash',\n 'models': {\n 'deepseek-v4-flash': {},\n 'sensenova-6.7-flash-lite': {},\n },\n }],\n )\n assert model == 'sensenova-6.7-flash-lite'\n assert provider == 'custom:litellm-proxy'\n assert base_url == 'http://127.0.0.1:8080/v1'\n\n\n# ── Issue #1922: default model shadowed by overlapping custom_providers[] ──\n\ndef test_default_model_not_shadowed_by_overlapping_custom_provider():\n r'''Regression test for #1922.\n\n When the active provider is an explicit non-custom provider (e.g. ai-gateway,\n openrouter, xiaomi) AND the requested model_id matches the configured default\n model, the active provider's base_url must take precedence over an overlapping\n custom_providers[] entry. Otherwise the WebUI routes to 'custom:' with\n the wrong endpoint, causing 401 errors.\n\n This test mirrors the reported scenario:\n - provider: ai-gateway\n - base_url: https://api.ai-gateway.example/v1\n - default: gpt-5.4\n - An overlapping custom_providers[] entry with the same default model\n\n Expected: active provider (ai-gateway) wins over custom provider.\n '''\n model, provider, base_url = _resolve_with_config(\n 'gpt-5.4',\n provider='ai-gateway',\n base_url='https://api.ai-gateway.example/v1',\n default='gpt-5.4',\n custom_providers=[{\n 'name': 'My Custom Endpoint',\n 'base_url': 'http://localhost:8080/v1',\n 'model': 'gpt-5.4',\n }],\n )\n assert model == 'gpt-5.4', f'Expected model=gpt-5.4, got {model!r}'\n assert provider == 'ai-gateway', f'Expected provider=ai-gateway, got {provider!r}'\n assert base_url == 'https://api.ai-gateway.example/v1', f'Expected base_url from active provider, got {base_url!r}'\n\n\ndef test_default_model_shadowed_with_xiaomi_provider():\n r'''Same regression test with provider=xiaomi instead of ai-gateway.'''\n model, provider, base_url = _resolve_with_config(\n 'deepseek-v4-flash',\n provider='xiaomi',\n default='deepseek-v4-flash',\n custom_providers=[{\n 'name': 'LiteLLM Proxy',\n 'base_url': 'http://127.0.0.1:8080/v1',\n 'model': 'deepseek-v4-flash',\n }],\n )\n assert model == 'deepseek-v4-flash'\n assert provider == 'xiaomi'\n assert base_url is None # xiaomi has no config base_url in this test\n\n\n# ── get_available_models() @provider: hint behaviour ──────────────────────\n\n\n@pytest.fixture(autouse=True)\ndef _isolate_models_cache():\n \"\"\"Invalidate the models TTL cache before and after every test in this file.\n\n Several helpers here mutate ``config.cfg`` in-memory and call\n ``get_available_models()``. Without this guard, a prior test that called\n ``get_available_models()`` leaves a 60-second TTL cache entry; the next\n test that mutates cfg and calls the function gets a cache hit instead of\n running the function body, causing silently wrong results (e.g. the\n ``test_custom_endpoint_uses_model_config_api_key_for_model_discovery``\n ``KeyError: 'auth'`` on CI where ``urlopen`` is never reached).\n \"\"\"\n try:\n config.invalidate_models_cache()\n except Exception:\n pass\n yield\n try:\n config.invalidate_models_cache()\n except Exception:\n pass\n\n\ndef _available_models_with_provider(provider):\n \"\"\"Helper: temporarily set active_provider in config.\"\"\"\n old_cfg = dict(config.cfg)\n config.cfg['model'] = {'provider': provider}\n try:\n return config.get_available_models()\n finally:\n config.cfg.clear()\n config.cfg.update(old_cfg)\n\n\ndef test_non_default_provider_models_use_hint_prefix():\n \"\"\"With anthropic as default, minimax model IDs should use @minimax: prefix.\"\"\"\n result = _available_models_with_provider('anthropic')\n groups = {g['provider']: g['models'] for g in result['groups']}\n if 'MiniMax' in groups:\n for m in groups['MiniMax']:\n assert m['id'].startswith('@minimax:'), (\n f\"Expected @minimax: prefix, got: {m['id']!r}\"\n )\n\n\ndef test_no_duplicate_when_default_model_is_prefixed():\n \"\"\"Issue #147 Bug 2: 'anthropic/claude-opus-4.6' as default_model must not\n inject a duplicate alongside the existing bare 'claude-opus-4.6' entry in\n the same provider group.\"\"\"\n import api.config as _cfg\n old_cfg = dict(_cfg.cfg)\n _cfg.cfg['model'] = {\n 'provider': 'anthropic',\n 'default': 'anthropic/claude-opus-4.6',\n }\n try:\n result = _cfg.get_available_models()\n norm = lambda mid: mid.split('/', 1)[-1] if '/' in mid else mid\n # Check each group individually: no group should have two entries that\n # normalize to the same bare model name\n for g in result['groups']:\n bare_ids = [norm(m['id']) for m in g['models']]\n duplicates = [mid for mid in set(bare_ids) if bare_ids.count(mid) > 1]\n assert not duplicates, (\n f\"Provider group '{g['provider']}' has duplicate models after normalization: \"\n f\"{duplicates}\\nFull group: {[m['id'] for m in g['models']]}\"\n )\n finally:\n _cfg.cfg.clear()\n _cfg.cfg.update(old_cfg)\n\n\ndef test_default_provider_models_not_prefixed(monkeypatch):\n \"\"\"The active provider's models remain bare (no @prefix added).\"\"\"\n import api.config as _cfg\n monkeypatch.setattr(_cfg, \"_read_live_provider_model_ids\", lambda pid: [\"claude-sonnet-5.0\"] if pid == \"anthropic\" else [])\n result = _available_models_with_provider('anthropic')\n groups = {g['provider']: g['models'] for g in result['groups']}\n if 'Anthropic' in groups:\n returned_ids = {m['id'] for m in groups['Anthropic']}\n assert \"claude-sonnet-5.0\" in returned_ids\n assert not any(mid.startswith('@anthropic:') for mid in returned_ids), returned_ids\n\n\n# ── get_available_models(): phantom \"Custom\" group regression ─────────────\n#\n# When the user has model.provider set to a real provider (e.g. openai-codex)\n# AND a model.base_url set, hermes_cli reports the 'custom' pseudo-provider as\n# authenticated. The WebUI picker must NOT build a separate \"Custom\" group in\n# that case — the base_url belongs to the active provider.\n\ndef _available_models_with_full_cfg(provider, default, base_url):\n \"\"\"Helper: set model.provider, model.default, model.base_url at once.\n\n Clears model-override env vars (HERMES_MODEL, OPENAI_MODEL, LLM_MODEL)\n during the call so the real hermes profile environment doesn't leak into\n the test and override the fixture's default model.\n \"\"\"\n import os\n import api.config as _cfg\n old_cfg = dict(_cfg.cfg)\n _cfg.cfg['model'] = {\n 'provider': provider,\n 'default': default,\n 'base_url': base_url,\n }\n try:\n _cfg._cfg_mtime = _cfg.Path(_cfg._get_config_path()).stat().st_mtime\n except Exception:\n # No config.yaml on this machine (e.g. CI); pin to 0.0 so the mtime check\n # inside get_available_models() sees 0.0 == 0.0 and doesn't call reload_config(),\n # which would overwrite the in-memory cfg we just set up.\n _cfg._cfg_mtime = 0.0\n # Clear model-override env vars to prevent the real profile from leaking in\n _model_env_keys = ('HERMES_MODEL', 'OPENAI_MODEL', 'LLM_MODEL')\n _saved_env = {k: os.environ.pop(k, None) for k in _model_env_keys}\n try:\n return _cfg.get_available_models()\n finally:\n _cfg.cfg.clear()\n _cfg.cfg.update(old_cfg)\n for k, v in _saved_env.items():\n if v is not None:\n os.environ[k] = v\n\n\ndef test_no_phantom_custom_group_when_active_provider_is_set(monkeypatch):\n \"\"\"Issue: with provider=openai-codex + base_url set, gpt-5.4 was landing\n under a phantom \"Custom\" group instead of the \"OpenAI Codex\" group.\"\"\"\n import sys, types\n\n # Force hermes_cli to report both the real provider and the phantom\n # 'custom' as authenticated, simulating what list_available_providers()\n # returns when base_url is configured.\n fake_mod = types.ModuleType('hermes_cli.models')\n fake_mod.list_available_providers = lambda: [\n {'id': 'openai-codex', 'authenticated': True},\n {'id': 'custom', 'authenticated': True},\n ]\n fake_auth = types.ModuleType('hermes_cli.auth')\n fake_auth.get_auth_status = lambda pid: {'key_source': 'env'}\n monkeypatch.setitem(sys.modules, 'hermes_cli.models', fake_mod)\n monkeypatch.setitem(sys.modules, 'hermes_cli.auth', fake_auth)\n\n result = _available_models_with_full_cfg(\n provider='openai-codex',\n default='gpt-5.4',\n base_url='https://chatgpt.com/backend-api/codex',\n )\n group_names = [g['provider'] for g in result['groups']]\n assert 'Custom' not in group_names, (\n f\"Phantom 'Custom' group present; full groups: {group_names}\"\n )\n\n\ndef test_default_model_lands_under_active_provider_group(monkeypatch):\n \"\"\"The configured default_model must appear under the active provider's\n display group, even when the model isn't in _PROVIDER_MODELS[provider]\n AND the active provider isn't the alphabetical first detected provider.\n\n Regression guard for a hyphen-vs-space bug in the \"ensure default_model\n appears\" post-pass: the substring check `active_provider.lower() in\n g.get('provider', '').lower()` was failing for 'openai-codex' vs\n display name 'OpenAI Codex' (hyphen vs. space), silently falling back\n to groups[0] — which, when another provider sorted earlier\n alphabetically (e.g. 'anthropic'), placed gpt-5.4 in the WRONG group.\n \"\"\"\n import sys, types\n fake_mod = types.ModuleType('hermes_cli.models')\n fake_mod.list_available_providers = lambda: [\n {'id': 'anthropic', 'authenticated': True}, # sorts before openai-codex\n {'id': 'openai-codex', 'authenticated': True},\n {'id': 'custom', 'authenticated': True},\n ]\n fake_auth = types.ModuleType('hermes_cli.auth')\n fake_auth.get_auth_status = lambda pid: {'key_source': 'env'}\n monkeypatch.setitem(sys.modules, 'hermes_cli.models', fake_mod)\n monkeypatch.setitem(sys.modules, 'hermes_cli.auth', fake_auth)\n\n result = _available_models_with_full_cfg(\n provider='openai-codex',\n default='gpt-5.4',\n base_url='https://chatgpt.com/backend-api/codex',\n )\n groups = {g['provider']: [m['id'] for m in g['models']] for g in result['groups']}\n assert 'OpenAI Codex' in groups, f\"OpenAI Codex group missing: {list(groups)}\"\n norm = lambda mid: mid.split('/', 1)[-1].split(':', 1)[-1]\n assert 'gpt-5.4' in {norm(mid) for mid in groups['OpenAI Codex']}, (\n f\"gpt-5.4 not in OpenAI Codex group; contents: {groups['OpenAI Codex']}\"\n )\n # And crucially, it must NOT have landed in the alphabetically-first\n # group (Anthropic) via the fallback path.\n assert 'gpt-5.4' not in {norm(mid) for mid in groups.get('Anthropic', [])}, (\n f\"gpt-5.4 leaked into Anthropic group via fallback: {groups.get('Anthropic')}\"\n )\n\n\ndef test_unknown_providers_do_not_inherit_default_model(monkeypatch):\n \"\"\"Detected providers without their own model catalog must not be filled\n with the global default_model placeholder.\n\n Regression guard for the bug where unknown providers ended up showing\n gpt-5.4-mini even though those providers do not serve it. Minimax-Cn is\n now known and should show its own catalog instead.\n \"\"\"\n import sys, types\n\n fake_mod = types.ModuleType('hermes_cli.models')\n fake_mod.list_available_providers = lambda: [\n {'id': 'openai-codex', 'authenticated': True},\n {'id': 'alibaba', 'authenticated': True},\n {'id': 'minimax-cn', 'authenticated': True},\n ]\n fake_auth = types.ModuleType('hermes_cli.auth')\n fake_auth.get_auth_status = lambda pid: {'key_source': 'env'}\n monkeypatch.setitem(sys.modules, 'hermes_cli.models', fake_mod)\n monkeypatch.setitem(sys.modules, 'hermes_cli.auth', fake_auth)\n\n result = _available_models_with_full_cfg(\n provider='openai-codex',\n default='gpt-5.4-mini',\n base_url='',\n )\n groups = {g['provider']: [m['id'] for m in g['models']] for g in result['groups']}\n norm = lambda mid: mid.split('/', 1)[-1].split(':', 1)[-1]\n\n assert 'Alibaba' not in groups, (\n f\"Alibaba should not inherit the default model placeholder: {groups}\"\n )\n assert 'MiniMax (China)' in groups, (\n f\"Minimax-Cn should render its own static catalog: {groups}\"\n )\n assert not any(\n norm(mid) == 'gpt-5.4-mini'\n for mid in groups.get('Alibaba', []) + groups.get('MiniMax (China)', [])\n ), (\n f\"Unknown provider groups still inherited the default model: {groups}\"\n )\n\n\ndef test_custom_endpoint_uses_model_config_api_key_for_model_discovery(monkeypatch):\n \"\"\"Custom endpoint model discovery must use model.api_key from config.yaml,\n not only environment variables, otherwise the dropdown collapses to the\n default model when /v1/models requires auth.\"\"\"\n import json as _json\n import api.config as _cfg\n\n old_cfg = dict(_cfg.cfg)\n _cfg.cfg['model'] = {\n 'provider': 'custom',\n 'default': 'gpt-5.4',\n 'base_url': 'https://example.test/v1',\n 'api_key': 'sk-test-model-key',\n }\n try:\n _cfg._cfg_mtime = _cfg.Path(_cfg._get_config_path()).stat().st_mtime\n except Exception:\n # No config.yaml on this machine (e.g. CI); pin to 0.0 so the mtime check\n # inside get_available_models() sees 0.0 == 0.0 and skips reload_config().\n _cfg._cfg_mtime = 0.0\n _cfg.cfg.pop('providers', None)\n\n captured = {}\n\n class _Resp:\n def read(self):\n return _json.dumps({'data': [{'id': 'gpt-5.2', 'name': 'GPT-5.2'}]}).encode('utf-8')\n def __enter__(self):\n return self\n def __exit__(self, exc_type, exc, tb):\n return False\n\n def _fake_urlopen(req, timeout=10):\n url = getattr(req, 'full_url', '')\n if 'example.test' in url:\n captured['auth'] = req.get_header('Authorization')\n captured['ua'] = req.get_header('User-agent')\n return _Resp()\n\n monkeypatch.setattr('urllib.request.urlopen', _fake_urlopen)\n monkeypatch.setattr('socket.getaddrinfo', lambda *a, **k: [])\n monkeypatch.delenv('OPENAI_API_KEY', raising=False)\n monkeypatch.delenv('HERMES_API_KEY', raising=False)\n monkeypatch.delenv('HERMES_OPENAI_API_KEY', raising=False)\n monkeypatch.delenv('LOCAL_API_KEY', raising=False)\n monkeypatch.delenv('OPENROUTER_API_KEY', raising=False)\n monkeypatch.delenv('API_KEY', raising=False)\n try:\n result = _cfg.get_available_models()\n finally:\n _cfg.cfg.clear()\n _cfg.cfg.update(old_cfg)\n\n assert captured['auth'] == 'Bearer sk-test-model-key'\n assert captured['ua'] == 'OpenAI/Python 1.0'\n groups = {g['provider']: [m['id'] for m in g['models']] for g in result['groups']}\n assert 'Custom' in groups\n # Model ID may be prefixed with @provider: due to cross-provider dedup (#1228)\n assert any('gpt-5.2' in m for m in groups['Custom']), f'gpt-5.2 not found in Custom: {groups}'\n\n\n# -- Issue #230: custom provider with slash model name -----------------------\n\ndef test_custom_endpoint_slash_model_routes_to_custom_not_openrouter():\n \"\"\"Regression test for #230, updated for #1625.\n\n When provider=custom (or any non-openrouter provider) and base_url is set,\n a model name containing a slash (e.g. google/gemma-4-26b-a4b) must NOT be\n rerouted to OpenRouter -- it should stay on the configured custom endpoint.\n\n #1625 layered an additional rule on top: a base_url pointing at a loopback\n or private-IP host is treated as a local model server (LM Studio, Ollama,\n llama.cpp, vLLM, TabbyAPI), which register models under their full\n HuggingFace path. On such hosts the prefix is now PRESERVED. The original\n #433 strip behaviour still applies on public hosts (real OpenAI-compatible\n proxies like LiteLLM at https://litellm.example.com/v1).\n \"\"\"\n # --- custom provider with slash model name should NOT go to openrouter ---\n model, provider, base_url = _resolve_with_config(\n 'google/gemma-4-26b-a4b',\n provider='custom',\n base_url='http://127.0.0.1:1234/v1',\n default='google/gemma-4-26b-a4b',\n )\n assert provider.startswith('custom'), (\n \"Expected provider starting with 'custom', got '{}'. \"\n \"Slash in model name should NOT trigger OpenRouter rerouting when base_url is set.\".format(provider)\n )\n assert base_url == 'http://127.0.0.1:1234/v1', (\n \"Expected base_url 'http://127.0.0.1:1234/v1', got '{}'.\".format(base_url)\n )\n # #1625 (supersedes the v0.50 #433 strip-on-custom rule for loopback hosts):\n # 127.0.0.1 base_url is almost certainly a local LM Studio / Ollama / etc.,\n # which keys models on the full HuggingFace path. Preserve the prefix.\n assert model == 'google/gemma-4-26b-a4b', (\n \"Model name prefix must be PRESERVED on loopback base_url (#1625), got '{}'.\".format(model)\n )\n\n # --- public-host openai-compatible proxy STILL strips per #433 ----------\n model2, provider2, base_url2 = _resolve_with_config(\n 'google/gemma-4-26b-a4b',\n provider='openai',\n base_url='https://litellm.example.com/v1',\n default='google/gemma-4-26b-a4b',\n )\n assert model2 == 'gemma-4-26b-a4b', (\n \"Public-host OpenAI-compat proxy must still strip prefix per #433, got '{}'.\".format(model2)\n )\n\n # --- openrouter with slash model name MUST still route to openrouter -----\n model_or, provider_or, _ = _resolve_with_config(\n 'google/gemma-4-26b-a4b',\n provider='openrouter',\n base_url='https://openrouter.ai/api/v1',\n default='google/gemma-4-26b-a4b',\n )\n assert provider_or == 'openrouter', (\n \"Expected provider 'openrouter', got '{}'. \"\n \"Slash model via openrouter provider must still resolve to openrouter.\".format(provider_or)\n )\n assert model_or == 'google/gemma-4-26b-a4b', (\n \"Model name should be preserved for openrouter, got '{}'.\".format(model_or)\n )\n"}}}}