tenseleyflow/loader / 46cff53

Browse files

Fix capability resolution: strip :tag from model names, use prefix matching for family heuristic

Authored by espadonne
SHA
46cff53f7227dc3e2e2f55f5065d76dfc3a41030
Parents
0e59810
Tree
0b5cdc7

1 changed file

StatusFile+-
M src/loader/runtime/capabilities.py 29 14
src/loader/runtime/capabilities.pymodified
@@ -149,7 +149,11 @@ NO_TOOL_FAMILIES = {
149149
 def _family_tokens(model_name: str, model_details: dict[str, Any] | None) -> set[str]:
150150
     """Collect lowercase family tokens from model name and model details."""
151151
 
152
-    tokens = {model_name.lower()}
152
+    name = model_name.lower()
153
+    tokens = {name}
154
+    # Strip :tag so "devstral:24b" also produces "devstral"
155
+    if ":" in name:
156
+        tokens.add(name.split(":")[0])
153157
     if model_details:
154158
         details = model_details.get("details", model_details)
155159
         families = details.get("families", [])
@@ -161,6 +165,15 @@ def _family_tokens(model_name: str, model_details: dict[str, Any] | None) -> set
161165
     return tokens
162166
 
163167
 
168
+def _any_prefix_match(tokens: set[str], family_set: set[str]) -> bool:
169
+    """Check if any family entry is a prefix of any token."""
170
+    for token in tokens:
171
+        for family in family_set:
172
+            if token.startswith(family):
173
+                return True
174
+    return False
175
+
176
+
164177
 def resolve_capability_profile(
165178
     model_name: str,
166179
     *,
@@ -179,21 +192,23 @@ def resolve_capability_profile(
179192
         return override
180193
 
181194
     normalized = model_name.lower().strip()
182
-    if normalized in KNOWN_CAPABILITY_PROFILES:
183
-        known = KNOWN_CAPABILITY_PROFILES[normalized]
184
-        return CapabilityProfile(
185
-            model_name=model_name,
186
-            supports_native_tools=known.supports_native_tools,
187
-            supports_streaming=known.supports_streaming,
188
-            context_window=known.context_window,
189
-            preferred_tool_call_format=known.preferred_tool_call_format,
190
-            verification_strictness=known.verification_strictness,
191
-            notes=list(known.notes),
192
-        )
195
+    # Try full name first, then without :tag (e.g. "deepseek-r1:14b" -> "deepseek-r1")
196
+    for key in (normalized, normalized.split(":")[0]):
197
+        if key in KNOWN_CAPABILITY_PROFILES:
198
+            known = KNOWN_CAPABILITY_PROFILES[key]
199
+            return CapabilityProfile(
200
+                model_name=model_name,
201
+                supports_native_tools=known.supports_native_tools,
202
+                supports_streaming=known.supports_streaming,
203
+                context_window=known.context_window,
204
+                preferred_tool_call_format=known.preferred_tool_call_format,
205
+                verification_strictness=known.verification_strictness,
206
+                notes=list(known.notes),
207
+            )
193208
 
194209
     tokens = _family_tokens(normalized, model_details)
195210
 
196
-    if any(token in NATIVE_TOOL_FAMILIES for token in tokens):
211
+    if _any_prefix_match(tokens, NATIVE_TOOL_FAMILIES):
197212
         return _profile(
198213
             model_name,
199214
             supports_native_tools=True,
@@ -202,7 +217,7 @@ def resolve_capability_profile(
202217
             notes=["Resolved from model family heuristic."],
203218
         )
204219
 
205
-    if any(token in NO_TOOL_FAMILIES for token in tokens):
220
+    if _any_prefix_match(tokens, NO_TOOL_FAMILIES):
206221
         return _profile(
207222
             model_name,
208223
             supports_native_tools=False,