mirror of
https://github.com/Routstr/routstr-core.git
synced 2026-08-11 11:47:50 +00:00
Merge pull request #215 from Routstr/v0.2.0-final
* fix model filtering * cleanup desing
This commit is contained in:
@@ -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
@@ -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()}
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -49,6 +49,7 @@ else
|
||||
npm run build
|
||||
fi
|
||||
|
||||
rm -rf ../ui_out
|
||||
mkdir -p ../ui_out
|
||||
mv out/* ../ui_out
|
||||
|
||||
|
||||
+1
-1
@@ -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
@@ -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
@@ -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's catalog.
|
||||
Custom models override or extend the
|
||||
provider'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's catalog.
|
||||
Models automatically discovered from the
|
||||
provider'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'>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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({
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -43,4 +43,3 @@ export function formatFromMsat(
|
||||
});
|
||||
return formatter.format(usd);
|
||||
}
|
||||
|
||||
|
||||
@@ -51,4 +51,3 @@ export async function fetchBtcUsdPrice(): Promise<number | null> {
|
||||
export function btcToSatsRate(btcUsdPrice: number): number {
|
||||
return btcUsdPrice / 100_000_000;
|
||||
}
|
||||
|
||||
|
||||
@@ -14,4 +14,3 @@ export function getDisplayUnitLabel(unit: DisplayUnit): string {
|
||||
return unit;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
Reference in New Issue
Block a user