Skip to content

Commit ca14f7c

Browse files
Nueva vista models
1 parent 11263dc commit ca14f7c

File tree

6 files changed

+407
-1
lines changed

6 files changed

+407
-1
lines changed

frontend/api.py

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -102,12 +102,31 @@ async def index(request: Request, session = Depends(get_session)):
102102
orphaned_models = await session.scalar(select(func.count()).select_from(Model).where(Model.is_orphaned == True))
103103
modified_models = await session.scalar(select(func.count()).select_from(Model).where(Model.user_modified == True))
104104

105+
# Get category stats
106+
from shared.db_models import Provider as ProviderModel
107+
from shared.categorization import get_category_stats
108+
result = await session.execute(
109+
select(Model)
110+
.join(ProviderModel)
111+
.where(
112+
Model.is_orphaned == False,
113+
ProviderModel.type != "compat"
114+
)
115+
)
116+
all_models = result.scalars().all()
117+
models_data = [
118+
{"capabilities": m.capabilities, "system_tags": m.system_tags}
119+
for m in all_models
120+
]
121+
category_stats = get_category_stats(models_data)
122+
105123
stats = {
106124
"providers": len(providers_list),
107125
"models": total_models or 0,
108126
"orphaned": orphaned_models or 0,
109127
"modified": modified_models or 0,
110-
"litellm_models": litellm_models
128+
"litellm_models": litellm_models,
129+
"categories": category_stats
111130
}
112131

113132
# Convert config to template-compatible format
@@ -159,6 +178,25 @@ async def sources_page(request: Request, session = Depends(get_session)):
159178
"config": config_dict
160179
})
161180

181+
@app.get("/models", response_class=HTMLResponse)
182+
async def models_page(request: Request, session = Depends(get_session)):
183+
"""Models browser page."""
184+
config = await get_config(session)
185+
config_dict = {
186+
"litellm": {
187+
"configured": bool(config.litellm_base_url),
188+
"base_url": config.litellm_base_url or "",
189+
"api_key": config.litellm_api_key or ""
190+
},
191+
"sync_interval_seconds": config.sync_interval_seconds,
192+
"default_pricing_profile": config.default_pricing_profile,
193+
"default_pricing_override": config.default_pricing_override_dict,
194+
}
195+
return templates.TemplateResponse("models.html", {
196+
"request": request,
197+
"config": config_dict
198+
})
199+
162200
@app.get("/compat", response_class=HTMLResponse)
163201
async def compat_page(request: Request, session = Depends(get_session)):
164202
"""Compat models page."""

frontend/routes/providers.py

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
get_models_by_provider,
1313
get_config,
1414
)
15+
from shared.categorization import get_category_stats
1516
from backend.provider_sync import sync_provider
1617

1718
router = APIRouter()
@@ -84,6 +85,40 @@ async def list_providers(session: AsyncSession = Depends(get_session)):
8485
]
8586

8687

88+
@router.get("/stats/all")
89+
async def get_all_stats(session: AsyncSession = Depends(get_session)):
90+
"""Get category statistics for all providers combined."""
91+
from sqlalchemy import select
92+
from shared.db_models import Model, Provider
93+
94+
# Get all non-orphaned models from non-compat providers
95+
result = await session.execute(
96+
select(Model)
97+
.join(Provider)
98+
.where(
99+
Model.is_orphaned == False,
100+
Provider.type != "compat"
101+
)
102+
)
103+
models = result.scalars().all()
104+
105+
# Convert to dict format for categorization
106+
models_data = [
107+
{
108+
"capabilities": m.capabilities,
109+
"system_tags": m.system_tags
110+
}
111+
for m in models
112+
]
113+
114+
stats = get_category_stats(models_data)
115+
116+
return {
117+
"total_models": len(models),
118+
"categories": stats
119+
}
120+
121+
87122
@router.get("/{provider_id}/models")
88123
async def list_provider_models(
89124
provider_id: int,
@@ -128,6 +163,38 @@ async def list_provider_models(
128163
}, "models": result}
129164

130165

166+
@router.get("/{provider_id}/stats")
167+
async def get_provider_stats(
168+
provider_id: int,
169+
session: AsyncSession = Depends(get_session)
170+
):
171+
"""Get category statistics for a provider's models."""
172+
provider = await get_provider_by_id(session, provider_id)
173+
if not provider:
174+
raise HTTPException(404, "Provider not found")
175+
176+
# Get all non-orphaned models
177+
models = await get_models_by_provider(session, provider_id, include_orphaned=False)
178+
179+
# Convert to dict format for categorization
180+
models_data = [
181+
{
182+
"capabilities": m.capabilities,
183+
"system_tags": m.system_tags
184+
}
185+
for m in models
186+
]
187+
188+
stats = get_category_stats(models_data)
189+
190+
return {
191+
"provider_id": provider_id,
192+
"provider_name": provider.name,
193+
"total_models": len(models),
194+
"categories": stats
195+
}
196+
197+
131198
@router.get("/{provider_id}")
132199
async def get_provider(provider_id: int, session: AsyncSession = Depends(get_session)):
133200
"""Get provider by ID."""

frontend/templates/base.html

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ <h1>LiteLLM Updater <span class="version-badge">v{{ APP_VERSION }}</span></h1>
1212
<nav>
1313
<a href="/">Overview</a>
1414
<a href="/sources">Providers</a>
15+
<a href="/models">Models</a>
1516
<a href="/compat">Compat</a>
1617
<a href="/litellm">LiteLLM</a>
1718
<a href="/admin">Admin</a>

frontend/templates/compat.html

Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,14 @@ <h2>Compatibility Models</h2>
77
This allows external services to use familiar model names while routing to your custom models.
88
</p>
99

10+
<!-- OpenAI API Coverage Panel -->
11+
<div class="coverage-panel">
12+
<h3 style="margin: 0 0 1rem 0; font-size: 1.1rem;">OpenAI API Coverage</h3>
13+
<div id="coverage-badges" class="coverage-badges">
14+
Loading coverage...
15+
</div>
16+
</div>
17+
1018
<!-- Actions -->
1119
<div class="compat-actions">
1220
<button class="btn-primary" onclick="openAddCompatModal()">Add Compat Model</button>
@@ -77,6 +85,68 @@ <h3 id="compat-modal-title">Add Compat Model</h3>
7785
</div>
7886

7987
<style>
88+
/* Coverage Panel */
89+
.coverage-panel {
90+
background: #f9f9f9;
91+
border: 1px solid #e0e0e0;
92+
border-radius: 8px;
93+
padding: 1.5rem;
94+
margin-bottom: 1.5rem;
95+
}
96+
97+
.coverage-badges {
98+
display: flex;
99+
flex-wrap: wrap;
100+
gap: 0.75rem;
101+
}
102+
103+
.coverage-badge {
104+
display: flex;
105+
align-items: center;
106+
padding: 0.75rem 1rem;
107+
border-radius: 20px;
108+
font-size: 0.9rem;
109+
font-weight: 500;
110+
transition: all 0.2s;
111+
cursor: help;
112+
min-width: 140px;
113+
}
114+
115+
.coverage-badge:hover {
116+
transform: translateY(-2px);
117+
box-shadow: 0 4px 8px rgba(0,0,0,0.15);
118+
}
119+
120+
.coverage-badge.covered {
121+
background: #e8f5e9;
122+
border: 2px solid #4caf50;
123+
color: #2e7d32;
124+
}
125+
126+
.coverage-badge.partial {
127+
background: #fff3e0;
128+
border: 2px solid #ff9800;
129+
color: #e65100;
130+
}
131+
132+
.coverage-badge.missing {
133+
background: #ffebee;
134+
border: 2px solid #f44336;
135+
color: #c62828;
136+
}
137+
138+
.coverage-icon {
139+
font-size: 1.2rem;
140+
margin-right: 0.5rem;
141+
}
142+
143+
.coverage-count {
144+
margin-left: auto;
145+
padding-left: 0.75rem;
146+
font-weight: 600;
147+
font-size: 0.85rem;
148+
}
149+
80150
.compat-actions {
81151
margin-bottom: 1.5rem;
82152
}
@@ -250,15 +320,113 @@ <h3 id="compat-modal-title">Add Compat Model</h3>
250320
let allProviders = [];
251321
let providerModels = {};
252322

323+
// OpenAI API Categories with representative model names
324+
const OPENAI_CATEGORIES = {
325+
'Chat': {
326+
icon: '💬',
327+
models: ['gpt-4', 'gpt-4-turbo', 'gpt-4o', 'gpt-4o-mini', 'gpt-3.5-turbo', 'gpt-3.5-turbo-16k', 'gpt-4-32k'],
328+
description: 'Chat completion models'
329+
},
330+
'Vision': {
331+
icon: '👁️',
332+
models: ['gpt-4-vision-preview', 'gpt-4-turbo-vision', 'gpt-4o-vision'],
333+
description: 'Vision-enabled models'
334+
},
335+
'Code': {
336+
icon: '💻',
337+
models: ['code-davinci-002', 'gpt-4-code'],
338+
description: 'Code generation models'
339+
},
340+
'Reasoning': {
341+
icon: '🧠',
342+
models: ['o1', 'o1-mini', 'o1-preview'],
343+
description: 'Advanced reasoning models'
344+
},
345+
'Embeddings': {
346+
icon: '🔢',
347+
models: ['text-embedding-3-small', 'text-embedding-3-large', 'text-embedding-ada-002'],
348+
description: 'Text embedding models'
349+
},
350+
'Speech-to-Text': {
351+
icon: '🎤',
352+
models: ['whisper-1'],
353+
description: 'Audio transcription'
354+
},
355+
'Text-to-Speech': {
356+
icon: '🔊',
357+
models: ['tts-1', 'tts-1-hd'],
358+
description: 'Voice generation'
359+
},
360+
'Images': {
361+
icon: '🎨',
362+
models: ['dall-e-2', 'dall-e-3'],
363+
description: 'Image generation'
364+
}
365+
};
366+
253367
// Load compat models on page load
254368
async function loadCompatModels() {
255369
try {
256370
const response = await fetch('/api/compat/models');
257371
compatModels = await response.json();
258372
renderCompatModels();
373+
await renderCoverage();
259374
} catch (error) {
260375
document.getElementById('compat-list').innerHTML = `<p style="color: red;">Error loading compat models: ${error.message}</p>`;
376+
document.getElementById('coverage-badges').innerHTML = `<p style="color: red;">Error loading coverage</p>`;
377+
}
378+
}
379+
380+
// Render OpenAI API coverage badges
381+
async function renderCoverage() {
382+
// Get model names from both compat models AND database models
383+
const compatModelNames = new Set(compatModels.map(m => m.model_name.toLowerCase()));
384+
385+
// Also fetch database models to include in coverage
386+
let dbModelNames = new Set();
387+
try {
388+
const providers = await fetch('/api/providers').then(r => r.json());
389+
for (const provider of providers) {
390+
if (provider.type !== 'compat') {
391+
const data = await fetch(`/api/providers/${provider.id}/models?include_orphaned=false`).then(r => r.json());
392+
const models = Array.isArray(data) ? data : (data.models || []);
393+
models.forEach(m => dbModelNames.add(m.model_id.toLowerCase()));
394+
}
395+
}
396+
} catch (error) {
397+
console.error('Failed to load database models for coverage:', error);
261398
}
399+
400+
// Combine both sets
401+
const allModelNames = new Set([...compatModelNames, ...dbModelNames]);
402+
403+
const badges = Object.entries(OPENAI_CATEGORIES).map(([category, info]) => {
404+
const coveredModels = info.models.filter(m => allModelNames.has(m.toLowerCase()));
405+
const coverageCount = coveredModels.length;
406+
const totalCount = info.models.length;
407+
408+
let statusClass = 'missing';
409+
let statusIcon = '❌';
410+
if (coverageCount === totalCount) {
411+
statusClass = 'covered';
412+
statusIcon = '✓';
413+
} else if (coverageCount > 0) {
414+
statusClass = 'partial';
415+
statusIcon = '⚠️';
416+
}
417+
418+
const title = `${info.description}\nCovered: ${coveredModels.join(', ') || 'None'}\nMissing: ${info.models.filter(m => !allModelNames.has(m.toLowerCase())).join(', ') || 'None'}`;
419+
420+
return `
421+
<div class="coverage-badge ${statusClass}" title="${title}">
422+
<span class="coverage-icon">${info.icon}</span>
423+
<span>${category}</span>
424+
<span class="coverage-count">${statusIcon} ${coverageCount}/${totalCount}</span>
425+
</div>
426+
`;
427+
}).join('');
428+
429+
document.getElementById('coverage-badges').innerHTML = badges;
262430
}
263431

264432
// Load all providers for dropdown

0 commit comments

Comments
 (0)