Skip to content

Commit 0b43820

Browse files
committed
fix: support non-catalog model id fallback
Cherry-pick PR #25's non-catalog model ID mapping fix and update the registry docs to match the new api_model_id behavior.
1 parent ec66bbb commit 0b43820

3 files changed

Lines changed: 66 additions & 6 deletions

File tree

docs/model_catalog_and_routing_intelligence.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -144,7 +144,7 @@ See: [OpenRouter Integrations & Telemetry](openrouter_integrations_and_telemetry
144144
| `ModelFamily.capabilities(model_id)` | `dict[str,bool]` | Open WebUI capability checkboxes for UI affordances. |
145145
| `ModelFamily.supported_parameters(model_id)` | `frozenset[str]` | Provider-supported request parameter set (used for reasoning compatibility decisions). |
146146
| `ModelFamily.max_completion_tokens(model_id)` | `int \| None` | Provider-advertised max completion tokens, used when `USE_MODEL_MAX_OUTPUT_TOKENS=True`. |
147-
| `OpenRouterModelRegistry.api_model_id(model_id)` | provider slug or `None` | Maps the normalized/sanitized model ID back to the provider’s original ID for outbound API calls. |
147+
| `OpenRouterModelRegistry.api_model_id(model_id)` | provider slug, reconstructed non-catalog slug, or `None` | Maps the normalized/sanitized model ID back to the provider’s original ID for outbound API calls. |
148148

149149
---
150150

open_webui_openrouter_pipe/models/registry.py

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -558,7 +558,21 @@ def api_model_id(cls, model_id: str) -> Optional[str]:
558558
provider_id = cls._id_map.get(norm)
559559

560560
if not provider_id:
561-
return None
561+
# Non-catalog model: convert dotted format back to slash format
562+
if "/" in model_id_base:
563+
api_base = model_id_base
564+
elif "." in model_id_base:
565+
api_base = model_id_base.replace(".", "/", 1)
566+
else:
567+
api_base = model_id_base
568+
569+
# Re-attach suffix with appropriate separator
570+
if suffix_tag:
571+
if suffix_tag.startswith("preset/"):
572+
return f"{api_base}@{suffix_tag}"
573+
else:
574+
return f"{api_base}:{suffix_tag}"
575+
return api_base
562576

563577
# Re-attach suffix with appropriate separator
564578
# Presets use @ separator, variants use : separator

tests/test_variant_models.py

Lines changed: 50 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -193,11 +193,11 @@ def test_api_model_id_multiple_variants(self):
193193
assert result == expected_output, f"Failed for {input_id}"
194194

195195
def test_api_model_id_nonexistent_base(self):
196-
"""Test that non-existent base model returns None even with variant."""
196+
"""Test that non-catalog model with variant converts dotted to slash format."""
197197
OpenRouterModelRegistry._id_map = {}
198198

199199
result = OpenRouterModelRegistry.api_model_id("nonexistent.model:exacto")
200-
assert result is None
200+
assert result == "nonexistent/model:exacto"
201201

202202

203203
class TestPhaseModelSupport:
@@ -500,11 +500,57 @@ def test_api_model_id_multiple_presets(self):
500500
assert result == expected_output, f"Failed for {input_id}"
501501

502502
def test_api_model_id_nonexistent_preset_base(self):
503-
"""Test that non-existent base model returns None even with preset."""
503+
"""Test that non-catalog model with preset converts dotted to slash format."""
504504
OpenRouterModelRegistry._id_map = {}
505505

506506
result = OpenRouterModelRegistry.api_model_id("nonexistent.model:preset/my-preset")
507-
assert result is None
507+
assert result == "nonexistent/model@preset/my-preset"
508+
509+
510+
class TestAPIModelIDNonCatalogConversion:
511+
"""Test that api_model_id() converts dotted format to slash format for non-catalog models."""
512+
513+
def test_non_catalog_model_with_free_suffix(self):
514+
"""Test non-catalog model with :free suffix converts correctly."""
515+
OpenRouterModelRegistry._id_map = {}
516+
result = OpenRouterModelRegistry.api_model_id("qwen.qwen3.6-plus-preview:free")
517+
assert result == "qwen/qwen3.6-plus-preview:free"
518+
519+
def test_non_catalog_model_with_variant_suffix(self):
520+
"""Test non-catalog model with other variant suffix."""
521+
OpenRouterModelRegistry._id_map = {}
522+
result = OpenRouterModelRegistry.api_model_id("cognitivecomputations.dolphin-mistral-24b-venice-edition:free")
523+
assert result == "cognitivecomputations/dolphin-mistral-24b-venice-edition:free"
524+
525+
def test_non_catalog_model_without_suffix(self):
526+
"""Test non-catalog model without suffix converts correctly."""
527+
OpenRouterModelRegistry._id_map = {}
528+
result = OpenRouterModelRegistry.api_model_id("qwen.qwen3.6-plus-preview")
529+
assert result == "qwen/qwen3.6-plus-preview"
530+
531+
def test_non_catalog_model_already_has_slash(self):
532+
"""Test model that already has slash format is preserved."""
533+
OpenRouterModelRegistry._id_map = {}
534+
result = OpenRouterModelRegistry.api_model_id("qwen/qwen3.6-plus-preview:free")
535+
assert result == "qwen/qwen3.6-plus-preview:free"
536+
537+
def test_non_catalog_model_with_preset_suffix(self):
538+
"""Test non-catalog model with preset suffix uses @ separator."""
539+
OpenRouterModelRegistry._id_map = {}
540+
result = OpenRouterModelRegistry.api_model_id("qwen.qwen3.6-plus-preview:preset/my-preset")
541+
assert result == "qwen/qwen3.6-plus-preview@preset/my-preset"
542+
543+
def test_non_catalog_model_no_provider_prefix(self):
544+
"""Test model without provider prefix returns as-is."""
545+
OpenRouterModelRegistry._id_map = {}
546+
result = OpenRouterModelRegistry.api_model_id("some-model:free")
547+
assert result == "some-model:free"
548+
549+
def test_non_catalog_model_multiple_dots(self):
550+
"""Test model with multiple dots only converts first."""
551+
OpenRouterModelRegistry._id_map = {}
552+
result = OpenRouterModelRegistry.api_model_id("a.b.c:free")
553+
assert result == "a/b.c:free"
508554

509555

510556
class TestVariantEnforcementExpansion:

0 commit comments

Comments
 (0)