@@ -149,7 +149,11 @@ NO_TOOL_FAMILIES = { |
| 149 | 149 | def _family_tokens(model_name: str, model_details: dict[str, Any] | None) -> set[str]: |
| 150 | 150 | """Collect lowercase family tokens from model name and model details.""" |
| 151 | 151 | |
| 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]) |
| 153 | 157 | if model_details: |
| 154 | 158 | details = model_details.get("details", model_details) |
| 155 | 159 | families = details.get("families", []) |
@@ -161,6 +165,15 @@ def _family_tokens(model_name: str, model_details: dict[str, Any] | None) -> set |
| 161 | 165 | return tokens |
| 162 | 166 | |
| 163 | 167 | |
| 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 | + |
| 164 | 177 | def resolve_capability_profile( |
| 165 | 178 | model_name: str, |
| 166 | 179 | *, |
@@ -179,21 +192,23 @@ def resolve_capability_profile( |
| 179 | 192 | return override |
| 180 | 193 | |
| 181 | 194 | 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 | + ) |
| 193 | 208 | |
| 194 | 209 | tokens = _family_tokens(normalized, model_details) |
| 195 | 210 | |
| 196 | | - if any(token in NATIVE_TOOL_FAMILIES for token in tokens): |
| 211 | + if _any_prefix_match(tokens, NATIVE_TOOL_FAMILIES): |
| 197 | 212 | return _profile( |
| 198 | 213 | model_name, |
| 199 | 214 | supports_native_tools=True, |
@@ -202,7 +217,7 @@ def resolve_capability_profile( |
| 202 | 217 | notes=["Resolved from model family heuristic."], |
| 203 | 218 | ) |
| 204 | 219 | |
| 205 | | - if any(token in NO_TOOL_FAMILIES for token in tokens): |
| 220 | + if _any_prefix_match(tokens, NO_TOOL_FAMILIES): |
| 206 | 221 | return _profile( |
| 207 | 222 | model_name, |
| 208 | 223 | supports_native_tools=False, |