fix model update

This commit is contained in:
9qeklajc
2025-10-24 12:37:11 +02:00
parent 65c19bc375
commit 307e81bd61
4 changed files with 345 additions and 119 deletions
+289 -72
View File
@@ -3,11 +3,9 @@
import React, { useState, useEffect } from 'react';
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import {
ManualModelSchema,
type ManualModel,
type Model,
} from '@/lib/api/schemas/models';
import { z } from 'zod';
import { type Model } from '@/lib/api/schemas/models';
import { AdminService } from '@/lib/api/services/admin';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Textarea } from '@/components/ui/textarea';
@@ -29,63 +27,208 @@ import {
} from '@/components/ui/form';
import { Edit3, Loader2 } from 'lucide-react';
import { toast } from 'sonner';
import { Switch } from '@/components/ui/switch';
const EditModelFormSchema = z.object({
name: z.string().min(1, 'Name is required'),
description: z.string().optional(),
context_length: z.coerce.number().min(0),
prompt: z.coerce.number().min(0),
completion: z.coerce.number().min(0),
request: z.coerce.number().min(0).optional(),
image: z.coerce.number().min(0).optional(),
enabled: z.boolean(),
});
type EditModelFormData = z.infer<typeof EditModelFormSchema>;
interface EditModelFormProps {
model: Model;
onModelUpdate: (modelId: string, updatedModel: ManualModel) => void;
providerId?: number;
onModelUpdate?: () => void;
onCancel?: () => void;
isOpen: boolean;
}
export function EditModelForm({
model,
providerId,
onModelUpdate,
onCancel,
isOpen,
}: EditModelFormProps) {
const [isSubmitting, setIsSubmitting] = useState(false);
const [adminModelData, setAdminModelData] = useState<any>(null);
const [isNewOverride, setIsNewOverride] = useState(false);
const form = useForm<ManualModel>({
resolver: zodResolver(ManualModelSchema) as any, // eslint-disable-line @typescript-eslint/no-explicit-any
const form = useForm<EditModelFormData>({
resolver: zodResolver(EditModelFormSchema),
defaultValues: {
name: model.name,
full_name: model.full_name,
input_cost: model.input_cost,
output_cost: model.output_cost,
provider: model.provider,
modelType: model.modelType as ManualModel['modelType'],
description: model.description || '',
contextLength: model.contextLength || 0,
context_length: model.contextLength || 4096,
prompt: model.input_cost,
completion: model.output_cost,
request: 0,
image: 0,
enabled: model.isEnabled !== false,
},
});
// Reset form when model changes
useEffect(() => {
form.reset({
name: model.name,
full_name: model.full_name,
input_cost: model.input_cost,
output_cost: model.output_cost,
provider: model.provider,
modelType: model.modelType as ManualModel['modelType'],
description: model.description || '',
contextLength: model.contextLength || 0,
});
}, [model, form]);
if (isOpen && providerId) {
loadAdminModel();
} else if (isOpen && !providerId) {
console.error('EditModelForm opened without providerId', {
model,
providerId,
});
toast.error('Missing provider information for this model');
}
}, [isOpen, model.id, providerId]);
const loadAdminModel = async () => {
if (!providerId) {
console.error('loadAdminModel called without providerId');
return;
}
try {
console.log('Loading admin model:', {
providerId,
modelId: model.full_name,
});
const adminModel = await AdminService.getProviderModel(
providerId,
model.full_name
);
setAdminModelData(adminModel);
setIsNewOverride(false);
form.reset({
name: adminModel.name,
description: adminModel.description || '',
context_length: adminModel.context_length,
prompt: adminModel.pricing.prompt || 0,
completion: adminModel.pricing.completion || 0,
request: adminModel.pricing.request || 0,
image: adminModel.pricing.image || 0,
enabled: adminModel.enabled !== false,
});
} catch (error: any) {
console.log('Model not in database, will create new override:', error);
setIsNewOverride(true);
setAdminModelData({
id: model.full_name,
name: model.name,
description: model.description || '',
created: Math.floor(Date.now() / 1000),
context_length: model.contextLength || 4096,
architecture: {
modality: model.modelType || 'text',
input_modalities: [model.modelType || 'text'],
output_modalities: [model.modelType || 'text'],
tokenizer: '',
instruct_type: null,
},
pricing: {
prompt: model.input_cost,
completion: model.output_cost,
request: 0,
image: 0,
web_search: 0,
internal_reasoning: 0,
},
per_request_limits: null,
top_provider: null,
upstream_provider_id: providerId,
enabled: model.isEnabled !== false,
});
form.reset({
name: model.name,
description: model.description || '',
context_length: model.contextLength || 4096,
prompt: model.input_cost,
completion: model.output_cost,
request: 0,
image: 0,
enabled: model.isEnabled !== false,
});
}
};
const onSubmit = async (data: EditModelFormData) => {
if (!providerId) {
console.error('onSubmit called without providerId', {
model,
providerId,
});
toast.error('Missing provider ID - cannot update model');
return;
}
if (!adminModelData) {
console.error('onSubmit called without adminModelData', {
model,
providerId,
adminModelData,
});
toast.error('Model data not loaded - please try reopening the form');
return;
}
const onSubmit = async (data: ManualModel) => {
setIsSubmitting(true);
try {
// Ensure we're sending the correct API key value
const updatedData = {
...data,
const payload = {
id: adminModelData.id,
name: data.name,
description: data.description || '',
created: adminModelData.created || Math.floor(Date.now() / 1000),
context_length: data.context_length,
architecture: adminModelData.architecture || {
modality: 'text',
input_modalities: ['text'],
output_modalities: ['text'],
tokenizer: '',
instruct_type: null,
},
pricing: {
prompt: data.prompt,
completion: data.completion,
request: data.request || 0,
image: data.image || 0,
web_search: 0,
internal_reasoning: 0,
},
per_request_limits: adminModelData.per_request_limits,
top_provider: adminModelData.top_provider,
upstream_provider_id: providerId,
enabled: data.enabled,
};
await onModelUpdate(model.id, updatedData);
toast.success('Model updated successfully!');
if (isNewOverride) {
console.log('Creating new model override');
await AdminService.createProviderModel(providerId, payload);
toast.success('Model override created successfully!');
} else {
console.log('Updating existing model override');
await AdminService.updateProviderModel(
providerId,
adminModelData.id,
payload
);
toast.success('Model updated successfully!');
}
onModelUpdate?.();
onCancel?.();
} catch (error) {
toast.error('Failed to update model. Please try again.');
console.error('Error updating model:', error);
const action = isNewOverride ? 'create' : 'update';
toast.error(`Failed to ${action} model. Please try again.`);
console.error(`Error ${action}ing model:`, error);
} finally {
setIsSubmitting(false);
}
@@ -103,33 +246,18 @@ export function EditModelForm({
<DialogHeader>
<DialogTitle className='flex items-center gap-2'>
<Edit3 className='h-5 w-5' />
Edit Model
{isNewOverride ? 'Create Model Override' : 'Edit Model Override'}
</DialogTitle>
<DialogDescription>
Update the details for &quot;{model.name}&quot;
{isNewOverride
? `Create an override for &quot;${model.name}&quot;`
: `Update the model override for &quot;${model.name}&quot;`}
</DialogDescription>
</DialogHeader>
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className='space-y-4'>
<div className='grid grid-cols-1 gap-4 sm:grid-cols-2'>
<FormField
control={form.control}
name='full_name'
render={({ field }) => (
<FormItem>
<FormLabel>Original Model Name</FormLabel>
<FormControl>
<Input {...field} className='bg-muted w-full' disabled />
</FormControl>
<FormDescription>
Original name from the provider (cannot be changed)
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name='name'
@@ -153,19 +281,21 @@ export function EditModelForm({
<FormField
control={form.control}
name='provider'
name='context_length'
render={({ field }) => (
<FormItem>
<FormLabel>Provider *</FormLabel>
<FormLabel>Context Length *</FormLabel>
<FormControl>
<Input
placeholder='e.g., OpenAI, Anthropic'
type='number'
min='0'
placeholder='4096'
{...field}
className='w-full'
/>
</FormControl>
<FormDescription>
AI model provider or company
Maximum context window size
</FormDescription>
<FormMessage />
</FormItem>
@@ -173,10 +303,32 @@ export function EditModelForm({
/>
</div>
<FormField
control={form.control}
name='description'
render={({ field }) => (
<FormItem>
<FormLabel>Description</FormLabel>
<FormControl>
<Textarea
placeholder='Brief description of the model...'
{...field}
rows={3}
className='w-full'
/>
</FormControl>
<FormDescription>
Optional description or notes about the model
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<div className='grid grid-cols-1 gap-4 sm:grid-cols-2'>
<FormField
control={form.control}
name='input_cost'
name='prompt'
render={({ field }) => (
<FormItem>
<FormLabel>Input Cost (per 1M tokens) *</FormLabel>
@@ -208,7 +360,7 @@ export function EditModelForm({
<FormField
control={form.control}
name='output_cost'
name='completion'
render={({ field }) => (
<FormItem>
<FormLabel>Output Cost (per 1M tokens) *</FormLabel>
@@ -239,24 +391,87 @@ export function EditModelForm({
/>
</div>
<div className='grid grid-cols-1 gap-4 sm:grid-cols-2'>
<FormField
control={form.control}
name='request'
render={({ field }) => (
<FormItem>
<FormLabel>Request Cost (per request)</FormLabel>
<FormControl>
<Input
type='number'
step='0.001'
min='0'
placeholder='0.00'
{...field}
value={
field.value ? parseFloat(field.value.toFixed(3)) : ''
}
onChange={(e) => {
const value = parseFloat(e.target.value) || 0;
const rounded = Math.round(value * 1000) / 1000;
field.onChange(rounded);
}}
className='w-full'
/>
</FormControl>
<FormDescription>
Fixed cost per request in USD
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name='image'
render={({ field }) => (
<FormItem>
<FormLabel>Image Cost (per image)</FormLabel>
<FormControl>
<Input
type='number'
step='0.001'
min='0'
placeholder='0.00'
{...field}
value={
field.value ? parseFloat(field.value.toFixed(3)) : ''
}
onChange={(e) => {
const value = parseFloat(e.target.value) || 0;
const rounded = Math.round(value * 1000) / 1000;
field.onChange(rounded);
}}
className='w-full'
/>
</FormControl>
<FormDescription>Cost per image in USD</FormDescription>
<FormMessage />
</FormItem>
)}
/>
</div>
<FormField
control={form.control}
name='description'
name='enabled'
render={({ field }) => (
<FormItem>
<FormLabel>Description</FormLabel>
<FormItem className='flex flex-row items-center justify-between rounded-lg border p-4'>
<div className='space-y-0.5'>
<FormLabel className='text-base'>Model Enabled</FormLabel>
<FormDescription>
Enable or disable this model override
</FormDescription>
</div>
<FormControl>
<Textarea
placeholder='Brief description of the model...'
{...field}
rows={3}
className='w-full'
<Switch
checked={field.value}
onCheckedChange={field.onChange}
/>
</FormControl>
<FormDescription>
Optional description or notes about the model
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
@@ -274,8 +489,10 @@ export function EditModelForm({
{isSubmitting ? (
<>
<Loader2 className='mr-2 h-4 w-4 animate-spin' />
Updating...
{isNewOverride ? 'Creating...' : 'Updating...'}
</>
) : isNewOverride ? (
'Create Override'
) : (
'Update Model'
)}
+6 -16
View File
@@ -368,22 +368,7 @@ export function ModelSelector({
};
// Handle model update
const handleModelUpdate = async (
modelId: string,
updatedModel: ManualModel
) => {
const existingModel = models.find((m) => m.id === modelId);
if (!existingModel) {
throw new Error('Model not found');
}
const updateData = {
...updatedModel,
provider_id: existingModel.provider_id,
isEnabled: !existingModel.soft_deleted,
};
await AdminService.updateModel(modelId, updateData);
const handleModelUpdate = async () => {
await refetchModels();
setEditingModel(null);
};
@@ -1114,6 +1099,11 @@ export function ModelSelector({
{editingModel && (
<EditModelForm
model={editingModel}
providerId={
editingModel.provider_id
? parseInt(editingModel.provider_id)
: undefined
}
onModelUpdate={handleModelUpdate}
onCancel={() => setEditingModel(null)}
isOpen={!!editingModel}
+12 -14
View File
@@ -33,7 +33,8 @@ export function AdminSettings() {
const data = await AdminService.getSettings();
setSettings(JSON.stringify(data, null, 2));
} catch (err) {
const message = err instanceof Error ? err.message : 'Failed to load settings';
const message =
err instanceof Error ? err.message : 'Failed to load settings';
setError(message);
toast.error(message);
} finally {
@@ -50,7 +51,8 @@ export function AdminSettings() {
try {
payload = JSON.parse(settings);
} catch (e) {
const message = 'Invalid JSON: ' + (e instanceof Error ? e.message : String(e));
const message =
'Invalid JSON: ' + (e instanceof Error ? e.message : String(e));
setError(message);
toast.error(message);
return;
@@ -66,7 +68,8 @@ export function AdminSettings() {
setSettings(JSON.stringify(updatedData, null, 2));
toast.success('Settings saved successfully');
} catch (err) {
const message = err instanceof Error ? err.message : 'Failed to save settings';
const message =
err instanceof Error ? err.message : 'Failed to save settings';
setError(message);
toast.error(message);
} finally {
@@ -77,21 +80,20 @@ export function AdminSettings() {
return (
<>
<div className='mb-6'>
<h2 className='text-xl font-semibold tracking-tight'>
Admin Settings
</h2>
<h2 className='text-xl font-semibold tracking-tight'>Admin Settings</h2>
</div>
<Card>
<CardHeader>
<CardTitle>Server Configuration (JSON)</CardTitle>
<CardDescription>
Edit server settings in JSON format. Values shown as &quot;[REDACTED]&quot; will remain unchanged if left as-is.
Edit server settings in JSON format. Values shown as
&quot;[REDACTED]&quot; will remain unchanged if left as-is.
</CardDescription>
</CardHeader>
<CardContent className='space-y-4'>
{loading ? (
<div className='flex items-center justify-center py-8 text-muted-foreground'>
<div className='text-muted-foreground flex items-center justify-center py-8'>
Loading settings...
</div>
) : (
@@ -105,7 +107,7 @@ export function AdminSettings() {
<textarea
value={settings}
onChange={(e) => setSettings(e.target.value)}
className='w-full min-h-[400px] font-mono text-sm bg-muted p-4 rounded-md border focus:outline-none focus:ring-2 focus:ring-ring'
className='bg-muted focus:ring-ring min-h-[400px] w-full rounded-md border p-4 font-mono text-sm focus:ring-2 focus:outline-none'
placeholder='{}'
/>
</>
@@ -119,10 +121,7 @@ export function AdminSettings() {
>
Reload
</Button>
<Button
onClick={handleSave}
disabled={loading || saving}
>
<Button onClick={handleSave} disabled={loading || saving}>
<Save className='mr-2 h-4 w-4' />
{saving ? 'Saving...' : 'Save Settings'}
</Button>
@@ -131,4 +130,3 @@ export function AdminSettings() {
</>
);
}
+38 -17
View File
@@ -125,10 +125,16 @@ export class AdminService {
if (!pricing) return pricing;
const result = { ...pricing };
if (typeof result.prompt === 'number') {
result.prompt = result.prompt * 1000;
result.prompt = result.prompt * 1000000;
}
if (typeof result.completion === 'number') {
result.completion = result.completion * 1000;
result.completion = result.completion * 1000000;
}
if (typeof result.request === 'number') {
result.request = result.request * 1000000;
}
if (typeof result.image === 'number') {
result.image = result.image * 1000000;
}
return result;
}
@@ -139,10 +145,16 @@ export class AdminService {
if (!pricing) return pricing;
const result = { ...pricing };
if (typeof result.prompt === 'number') {
result.prompt = result.prompt;
result.prompt = result.prompt / 1000000;
}
if (typeof result.completion === 'number') {
result.completion = result.completion;
result.completion = result.completion / 1000000;
}
if (typeof result.request === 'number') {
result.request = result.request / 1000000;
}
if (typeof result.image === 'number') {
result.image = result.image / 1000000;
}
return result;
}
@@ -259,17 +271,7 @@ export class AdminService {
const data = await apiClient.get<ProviderModels>(
`/admin/api/upstream-providers/${providerId}/models`
);
return {
...data,
db_models: data.db_models.map((m) => ({
...m,
pricing: this.convertPricingToPerMillionTokens(m.pricing),
})),
remote_models: data.remote_models.map((m) => ({
...m,
pricing: this.convertPricingToPerMillionTokens(m.pricing),
})),
};
return data;
}
static async createProviderModel(
@@ -280,6 +282,10 @@ export class AdminService {
...data,
pricing: this.convertPricingToPerToken(data.pricing),
};
console.log('Creating provider model - pricing conversion:', {
original: data.pricing,
converted: payload.pricing,
});
const model = await apiClient.post<AdminModel>(
`/admin/api/upstream-providers/${providerId}/models`,
payload
@@ -326,6 +332,10 @@ export class AdminService {
...data,
pricing: this.convertPricingToPerToken(data.pricing),
};
console.log('Updating provider model - pricing conversion:', {
original: data.pricing,
converted: payload.pricing,
});
const model = await apiClient.patch<AdminModel>(
`/admin/api/upstream-providers/${providerId}/models/${encodeURIComponent(modelId)}`,
payload
@@ -377,8 +387,15 @@ export class AdminService {
providerModels.db_models.forEach((dbModel) => {
seenModelIds.add(dbModel.id);
const modelWithProvider = {
...dbModel,
upstream_provider_id: provider.id,
};
allModels.push({
...this.transformAdminModelToModel(dbModel, provider.provider_type),
...this.transformAdminModelToModel(
modelWithProvider,
provider.provider_type
),
has_own_api_key: false,
api_key_type: 'group',
});
@@ -386,9 +403,13 @@ export class AdminService {
providerModels.remote_models.forEach((remoteModel) => {
if (!seenModelIds.has(remoteModel.id)) {
const modelWithProvider = {
...remoteModel,
upstream_provider_id: provider.id,
};
allModels.push({
...this.transformAdminModelToModel(
remoteModel,
modelWithProvider,
provider.provider_type
),
has_own_api_key: false,