44SINGLE SOURCE OF TRUTH for model thinking capabilities.
55Frontend fetches this to determine UI controls dynamically.
66
7- When new models are released, update ONLY this file.
7+ Configuration is loaded from config/model_capabilities.json.
8+ When new models are released, update the JSON file - no code changes needed.
89"""
910
10- from typing import Literal
11+ import json
12+ import re
13+ from functools import lru_cache
14+ from pathlib import Path
15+ from typing import Any
1116
1217from fastapi import APIRouter
1318from fastapi .responses import JSONResponse
1419
1520router = APIRouter ()
1621
17- # Model category types
18- ThinkingType = Literal ["level" , "budget" , "none" ]
22+ # Config file path
23+ _CONFIG_PATH = (
24+ Path (__file__ ).parent .parent .parent / "config" / "model_capabilities.json"
25+ )
1926
2027
21- def _get_model_capabilities (model_id : str ) -> dict :
28+ @lru_cache (maxsize = 1 )
29+ def _load_config () -> dict [str , Any ]:
30+ """
31+ Load model capabilities configuration from JSON file.
32+
33+ Uses LRU cache to avoid repeated file reads.
34+ Raises FileNotFoundError if config is missing.
35+ """
36+ if not _CONFIG_PATH .exists ():
37+ raise FileNotFoundError (f"Model capabilities config not found: { _CONFIG_PATH } " )
38+
39+ with open (_CONFIG_PATH , encoding = "utf-8" ) as f :
40+ return json .load (f )
41+
42+
43+ def reload_config () -> None :
44+ """Clear the config cache, forcing a reload on next access."""
45+ _load_config .cache_clear ()
46+
47+
48+ def _get_model_capabilities (model_id : str ) -> dict [str , Any ]:
2249 """
2350 Determine thinking capabilities for a model.
2451
@@ -27,73 +54,32 @@ def _get_model_capabilities(model_id: str) -> dict:
2754 - levels: List of thinking levels (for type="level")
2855 - alwaysOn: Whether thinking is always on (for Gemini 2.5 Pro)
2956 - budgetRange: [min, max] for budget slider
57+ - supportsGoogleSearch: Whether the model supports Google Search
3058 """
59+ config = _load_config ()
60+ categories = config .get ("categories" , {})
61+ matchers = config .get ("matchers" , [])
62+
3163 model_lower = model_id .lower ()
3264
33- # Gemini 3 Flash: 4-level selector
34- if (
35- "gemini-3" in model_lower or "gemini3" in model_lower
36- ) and "flash" in model_lower :
37- return {
38- "thinkingType" : "level" ,
39- "levels" : ["minimal" , "low" , "medium" , "high" ],
40- "defaultLevel" : "high" ,
41- "supportsGoogleSearch" : True ,
42- }
43-
44- # Gemini 3 Pro: 2-level selector
45- if ("gemini-3" in model_lower or "gemini3" in model_lower ) and "pro" in model_lower :
46- return {
47- "thinkingType" : "level" ,
48- "levels" : ["low" , "high" ],
49- "defaultLevel" : "high" ,
50- "supportsGoogleSearch" : True ,
51- }
52-
53- # Gemini 2.5 Pro: Always-on thinking with budget
54- if "gemini-2.5-pro" in model_lower or "gemini-2.5pro" in model_lower :
55- return {
56- "thinkingType" : "budget" ,
57- "alwaysOn" : True ,
58- "budgetRange" : [1024 , 32768 ],
59- "defaultBudget" : 32768 ,
60- "supportsGoogleSearch" : True ,
61- }
62-
63- # Gemini 2.5 Flash and latest variants: Toggle + budget
64- if (
65- "gemini-2.5-flash" in model_lower
66- or "gemini-2.5flash" in model_lower
67- or model_lower == "gemini-flash-latest"
68- or model_lower == "gemini-flash-lite-latest"
69- ):
70- return {
71- "thinkingType" : "budget" ,
72- "alwaysOn" : False ,
73- "budgetRange" : [512 , 24576 ],
74- "defaultBudget" : 24576 ,
75- "supportsGoogleSearch" : True ,
76- }
77-
78- # Gemini 2.0 models: No thinking, no Google Search
79- if "gemini-2.0" in model_lower or "gemini2.0" in model_lower :
80- return {
81- "thinkingType" : "none" ,
82- "supportsGoogleSearch" : False ,
83- }
84-
85- # Gemini robotics models: special case - has Google Search
86- if "gemini-robotics" in model_lower :
87- return {
88- "thinkingType" : "none" ,
89- "supportsGoogleSearch" : True ,
90- }
91-
92- # Other models: No thinking controls, default to Google Search enabled
93- return {
94- "thinkingType" : "none" ,
95- "supportsGoogleSearch" : True ,
96- }
65+ # Try each matcher in order (order matters: more specific first)
66+ for matcher in matchers :
67+ pattern = matcher .get ("pattern" , "" )
68+ category_name = matcher .get ("category" , "" )
69+
70+ if pattern and category_name :
71+ try :
72+ if re .search (pattern , model_lower , re .IGNORECASE ):
73+ if category_name in categories :
74+ return categories [category_name ].copy ()
75+ except re .error :
76+ # Invalid regex pattern, skip
77+ continue
78+
79+ # Default to "other" category
80+ return categories .get (
81+ "other" , {"thinkingType" : "none" , "supportsGoogleSearch" : True }
82+ )
9783
9884
9985@router .get ("/api/model-capabilities" )
@@ -103,68 +89,16 @@ async def get_model_capabilities() -> JSONResponse:
10389
10490 Frontend uses this to dynamically configure thinking controls.
10591 """
106- return JSONResponse (
107- content = {
108- "categories" : {
109- "gemini3Flash" : {
110- "thinkingType" : "level" ,
111- "levels" : ["minimal" , "low" , "medium" , "high" ],
112- "defaultLevel" : "high" ,
113- "supportsGoogleSearch" : True ,
114- },
115- "gemini3Pro" : {
116- "thinkingType" : "level" ,
117- "levels" : ["low" , "high" ],
118- "defaultLevel" : "high" ,
119- "supportsGoogleSearch" : True ,
120- },
121- "gemini25Pro" : {
122- "thinkingType" : "budget" ,
123- "alwaysOn" : True ,
124- "budgetRange" : [1024 , 32768 ],
125- "defaultBudget" : 32768 ,
126- "supportsGoogleSearch" : True ,
127- },
128- "gemini25Flash" : {
129- "thinkingType" : "budget" ,
130- "alwaysOn" : False ,
131- "budgetRange" : [512 , 24576 ],
132- "defaultBudget" : 24576 ,
133- "supportsGoogleSearch" : True ,
134- },
135- "gemini2" : {
136- "thinkingType" : "none" ,
137- "supportsGoogleSearch" : False ,
138- },
139- "other" : {
140- "thinkingType" : "none" ,
141- "supportsGoogleSearch" : True ,
142- },
143- },
144- "matchers" : [
145- # Order matters: more specific patterns first
146- {
147- "pattern" : "gemini-3.*flash|gemini3.*flash" ,
148- "category" : "gemini3Flash" ,
149- },
150- {"pattern" : "gemini-3.*pro|gemini3.*pro" , "category" : "gemini3Pro" },
151- {
152- "pattern" : "gemini-2\\ .5-pro|gemini-2\\ .5pro" ,
153- "category" : "gemini25Pro" ,
154- },
155- {
156- "pattern" : "gemini-2\\ .5-flash|gemini-2\\ .5flash|gemini-flash-latest|gemini-flash-lite-latest" ,
157- "category" : "gemini25Flash" ,
158- },
159- {"pattern" : "gemini-2\\ .0|gemini2\\ .0" , "category" : "gemini2" },
160- ],
161- }
162- )
92+ config = _load_config ()
93+ return JSONResponse (content = config )
16394
16495
165- @router .get ("/api/model-capabilities/{model_id}" )
96+ @router .get ("/api/model-capabilities/{model_id:path }" )
16697async def get_single_model_capabilities (model_id : str ) -> JSONResponse :
16798 """
16899 Return thinking capabilities for a specific model.
100+
101+ Args:
102+ model_id: Model identifier (e.g., "gemini-2.5-flash-preview")
169103 """
170104 return JSONResponse (content = _get_model_capabilities (model_id ))
0 commit comments