File size: 3,109 Bytes
a5784e9
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
"""
Model Capabilities API Endpoint

SINGLE SOURCE OF TRUTH for model thinking capabilities.
Frontend fetches this to determine UI controls dynamically.

Configuration is loaded from config/model_capabilities.json.
When new models are released, update the JSON file - no code changes needed.
"""

import json
import re
from functools import lru_cache
from pathlib import Path
from typing import Any

from fastapi import APIRouter
from fastapi.responses import JSONResponse

router = APIRouter()

# Config file path
_CONFIG_PATH = (
    Path(__file__).parent.parent.parent / "config" / "model_capabilities.json"
)


@lru_cache(maxsize=1)
def _load_config() -> dict[str, Any]:
    """
    Load model capabilities configuration from JSON file.

    Uses LRU cache to avoid repeated file reads.
    Raises FileNotFoundError if config is missing.
    """
    if not _CONFIG_PATH.exists():
        raise FileNotFoundError(f"Model capabilities config not found: {_CONFIG_PATH}")

    with open(_CONFIG_PATH, encoding="utf-8") as f:
        return json.load(f)


def reload_config() -> None:
    """Clear the config cache, forcing a reload on next access."""
    _load_config.cache_clear()


def _get_model_capabilities(model_id: str) -> dict[str, Any]:
    """
    Determine thinking capabilities for a model.

    Returns dict with:
    - thinkingType: "level" | "budget" | "none"
    - levels: List of thinking levels (for type="level")
    - alwaysOn: Whether thinking is always on (for Gemini 2.5 Pro)
    - budgetRange: [min, max] for budget slider
    - supportsGoogleSearch: Whether the model supports Google Search
    """
    config = _load_config()
    categories = config.get("categories", {})
    matchers = config.get("matchers", [])

    model_lower = model_id.lower()

    # Try each matcher in order (order matters: more specific first)
    for matcher in matchers:
        pattern = matcher.get("pattern", "")
        category_name = matcher.get("category", "")

        if pattern and category_name:
            try:
                if re.search(pattern, model_lower, re.IGNORECASE):
                    if category_name in categories:
                        return categories[category_name].copy()
            except re.error:
                # Invalid regex pattern, skip
                continue

    # Default to "other" category
    return categories.get(
        "other", {"thinkingType": "none", "supportsGoogleSearch": True}
    )


@router.get("/api/model-capabilities")
async def get_model_capabilities() -> JSONResponse:
    """
    Return thinking capabilities for all known model categories.

    Frontend uses this to dynamically configure thinking controls.
    """
    config = _load_config()
    return JSONResponse(content=config)


@router.get("/api/model-capabilities/{model_id:path}")
async def get_single_model_capabilities(model_id: str) -> JSONResponse:
    """
    Return thinking capabilities for a specific model.

    Args:
        model_id: Model identifier (e.g., "gemini-2.5-flash-preview")
    """
    return JSONResponse(content=_get_model_capabilities(model_id))