Skip to content

Commit b6e28f5

Browse files
committed
fix: lawful ragweld model catalog @codex
1 parent c5ba7a3 commit b6e28f5

2 files changed

Lines changed: 105 additions & 26 deletions

File tree

server/api/models.py

Lines changed: 93 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,17 +4,96 @@
44
in the UI. Every dropdown (embedding, generation, reranker) MUST use this endpoint.
55
66
NO HARDCODED MODEL LISTS ANYWHERE ELSE.
7+
8+
Note:
9+
- The catalog is primarily static (data/models.json), but we may *augment* it at
10+
request time with config-derived, runtime-only models (e.g. ragweld) so the UI
11+
stays "lawful" without special-casing.
712
"""
13+
814
import json
15+
import logging
916
from pathlib import Path
1017
from typing import Any
1118

12-
from fastapi import APIRouter, HTTPException
19+
from fastapi import APIRouter, Depends, HTTPException
20+
21+
from server.models.tribrid_config_model import CorpusScope
22+
from server.services.config_store import CorpusNotFoundError
23+
from server.services.config_store import get_config as load_scoped_config
1324

1425
router = APIRouter(prefix="/api/models", tags=["models"])
1526

1627
MODELS_PATH = Path(__file__).parent.parent.parent / "data" / "models.json"
1728

29+
logger = logging.getLogger(__name__)
30+
31+
# Ruff B008: avoid function calls in argument defaults (FastAPI Depends()).
32+
_CORPUS_SCOPE_DEP = Depends()
33+
34+
35+
async def _resolve_ragweld_base_model(scope: CorpusScope | None) -> str | None:
36+
repo_id: str | None = None
37+
try:
38+
repo_id = scope.resolved_repo_id if scope is not None else None
39+
except Exception:
40+
repo_id = None
41+
42+
cfg = None
43+
if repo_id:
44+
try:
45+
cfg = await load_scoped_config(repo_id=repo_id)
46+
except CorpusNotFoundError as e:
47+
# Don't break clients if a UI passes a stale corpus id.
48+
logger.warning("models catalog: corpus not found for scope repo_id=%s (%s)", repo_id, e)
49+
cfg = None
50+
except Exception as e:
51+
logger.warning("models catalog: failed to load scoped config for repo_id=%s (%s)", repo_id, e)
52+
cfg = None
53+
54+
if cfg is None:
55+
try:
56+
cfg = await load_scoped_config(repo_id=None)
57+
except Exception as e:
58+
logger.warning("models catalog: failed to load global config (%s)", e)
59+
return None
60+
61+
base = str(getattr(getattr(cfg, "training", None), "ragweld_agent_base_model", "") or "").strip()
62+
if base.startswith("ragweld:"):
63+
base = base.split(":", 1)[1].strip()
64+
return base or None
65+
66+
67+
def _augment_catalog_with_ragweld(catalog: dict[str, Any], ragweld_base_model: str | None) -> dict[str, Any]:
68+
if not ragweld_base_model:
69+
return catalog
70+
71+
model_id = f"ragweld:{ragweld_base_model}"
72+
models = catalog.get("models")
73+
if not isinstance(models, list):
74+
models = []
75+
catalog["models"] = models
76+
77+
for m in models:
78+
if isinstance(m, dict) and str(m.get("model") or "") == model_id:
79+
return catalog
80+
81+
models.append(
82+
{
83+
"provider": "ragweld",
84+
"family": ragweld_base_model,
85+
"model": model_id,
86+
"components": ["GEN"],
87+
# Required by /api/models contract tests; 0 means "unknown" (UI treats it as falsy).
88+
"context": 0,
89+
"unit": "1k_tokens",
90+
"input_per_1k": 0.0,
91+
"output_per_1k": 0.0,
92+
"notes": "Ragweld in-process MLX model (Qwen3 base + hot-swappable LoRA adapter; context unknown)",
93+
}
94+
)
95+
return catalog
96+
1897

1998
def _load_catalog() -> dict[str, Any]:
2099
"""Load the full models.json catalog.
@@ -41,18 +120,20 @@ def _catalog_models(catalog: dict[str, Any]) -> list[dict[str, Any]]:
41120

42121

43122
@router.get("")
44-
async def get_all_models() -> dict[str, Any]:
123+
async def get_all_models(scope: CorpusScope = _CORPUS_SCOPE_DEP) -> dict[str, Any]:
45124
"""
46125
Return the full models.json catalog (metadata + models list).
47126
48127
This is THE source of truth for all model selection in the UI.
49128
Every dropdown (embedding, generation, reranker) MUST use this endpoint.
50129
"""
51-
return _load_catalog()
130+
catalog = _load_catalog()
131+
base = await _resolve_ragweld_base_model(scope)
132+
return _augment_catalog_with_ragweld(catalog, base)
52133

53134

54135
@router.get("/by-type/{component_type}")
55-
async def get_models_by_type(component_type: str) -> list[dict[str, Any]]:
136+
async def get_models_by_type(component_type: str, scope: CorpusScope = _CORPUS_SCOPE_DEP) -> list[dict[str, Any]]:
56137
"""
57138
Return models filtered by component type.
58139
@@ -63,6 +144,8 @@ async def get_models_by_type(component_type: str) -> list[dict[str, Any]]:
63144
List of models that support the given component type
64145
"""
65146
catalog = _load_catalog()
147+
base = await _resolve_ragweld_base_model(scope)
148+
_augment_catalog_with_ragweld(catalog, base)
66149
models = _catalog_models(catalog)
67150
comp = component_type.upper()
68151
if comp not in ("EMB", "GEN", "RERANK"):
@@ -71,17 +154,21 @@ async def get_models_by_type(component_type: str) -> list[dict[str, Any]]:
71154

72155

73156
@router.get("/providers")
74-
async def get_providers() -> list[str]:
157+
async def get_providers(scope: CorpusScope = _CORPUS_SCOPE_DEP) -> list[str]:
75158
"""Return unique list of providers, sorted alphabetically."""
76159
catalog = _load_catalog()
160+
base = await _resolve_ragweld_base_model(scope)
161+
_augment_catalog_with_ragweld(catalog, base)
77162
models = _catalog_models(catalog)
78163
providers = sorted(set(str(m.get("provider", "unknown")) for m in models))
79164
return providers
80165

81166

82167
@router.get("/providers/{provider}")
83-
async def get_models_for_provider(provider: str) -> list[dict[str, Any]]:
168+
async def get_models_for_provider(provider: str, scope: CorpusScope = _CORPUS_SCOPE_DEP) -> list[dict[str, Any]]:
84169
"""Return all models for a specific provider."""
85170
catalog = _load_catalog()
171+
base = await _resolve_ragweld_base_model(scope)
172+
_augment_catalog_with_ragweld(catalog, base)
86173
models = _catalog_models(catalog)
87174
return [m for m in models if m.get("provider", "").lower() == provider.lower()]

web/src/components/RAG/RetrievalSubtab.tsx

Lines changed: 12 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { useState, useEffect, useCallback, useMemo, useRef } from 'react';
1+
import { useState, useEffect, useCallback, useRef } from 'react';
22
import { EmbeddingMismatchWarning } from '@/components/ui/EmbeddingMismatchWarning';
33
import { LiveTerminal, LiveTerminalHandle } from '@/components/LiveTerminal/LiveTerminal';
44
import { CollapsibleSection } from '@/components/ui/CollapsibleSection';
@@ -149,31 +149,23 @@ export function RetrievalSubtab() {
149149
clearError,
150150
} = useConfig();
151151

152-
// --- Derived helpers -----------------------------------------------------
153-
const ragweldGenModelOption = useMemo(() => {
154-
const base = String(config?.training?.ragweld_agent_base_model || 'mlx-community/Qwen3-1.7B-4bit').trim();
155-
if (!base) return '';
156-
return `ragweld:${base}`;
157-
}, [config?.training?.ragweld_agent_base_model]);
158-
159152
const loadModels = useCallback(async () => {
160-
let models: string[] = [];
161153
try {
162154
const data = await modelsApi.listByType('GEN');
163-
models = Array.isArray(data) ? data.map((m: any) => m.model).filter(Boolean) : [];
155+
const models = Array.isArray(data) ? data.map((m: any) => m.model).filter(Boolean) : [];
156+
157+
const unique: string[] = [];
158+
for (const m of models) {
159+
if (!m) continue;
160+
if (unique.includes(m)) continue;
161+
unique.push(m);
162+
}
163+
setAvailableModels(unique);
164164
} catch (error) {
165165
console.error('Failed to load models from /api/models/by-type/GEN:', error);
166+
setAvailableModels([]);
166167
}
167-
168-
const merged: string[] = [];
169-
if (ragweldGenModelOption) merged.push(ragweldGenModelOption);
170-
for (const m of models) {
171-
if (!m) continue;
172-
if (merged.includes(m)) continue;
173-
merged.push(m);
174-
}
175-
setAvailableModels(merged);
176-
}, [ragweldGenModelOption]);
168+
}, []);
177169

178170
useEffect(() => {
179171
loadModels();

0 commit comments

Comments
 (0)