Merge pull request #215 from Routstr/v0.2.0-final

* fix model filtering
* cleanup desing
This commit is contained in:
9qeklajc
2025-11-05 09:35:10 +01:00
committed by GitHub
14 changed files with 149 additions and 104 deletions
+3 -5
View File
@@ -577,7 +577,6 @@ async def _cleanup_enabled_models_once() -> None:
for db_model in db_models:
# Find corresponding upstream model
print(db_model.id)
upstream_model = None
for upstream in upstreams:
upstream_model = upstream.get_cached_model_by_id(db_model.id)
@@ -613,7 +612,7 @@ async def _cleanup_enabled_models_once() -> None:
def _pricing_matches(
db_pricing: dict, upstream_pricing: dict, tolerance: float = 0.1
db_pricing: dict, upstream_pricing: dict, tolerance: float = 0.0
) -> bool:
"""Check if pricing dictionaries match within tolerance."""
keys_to_compare = [
@@ -626,9 +625,8 @@ def _pricing_matches(
]
for key in keys_to_compare:
db_val = float(db_pricing.get(key, 0.0)) * 1000000
upstream_val = float(upstream_pricing.get(key, 0.0)) * 1000000
print(db_val - upstream_val)
db_val = int(float(db_pricing.get(key, 0.0)) * 1000000)
upstream_val = int(float(upstream_pricing.get(key, 0.0)) * 1000000)
if abs(db_val - upstream_val) > tolerance:
return False
+3 -5
View File
@@ -3,7 +3,7 @@ from typing import Any
from fastapi import APIRouter, Depends, HTTPException, Request
from fastapi.responses import Response, StreamingResponse
from sqlmodel import col, select
from sqlmodel import select
from .algorithm import create_model_mappings
from .auth import pay_for_request, revert_pay_for_request, validate_bearer_key
@@ -83,9 +83,7 @@ async def refresh_model_maps() -> None:
# Gather database overrides and disabled models
async with create_session() as session:
result = await session.exec(
select(ModelRow).where(col(ModelRow.enabled).is_(True))
)
result = await session.exec(select(ModelRow).where(ModelRow.enabled))
override_rows = result.all()
provider_result = await session.exec(select(UpstreamProviderRow))
@@ -103,7 +101,7 @@ async def refresh_model_maps() -> None:
}
disabled_result = await session.exec(
select(ModelRow.id).where(col(ModelRow.enabled).is_(False))
select(ModelRow.id).where(ModelRow.enabled == False) # noqa: E712
)
disabled_model_ids = {row for row in disabled_result.all()}
+1 -1
View File
@@ -121,7 +121,7 @@ class OllamaUpstreamProvider(UpstreamProvider):
models_list.append(
Model(
id=model_name,
name=model_name,
name=model_name.replace(":", " "),
created=0,
description=description,
context_length=context_length,
+1
View File
@@ -49,6 +49,7 @@ else
npm run build
fi
rm -rf ../ui_out
mkdir -p ../ui_out
mv out/* ../ui_out
+1 -1
View File
@@ -34,7 +34,7 @@ export default function RootLayout({
return (
<html lang='en' suppressHydrationWarning>
<body
className={`${geistSans.variable} ${geistMono.variable} antialiased font-sans`}
className={`${geistSans.variable} ${geistMono.variable} font-sans antialiased`}
>
<SuppressHydrationWarning>
<Providers>{children}</Providers>
+11 -12
View File
@@ -16,6 +16,7 @@ import { Alert, AlertDescription } from '@/components/ui/alert';
import { Badge } from '@/components/ui/badge';
import { useMemo, useState } from 'react';
import type { Model } from '@/lib/api/schemas/models';
import { groupAndSortModelsByProvider } from '@/lib/utils/modelSort';
export default function ModelsPage() {
const [filteredModels, setFilteredModels] = useState<Model[]>([]);
@@ -34,15 +35,7 @@ export default function ModelsPage() {
const groupedModels = useMemo(() => {
if (!models) return {};
return models.reduce<Record<string, typeof models>>((acc, model) => {
const provider = model.provider;
if (!acc[provider]) {
acc[provider] = [];
}
acc[provider].push(model);
return acc;
}, {});
return groupAndSortModelsByProvider(models);
}, [models]);
const groupDataMap = useMemo(() => {
@@ -52,7 +45,9 @@ export default function ModelsPage() {
const providerInfo = useMemo(() => {
return Object.entries(groupedModels).map(([provider, providerModels]) => {
const groupData = groupDataMap.get(provider);
const activeModels = providerModels.filter((m) => !m.soft_deleted).length;
const activeModels = providerModels.filter(
(m) => m.isEnabled && !m.soft_deleted
).length;
const totalModels = providerModels.length;
return {
@@ -156,7 +151,10 @@ export default function ModelsPage() {
models={models}
onFilteredModelsChange={setFilteredModels}
/>
<ModelSelector filteredModels={filteredModels} />
<ModelSelector
filteredModels={filteredModels}
showDeleteAllButton={true}
/>
</div>
</TabsContent>
@@ -183,7 +181,7 @@ export default function ModelsPage() {
(m) => m.soft_deleted
).length
}{' '}
soft deleted
disabled
</span>
)}
{groupData?.group_url && (
@@ -199,6 +197,7 @@ export default function ModelsPage() {
filterProvider={provider}
groupData={groupData}
showProviderActions={true}
showDeleteAllButton={false}
/>
</div>
</TabsContent>
+58 -38
View File
@@ -277,7 +277,11 @@ export default function ProvidersPage() {
}
placeholder='https://api.example.com/v1'
disabled={hasFixedBaseUrl(formData.provider_type)}
className={hasFixedBaseUrl(formData.provider_type) ? 'cursor-not-allowed opacity-60' : ''}
className={
hasFixedBaseUrl(formData.provider_type)
? 'cursor-not-allowed opacity-60'
: ''
}
/>
</div>
<div className='grid gap-2'>
@@ -453,7 +457,8 @@ export default function ProvidersPage() {
<div className='space-y-2'>
{providerModels.db_models.length === 0 ? (
<div className='text-muted-foreground py-4 text-center text-sm'>
No models configured. Add custom models to use this provider.
No models configured. Add custom models to
use this provider.
</div>
) : (
<div className='space-y-2'>
@@ -495,7 +500,10 @@ export default function ProvidersPage() {
</div>
) : (
// Has provided models - show tabs
<Tabs defaultValue='provided' className='w-full'>
<Tabs
defaultValue='provided'
className='w-full'
>
<TabsList className='grid w-full grid-cols-2'>
<TabsTrigger
value='provided'
@@ -504,7 +512,9 @@ export default function ProvidersPage() {
<span className='hidden sm:inline'>
Provided Models
</span>
<span className='sm:hidden'>Provided</span>
<span className='sm:hidden'>
Provided
</span>
<Badge
variant='secondary'
className='ml-1 text-xs sm:ml-2'
@@ -534,7 +544,8 @@ export default function ProvidersPage() {
>
{providerModels.db_models.length > 0 && (
<div className='text-muted-foreground mb-3 text-sm'>
Custom models override or extend the provider&apos;s catalog.
Custom models override or extend the
provider&apos;s catalog.
</div>
)}
{providerModels.db_models.length === 0 ? (
@@ -543,39 +554,42 @@ export default function ProvidersPage() {
</div>
) : (
<div className='space-y-2'>
{providerModels.db_models.map((model) => (
<div
key={model.id}
className='hover:bg-accent flex flex-col gap-2 rounded-lg border p-3 transition-colors sm:flex-row sm:items-center sm:justify-between'
>
<div className='min-w-0 flex-1'>
<div className='flex flex-col gap-1 sm:flex-row sm:items-center sm:gap-2'>
<span className='truncate font-mono text-sm font-medium'>
{model.id}
</span>
<Badge
variant={
model.enabled
? 'default'
: 'secondary'
}
className='w-fit text-xs'
>
{model.enabled
? 'Enabled'
: 'Disabled'}
</Badge>
{providerModels.db_models.map(
(model) => (
<div
key={model.id}
className='hover:bg-accent flex flex-col gap-2 rounded-lg border p-3 transition-colors sm:flex-row sm:items-center sm:justify-between'
>
<div className='min-w-0 flex-1'>
<div className='flex flex-col gap-1 sm:flex-row sm:items-center sm:gap-2'>
<span className='truncate font-mono text-sm font-medium'>
{model.id}
</span>
<Badge
variant={
model.enabled
? 'default'
: 'secondary'
}
className='w-fit text-xs'
>
{model.enabled
? 'Enabled'
: 'Disabled'}
</Badge>
</div>
<div className='text-muted-foreground mt-1 text-xs break-words'>
{model.description ||
model.name}
</div>
</div>
<div className='text-muted-foreground mt-1 text-xs break-words'>
{model.description || model.name}
<div className='text-muted-foreground text-xs whitespace-nowrap'>
{model.context_length?.toLocaleString()}{' '}
tokens
</div>
</div>
<div className='text-muted-foreground text-xs whitespace-nowrap'>
{model.context_length?.toLocaleString()}{' '}
tokens
</div>
</div>
))}
)
)}
</div>
)}
</TabsContent>
@@ -583,9 +597,11 @@ export default function ProvidersPage() {
value='provided'
className='mt-4 space-y-2'
>
{providerModels.remote_models.length > 0 && (
{providerModels.remote_models.length >
0 && (
<div className='text-muted-foreground mb-3 text-sm'>
Models automatically discovered from the provider&apos;s catalog.
Models automatically discovered from the
provider&apos;s catalog.
</div>
)}
<div className='space-y-2'>
@@ -670,7 +686,11 @@ export default function ProvidersPage() {
}
placeholder='https://api.example.com/v1'
disabled={hasFixedBaseUrl(formData.provider_type)}
className={hasFixedBaseUrl(formData.provider_type) ? 'cursor-not-allowed opacity-60' : ''}
className={
hasFixedBaseUrl(formData.provider_type)
? 'cursor-not-allowed opacity-60'
: ''
}
/>
</div>
<div className='grid gap-2'>
+25 -28
View File
@@ -59,18 +59,24 @@ import {
} from 'lucide-react';
import { toast } from 'sonner';
import { cn } from '@/lib/utils';
import {
sortModels,
groupAndSortModelsByProvider,
} from '@/lib/utils/modelSort';
interface ModelSelectorProps {
filterProvider?: string;
groupData?: ModelGroup;
showProviderActions?: boolean;
filteredModels?: Model[];
showDeleteAllButton?: boolean;
}
export function ModelSelector({
filterProvider,
groupData,
filteredModels: propFilteredModels,
showDeleteAllButton = false,
}: ModelSelectorProps) {
const [selectedModelId, setSelectedModelId] = useState<string>('');
const [, setHoveredModelId] = useState<string | null>(null);
@@ -387,14 +393,10 @@ export function ModelSelector({
providerIdNum,
model.id
);
await AdminService.updateProviderModel(
providerIdNum,
model.full_name,
{
...existingModel,
enabled: true,
}
);
await AdminService.updateProviderModel(providerIdNum, model.id, {
...existingModel,
enabled: true,
});
totalEnabled++;
} catch (error) {
console.error(`Failed to enable model ${model.full_name}:`, error);
@@ -426,20 +428,13 @@ export function ModelSelector({
: providerFilteredModels;
if (filterProvider) {
// If filtering by provider, return single group
return { [filterProvider]: modelsToGroup };
const sortedModels = sortModels(modelsToGroup);
return { [filterProvider]: sortedModels };
}
if (!modelsToGroup) return {};
return modelsToGroup.reduce<Record<string, Model[]>>((acc, model) => {
const provider = model.provider;
if (!acc[provider]) {
acc[provider] = [];
}
acc[provider].push(model);
return acc;
}, {});
return groupAndSortModelsByProvider(modelsToGroup);
}, [
providerFilteredModels,
filteredModels,
@@ -828,13 +823,15 @@ export function ModelSelector({
<Square className='mr-2 h-4 w-4' />
Deselect All
</Button>
<Button
onClick={handleDeleteAll}
className='text-destructive focus:text-destructive'
>
<AlertTriangle className='mr-2 h-4 w-4' />
Delete All Models Permanently
</Button>
{showDeleteAllButton && (
<Button
onClick={handleDeleteAll}
className='text-destructive focus:text-destructive'
>
<AlertTriangle className='mr-2 h-4 w-4' />
Delete All Overrides Permanently
</Button>
)}
{/* Model Management Actions
<Button onClick={() => setIsAddFormOpen(true)} className='gap-2'>
<Plus className='h-4 w-4' />
@@ -1077,9 +1074,9 @@ export function ModelSelector({
variant='ghost'
size='sm'
onClick={(e) => e.stopPropagation()}
className='h-8 w-8 p-0'
className='hover:bg-muted/50 dark:hover:bg-muted/80 h-8 w-8 p-0'
>
<MoreVertical className='h-4 w-4' />
<MoreVertical className='text-muted-foreground hover:text-foreground h-4 w-4' />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align='end'>
@@ -1181,7 +1178,7 @@ export function ModelSelector({
{model.soft_deleted && (
<span className='inline-flex items-center rounded-full border border-red-300 bg-red-100 px-2.5 py-0.5 text-xs font-medium text-red-800'>
<Trash2 className='mr-1 h-3 w-3' />
Deleted
Disabled
</span>
)}
</div>
+5 -3
View File
@@ -203,7 +203,8 @@ export function DetailedWalletBalance({
<div
className={cn(
'text-right font-mono',
!detail.error && ownerMsat > 0 &&
!detail.error &&
ownerMsat > 0 &&
'font-semibold text-green-600'
)}
>
@@ -250,7 +251,8 @@ export function DetailedWalletBalance({
<div
className={cn(
'truncate font-mono text-sm',
!detail.error && ownerMsat > 0 &&
!detail.error &&
ownerMsat > 0 &&
'font-semibold text-green-600'
)}
>
@@ -283,4 +285,4 @@ export function DetailedWalletBalance({
/>
</>
);
}
}
+5 -8
View File
@@ -11,10 +11,7 @@ import {
DollarSign,
Activity,
} from 'lucide-react';
import {
AdminService,
TemporaryBalance,
} from '@/lib/api/services/admin';
import { AdminService, TemporaryBalance } from '@/lib/api/services/admin';
import {
Card,
CardContent,
@@ -243,16 +240,16 @@ export function TemporaryBalances({
<div className='text-muted-foreground text-xs font-medium'>
Balance
</div>
<div className='truncate font-mono text-sm'>
{formatBalance(balance.balance)}
<div className='truncate font-mono text-sm'>
{formatBalance(balance.balance)}
</div>
</div>
<div className='space-y-1'>
<div className='text-muted-foreground text-xs font-medium'>
Spent
</div>
<div className='truncate font-mono text-sm'>
{formatBalance(balance.total_spent)}
<div className='truncate font-mono text-sm'>
{formatBalance(balance.total_spent)}
</div>
</div>
</div>
-1
View File
@@ -43,4 +43,3 @@ export function formatFromMsat(
});
return formatter.format(usd);
}
-1
View File
@@ -51,4 +51,3 @@ export async function fetchBtcUsdPrice(): Promise<number | null> {
export function btcToSatsRate(btcUsdPrice: number): number {
return btcUsdPrice / 100_000_000;
}
-1
View File
@@ -14,4 +14,3 @@ export function getDisplayUnitLabel(unit: DisplayUnit): string {
return unit;
}
}
+36
View File
@@ -0,0 +1,36 @@
import { type Model } from '@/lib/api/schemas/models';
export function sortModelsByStatus(a: Model, b: Model): number {
if (a.isEnabled && !b.isEnabled) return -1;
if (!a.isEnabled && b.isEnabled) return 1;
if (a.isEnabled === b.isEnabled) {
if (!a.soft_deleted && b.soft_deleted) return -1;
if (a.soft_deleted && !b.soft_deleted) return 1;
}
return 0;
}
export function sortModels(models: Model[]): Model[] {
return [...models].sort(sortModelsByStatus);
}
export function groupAndSortModelsByProvider(
models: Model[]
): Record<string, Model[]> {
const grouped = models.reduce<Record<string, Model[]>>((acc, model) => {
const provider = model.provider;
if (!acc[provider]) {
acc[provider] = [];
}
acc[provider].push(model);
return acc;
}, {});
Object.keys(grouped).forEach((provider) => {
grouped[provider].sort(sortModelsByStatus);
});
return grouped;
}