mirror of
https://github.com/Routstr/routstr-core.git
synced 2026-07-22 12:22:20 +00:00
Compare commits
4 Commits
refactor/m
...
batch-over
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e50facc835 | ||
|
|
55dc485705 | ||
|
|
80559a57d5 | ||
|
|
8e9f6647e7 |
@@ -2554,6 +2554,91 @@ async def delete_all_provider_models(provider_id: int) -> dict[str, object]:
|
||||
return {"ok": True, "deleted": len(rows)}
|
||||
|
||||
|
||||
class BatchOverrideRequest(BaseModel):
|
||||
models: list[ModelCreate]
|
||||
|
||||
|
||||
@admin_router.post(
|
||||
"/api/upstream-providers/{provider_id}/batch-override",
|
||||
dependencies=[Depends(require_admin_api)],
|
||||
)
|
||||
async def batch_override_provider_models(
|
||||
provider_id: int, payload: BatchOverrideRequest
|
||||
) -> dict[str, object]:
|
||||
"""Batch override models for a specific provider."""
|
||||
logger.info(
|
||||
f"BATCH_OVERRIDE called: provider_id={provider_id}, count={len(payload.models)}"
|
||||
)
|
||||
|
||||
async with create_session() as session:
|
||||
provider = await session.get(UpstreamProviderRow, provider_id)
|
||||
if not provider:
|
||||
raise HTTPException(status_code=404, detail="Provider not found")
|
||||
|
||||
overridden_count = 0
|
||||
|
||||
for model_data in payload.models:
|
||||
# Try to get existing model regardless of whether it's enabled or not
|
||||
existing_row = await session.get(ModelRow, (model_data.id, provider_id))
|
||||
|
||||
if existing_row:
|
||||
# Update existing
|
||||
existing_row.name = model_data.name
|
||||
existing_row.description = model_data.description
|
||||
existing_row.created = int(model_data.created)
|
||||
existing_row.context_length = int(model_data.context_length)
|
||||
existing_row.architecture = json.dumps(model_data.architecture)
|
||||
existing_row.pricing = json.dumps(model_data.pricing)
|
||||
existing_row.sats_pricing = None
|
||||
existing_row.per_request_limits = (
|
||||
json.dumps(model_data.per_request_limits)
|
||||
if model_data.per_request_limits is not None
|
||||
else None
|
||||
)
|
||||
existing_row.top_provider = (
|
||||
json.dumps(model_data.top_provider) if model_data.top_provider else None
|
||||
)
|
||||
existing_row.canonical_slug = model_data.canonical_slug
|
||||
existing_row.alias_ids = (
|
||||
json.dumps(model_data.alias_ids) if model_data.alias_ids else None
|
||||
)
|
||||
existing_row.enabled = model_data.enabled
|
||||
session.add(existing_row)
|
||||
else:
|
||||
# Create new
|
||||
row = ModelRow(
|
||||
id=model_data.id,
|
||||
name=model_data.name,
|
||||
description=model_data.description,
|
||||
created=int(model_data.created),
|
||||
context_length=int(model_data.context_length),
|
||||
architecture=json.dumps(model_data.architecture),
|
||||
pricing=json.dumps(model_data.pricing),
|
||||
sats_pricing=None,
|
||||
per_request_limits=(
|
||||
json.dumps(model_data.per_request_limits)
|
||||
if model_data.per_request_limits is not None
|
||||
else None
|
||||
),
|
||||
top_provider=(
|
||||
json.dumps(model_data.top_provider) if model_data.top_provider else None
|
||||
),
|
||||
canonical_slug=model_data.canonical_slug,
|
||||
alias_ids=(
|
||||
json.dumps(model_data.alias_ids) if model_data.alias_ids else None
|
||||
),
|
||||
upstream_provider_id=provider_id,
|
||||
enabled=model_data.enabled,
|
||||
)
|
||||
session.add(row)
|
||||
|
||||
overridden_count += 1
|
||||
|
||||
await session.commit()
|
||||
|
||||
await refresh_model_maps()
|
||||
return {"ok": True, "count": overridden_count, "message": f"Successfully batch overridden {overridden_count} models"}
|
||||
|
||||
class UpstreamProviderCreate(BaseModel):
|
||||
provider_type: str
|
||||
base_url: str
|
||||
@@ -2598,14 +2683,12 @@ async def create_upstream_provider(
|
||||
async with create_session() as session:
|
||||
result = await session.exec(
|
||||
select(UpstreamProviderRow).where(
|
||||
UpstreamProviderRow.base_url == payload.base_url,
|
||||
UpstreamProviderRow.api_key == payload.api_key,
|
||||
UpstreamProviderRow.base_url == payload.base_url
|
||||
)
|
||||
)
|
||||
if result.first():
|
||||
raise HTTPException(
|
||||
status_code=409,
|
||||
detail="Provider with this base URL and API key already exists",
|
||||
status_code=409, detail="Provider with this base URL already exists"
|
||||
)
|
||||
|
||||
provider = UpstreamProviderRow(
|
||||
|
||||
@@ -21,6 +21,7 @@ import {
|
||||
AdminModel,
|
||||
} from '@/lib/api/services/admin';
|
||||
import { AddProviderModelDialog } from '@/components/AddProviderModelDialog';
|
||||
import { BatchOverrideDialog } from '@/components/BatchOverrideDialog';
|
||||
import { Skeleton } from '@/components/ui/skeleton';
|
||||
import {
|
||||
AlertCircle,
|
||||
@@ -405,6 +406,9 @@ export default function ProvidersPage() {
|
||||
mode: 'create',
|
||||
initialData: null,
|
||||
});
|
||||
const [batchOverrideProviderId, setBatchOverrideProviderId] = useState<
|
||||
number | null
|
||||
>(null);
|
||||
|
||||
const [formData, setFormData] = useState<CreateUpstreamProvider>({
|
||||
provider_type: 'openrouter',
|
||||
@@ -629,6 +633,10 @@ export default function ProvidersPage() {
|
||||
});
|
||||
};
|
||||
|
||||
const handleBatchOverride = (providerId: number) => {
|
||||
setBatchOverrideProviderId(providerId);
|
||||
};
|
||||
|
||||
return (
|
||||
<SidebarProvider>
|
||||
<AppSidebar variant='inset' />
|
||||
@@ -986,16 +994,28 @@ export default function ProvidersPage() {
|
||||
provider's catalog.
|
||||
</div>
|
||||
)}
|
||||
<Button
|
||||
variant='outline'
|
||||
size='sm'
|
||||
onClick={() =>
|
||||
handleAddModel(provider.id)
|
||||
}
|
||||
>
|
||||
<Plus className='mr-2 h-4 w-4' />
|
||||
Add
|
||||
</Button>
|
||||
<div className='flex gap-2'>
|
||||
<Button
|
||||
variant='outline'
|
||||
size='sm'
|
||||
onClick={() =>
|
||||
handleBatchOverride(provider.id)
|
||||
}
|
||||
>
|
||||
<Database className='mr-2 h-4 w-4' />
|
||||
Batch Override
|
||||
</Button>
|
||||
<Button
|
||||
variant='outline'
|
||||
size='sm'
|
||||
onClick={() =>
|
||||
handleAddModel(provider.id)
|
||||
}
|
||||
>
|
||||
<Plus className='mr-2 h-4 w-4' />
|
||||
Add Custom Model
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
{providerModels.db_models.length === 0 ? (
|
||||
<div className='text-muted-foreground py-4 text-center text-sm'>
|
||||
@@ -1287,6 +1307,19 @@ export default function ProvidersPage() {
|
||||
mode={modelDialogState.mode}
|
||||
/>
|
||||
)}
|
||||
|
||||
{batchOverrideProviderId && (
|
||||
<BatchOverrideDialog
|
||||
providerId={batchOverrideProviderId}
|
||||
isOpen={!!batchOverrideProviderId}
|
||||
onClose={() => setBatchOverrideProviderId(null)}
|
||||
onSuccess={() => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: ['provider-models', batchOverrideProviderId],
|
||||
});
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</SidebarInset>
|
||||
</SidebarProvider>
|
||||
);
|
||||
|
||||
144
ui/components/BatchOverrideDialog.tsx
Normal file
144
ui/components/BatchOverrideDialog.tsx
Normal file
@@ -0,0 +1,144 @@
|
||||
'use client';
|
||||
|
||||
import React, { useState } from 'react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from '@/components/ui/dialog';
|
||||
import { Textarea } from '@/components/ui/textarea';
|
||||
import { toast } from 'sonner';
|
||||
import { AdminService } from '@/lib/api/services/admin';
|
||||
import { Loader2, Database } from 'lucide-react';
|
||||
|
||||
export interface BatchOverrideDialogProps {
|
||||
providerId: number;
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
onSuccess: () => void;
|
||||
}
|
||||
|
||||
export function BatchOverrideDialog({
|
||||
providerId,
|
||||
isOpen,
|
||||
onClose,
|
||||
onSuccess,
|
||||
}: BatchOverrideDialogProps) {
|
||||
const [jsonInput, setJsonInput] = useState('');
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
|
||||
const sampleJson = {
|
||||
models: [
|
||||
{
|
||||
id: 'model-id-1',
|
||||
name: 'Model Name 1',
|
||||
description: 'Description...',
|
||||
created: Math.floor(Date.now() / 1000),
|
||||
context_length: 8192,
|
||||
architecture: {
|
||||
modality: 'text',
|
||||
input_modalities: ['text'],
|
||||
output_modalities: ['text'],
|
||||
tokenizer: '',
|
||||
instruct_type: null,
|
||||
},
|
||||
pricing: {
|
||||
prompt: 0.0,
|
||||
completion: 0.0,
|
||||
request: 0.0,
|
||||
image: 0.0,
|
||||
web_search: 0.0,
|
||||
internal_reasoning: 0.0,
|
||||
},
|
||||
enabled: true,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const handleBatchOverride = async () => {
|
||||
if (!jsonInput.trim()) {
|
||||
toast.error('Please enter JSON content');
|
||||
return;
|
||||
}
|
||||
|
||||
setIsSubmitting(true);
|
||||
try {
|
||||
let data;
|
||||
try {
|
||||
data = JSON.parse(jsonInput);
|
||||
} catch {
|
||||
throw new Error('Invalid JSON format');
|
||||
}
|
||||
|
||||
if (!data.models || !Array.isArray(data.models)) {
|
||||
throw new Error('JSON match follow structure: { "models": [...] }');
|
||||
}
|
||||
|
||||
const result = await AdminService.batchOverrideProviderModels(
|
||||
providerId,
|
||||
data.models
|
||||
);
|
||||
|
||||
if (result.ok) {
|
||||
toast.success(result.message || 'Batch override successful');
|
||||
onSuccess();
|
||||
onClose();
|
||||
setJsonInput('');
|
||||
} else {
|
||||
throw new Error('Batch override failed');
|
||||
}
|
||||
} catch (error: unknown) {
|
||||
const message =
|
||||
error instanceof Error ? error.message : 'Batch override failed';
|
||||
toast.error(message);
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={isOpen} onOpenChange={(open) => !open && onClose()}>
|
||||
<DialogContent className='sm:max-w-[800px]'>
|
||||
<DialogHeader>
|
||||
<DialogTitle className='flex items-center gap-2'>
|
||||
<Database className='h-4 w-4' />
|
||||
Batch Override Models
|
||||
</DialogTitle>
|
||||
<DialogDescription>
|
||||
Paste a JSON object with a "models" array containing model
|
||||
definitions. Existing models with the same ID will be updated.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className='grid gap-4 py-4'>
|
||||
<Textarea
|
||||
value={jsonInput}
|
||||
onChange={(e) => setJsonInput(e.target.value)}
|
||||
placeholder={JSON.stringify(sampleJson, null, 2)}
|
||||
className='min-h-[400px] font-mono text-xs'
|
||||
/>
|
||||
</div>
|
||||
|
||||
<DialogFooter>
|
||||
<Button variant='outline' onClick={onClose}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button onClick={handleBatchOverride} disabled={isSubmitting}>
|
||||
{isSubmitting ? (
|
||||
<>
|
||||
<Loader2 className='mr-2 h-4 w-4 animate-spin' />
|
||||
Processing...
|
||||
</>
|
||||
) : (
|
||||
'Batch Override'
|
||||
)}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
@@ -341,6 +341,23 @@ export class AdminService {
|
||||
};
|
||||
}
|
||||
|
||||
static async batchOverrideProviderModels(
|
||||
providerId: number,
|
||||
models: AdminModel[]
|
||||
): Promise<{ ok: boolean; count: number; message: string }> {
|
||||
const payload = {
|
||||
models: models.map((m) => ({
|
||||
...m,
|
||||
pricing: this.convertPricingToPerToken(m.pricing),
|
||||
})),
|
||||
};
|
||||
return await apiClient.post<{
|
||||
ok: boolean;
|
||||
count: number;
|
||||
message: string;
|
||||
}>(`/admin/api/upstream-providers/${providerId}/batch-override`, payload);
|
||||
}
|
||||
|
||||
static async getProviderModel(
|
||||
providerId: number,
|
||||
modelId: string
|
||||
|
||||
Reference in New Issue
Block a user