Skip to content

Commit 7156780

Browse files
committed
refactor: enhance model catalog API and validation checks
- Updated model catalog documentation to clarify usage and API endpoints. - Implemented runtime checks to prevent frontend from fetching static models.json directly. - Added validation for model catalog synchronization between data/models.json and web/public/models.json. - Enhanced model catalog entry structure and validation in the API, including new request and response models. - Improved error handling for model capability validation in configuration updates.
1 parent 5fb9344 commit 7156780

33 files changed

Lines changed: 2003 additions & 750 deletions

mkdocs/docs/models.md

Lines changed: 47 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -1,81 +1,67 @@
1-
# Model Catalog (data/models.json)
1+
# Model Catalog (`data/models.json`)
22

3-
<div class="grid chunk_summaries" markdown>
3+
`data/models.json` is the canonical catalog for provider/model metadata, capabilities, and pricing.
4+
At runtime, clients must read catalog data from the API, not from static frontend files.
45

5-
- :material-currency-usd:{ .lg .middle } **Cost-Aware**
6+
## Runtime Contract
67

7-
---
8+
Use these routes:
89

9-
Pricing per 1k tokens with provider and family classifications.
10-
11-
- :material-brain:{ .lg .middle } **LLM/Embedding/Reranker**
12-
13-
---
14-
15-
Centralized catalog for generation, embeddings, and rerank models.
10+
| Route | Description |
11+
|---|---|
12+
| `GET /api/models` | Full typed catalog payload (`ModelCatalogResponse`) |
13+
| `GET /api/models/by-type/{component_type}` | Typed filtered rows (`GEN`, `EMB`, `RERANK`) |
14+
| `GET /api/models/providers` | Provider keys |
15+
| `GET /api/models/providers/{provider}` | Provider-scoped typed rows |
16+
| `POST /api/models/upsert` | Typed add/update flow (`ModelCatalogUpsertRequest`) |
1617

17-
- :material-api:{ .lg .middle } **API-Served**
18+
Notes:
1819

19-
---
20+
- Frontend runtime selectors must call `/api/models...`.
21+
- Do not fetch `web/public/models.json` in runtime UI code.
22+
- `web/public/models.json` remains a mirror for compatibility and is kept in sync on upsert.
2023

21-
UI and backend fetch from `/models/...`. No local lists.
24+
## Capability Semantics
2225

23-
</div>
26+
`components` is the capability contract:
2427

25-
[Get started](index.md){ .md-button .md-button--primary }
26-
[Configuration](configuration.md){ .md-button }
27-
[API](api.md){ .md-button }
28+
- `GEN`: generation/chat-capable
29+
- `EMB`: embedding-capable
30+
- `RERANK`: reranker-capable
2831

29-
!!! tip "Single Source"
30-
`data/models.json` is the authoritative source for model availability, pricing, and context sizes. Update it to change selectable models.
32+
Selectors and server config validation enforce capability compatibility. Known mismatches are rejected with `422`.
3133

32-
!!! note "Components"
33-
The `components` field indicates usage: `GEN` for generation, `EMB` for embeddings, `RERANK` for rerankers.
34+
## Upsert Flow
3435

35-
!!! warning "Pricing Staleness"
36-
Prices change over time. Keep `last_updated` current and reference sources in the file header.
36+
Use `POST /api/models/upsert` to add or update entries safely:
3737

38-
## API Endpoints
38+
- Request body is validated by Pydantic (`ModelCatalogUpsertRequest`).
39+
- Writes are atomic and update both `data/models.json` and `web/public/models.json`.
40+
- Provider `base_url` may be inferred from existing catalog entries/defaults if omitted, and remains editable in UI before submit.
3941

40-
| Route | Description |
41-
|-------|-------------|
42-
| `/models/by-type/{component_type}` | Filter by `GEN`, `EMB`, or `RERANK` |
43-
| `/models/providers` | List providers |
44-
| `/models/providers/{provider}` | Models for a specific provider |
42+
## Example
4543

46-
```mermaid
47-
flowchart LR
48-
Catalog["data/models.json"] --> API["/models"]
49-
API --> UI["Model Pickers"]
50-
API --> Server["Embedding/Reranker Selection"]
51-
```
52-
53-
=== "Python"
54-
```python
55-
import httpx
56-
base = "http://localhost:8000"
57-
gens = httpx.get(f"{base}/models/by-type/GEN").json() # (1)!
58-
providers = httpx.get(f"{base}/models/providers").json() # (2)!
59-
openai = httpx.get(f"{base}/models/providers/openai").json() # (3)!
60-
print(len(gens), providers, len(openai))
61-
```
62-
63-
=== "curl"
6444
```bash
6545
BASE=http://localhost:8000
66-
curl -sS "$BASE/models/by-type/GEN" | jq '.[0]'
67-
curl -sS "$BASE/models/providers" | jq .
68-
curl -sS "$BASE/models/providers/openai" | jq '.[].model'
46+
curl -sS "$BASE/api/models/by-type/GEN" | jq '.[0]'
47+
curl -sS "$BASE/api/models/providers" | jq .
48+
curl -sS -X POST "$BASE/api/models/upsert" \
49+
-H 'content-type: application/json' \
50+
-d '{
51+
"provider":"openai",
52+
"family":"gen",
53+
"model":"gpt-4.1-mini",
54+
"unit":"1k_tokens",
55+
"input_per_1k":0.0003,
56+
"output_per_1k":0.0012
57+
}' | jq .
6958
```
7059

71-
=== "TypeScript"
72-
```typescript
73-
type ModelItem = { provider: string; family: string; model: string; components: string[] };
74-
75-
async function listGen(): Promise<ModelItem[]> {
76-
return await (await fetch("/models/by-type/GEN")).json();
77-
}
60+
```mermaid
61+
flowchart LR
62+
Catalog["data/models.json"] --> API["/api/models"]
63+
API --> UI["All model selectors"]
64+
API --> Validate["Server capability validation"]
65+
Upsert["POST /api/models/upsert"] --> Catalog
66+
Upsert --> Mirror["web/public/models.json (mirror)"]
7867
```
79-
80-
??? info "UI Contract"
81-
All selectors in the UI must call these endpoints and use generated types for request/response where applicable.

scripts/check_banned.py

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
0 - No violations found
1313
1 - Violations found (see output for details)
1414
"""
15+
import json
1516
import re
1617
import sys
1718
from pathlib import Path
@@ -159,6 +160,7 @@
159160
'.venv',
160161
'venv',
161162
'.pytest_cache',
163+
'output/playwright',
162164
'dist',
163165
'build',
164166
'.mypy_cache',
@@ -519,6 +521,79 @@ def check_studio_no_inline_styles() -> List[str]:
519521
return errors
520522

521523

524+
def check_no_frontend_runtime_models_json_fetches() -> List[str]:
525+
"""Fail when frontend runtime code fetches static models.json directly."""
526+
errors: list[str] = []
527+
web_src = Path("web/src")
528+
if not web_src.exists():
529+
return errors
530+
531+
banned_patterns = [
532+
(
533+
re.compile(r"fetch\(\s*['\"][^'\"]*models\.json", re.IGNORECASE),
534+
"Runtime catalog fetch from models.json is banned. Use /api/models via web/src/api/models.ts.",
535+
),
536+
(
537+
re.compile(r"axios\.(get|post|request)\(\s*['\"][^'\"]*models\.json", re.IGNORECASE),
538+
"Runtime catalog fetch from models.json is banned. Use /api/models via web/src/api/models.ts.",
539+
),
540+
(
541+
re.compile(r"api\(\s*['\"][^'\"]*models\.json", re.IGNORECASE),
542+
"Runtime catalog fetch from models.json is banned. Use /api/models via web/src/api/models.ts.",
543+
),
544+
]
545+
546+
for f in web_src.rglob("*"):
547+
if should_skip(f) or not f.is_file():
548+
continue
549+
if f.suffix not in {".ts", ".tsx", ".js", ".jsx"}:
550+
continue
551+
# Generated file comments can mention models.json.
552+
if str(f).endswith("web/src/types/generated.ts"):
553+
continue
554+
try:
555+
content = f.read_text(errors="ignore")
556+
except Exception:
557+
continue
558+
for i, line in enumerate(content.split("\n"), 1):
559+
for pattern, message in banned_patterns:
560+
if pattern.search(line):
561+
errors.append(f"{_normalize_relpath(f)}:{i}: {message}")
562+
return errors
563+
564+
565+
def check_models_catalog_mirror_sync() -> List[str]:
566+
"""Fail when data/models.json and web/public/models.json diverge."""
567+
errors: list[str] = []
568+
data_path = Path("data/models.json")
569+
web_path = Path("web/public/models.json")
570+
571+
if not data_path.exists():
572+
errors.append("data/models.json missing (authoritative runtime catalog is required).")
573+
return errors
574+
if not web_path.exists():
575+
errors.append("web/public/models.json missing (legacy mirror is required for compatibility).")
576+
return errors
577+
578+
try:
579+
data_obj = json.loads(data_path.read_text(errors="ignore"))
580+
except Exception as e:
581+
errors.append(f"data/models.json: failed to parse JSON ({e})")
582+
return errors
583+
try:
584+
web_obj = json.loads(web_path.read_text(errors="ignore"))
585+
except Exception as e:
586+
errors.append(f"web/public/models.json: failed to parse JSON ({e})")
587+
return errors
588+
589+
if data_obj != web_obj:
590+
errors.append(
591+
"data/models.json and web/public/models.json are out of sync. "
592+
"Update both atomically (or use POST /api/models/upsert)."
593+
)
594+
return errors
595+
596+
522597
def main() -> int:
523598
print("Checking for banned patterns...")
524599
print("")
@@ -532,6 +607,8 @@ def main() -> int:
532607
errors.extend(check_env_example_legacy_keys())
533608
errors.extend(check_server_env_getenv_allowlist())
534609
errors.extend(check_studio_no_inline_styles())
610+
errors.extend(check_no_frontend_runtime_models_json_fetches())
611+
errors.extend(check_models_catalog_mirror_sync())
535612

536613
if errors:
537614
print("BANNED PATTERNS FOUND:")

scripts/generate_types.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -292,6 +292,10 @@ def main() -> None:
292292
ChatRequest,
293293
ChatResponse,
294294
ChatDebugInfo,
295+
ModelCatalogEntry,
296+
ModelCatalogResponse,
297+
ModelCatalogUpsertRequest,
298+
ModelCatalogUpsertResponse,
295299
ChatModelInfo,
296300
ChatModelsResponse,
297301
ProviderHealth,
@@ -419,6 +423,10 @@ def main() -> None:
419423
ChatRequest,
420424
ChatResponse,
421425
ChatDebugInfo,
426+
ModelCatalogEntry,
427+
ModelCatalogResponse,
428+
ModelCatalogUpsertRequest,
429+
ModelCatalogUpsertResponse,
422430
ChatModelInfo,
423431
ChatModelsResponse,
424432
ProviderHealth,

0 commit comments

Comments
 (0)