This commit is contained in:
9qeklajc
2025-10-24 11:50:45 +02:00
parent f9eaf48f45
commit 65c19bc375
4 changed files with 635 additions and 149 deletions
+395 -117
View File
File diff suppressed because it is too large Load Diff
+5
View File
@@ -3,6 +3,7 @@
import * as React from 'react';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
import { ServerConfigSettings } from '@/components/settings/server-config-settings';
import { AdminSettings } from '@/components/settings/admin-settings';
import { SiteHeader } from '@/components/site-header';
import { AppSidebar } from '@/components/app-sidebar';
import { SidebarInset, SidebarProvider } from '@/components/ui/sidebar';
@@ -22,10 +23,14 @@ export default function SettingsPage() {
<Tabs defaultValue='server' className='w-full'>
<TabsList className='mb-4'>
<TabsTrigger value='server'>Server Configuration</TabsTrigger>
<TabsTrigger value='admin'>Admin Settings</TabsTrigger>
</TabsList>
<TabsContent value='server'>
<ServerConfigSettings />
</TabsContent>
<TabsContent value='admin'>
<AdminSettings />
</TabsContent>
</Tabs>
</div>
</div>
+134
View File
@@ -0,0 +1,134 @@
'use client';
import * as React from 'react';
import { useState, useEffect } from 'react';
import { AdminService } from '@/lib/api/services/admin';
import {
Card,
CardContent,
CardHeader,
CardTitle,
CardDescription,
CardFooter,
} from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Alert, AlertDescription } from '@/components/ui/alert';
import { AlertCircle, Save } from 'lucide-react';
import { toast } from 'sonner';
export function AdminSettings() {
const [settings, setSettings] = useState<string>('{}');
const [loading, setLoading] = useState(true);
const [saving, setSaving] = useState(false);
const [error, setError] = useState<string>('');
useEffect(() => {
loadSettings();
}, []);
const loadSettings = async () => {
try {
setLoading(true);
setError('');
const data = await AdminService.getSettings();
setSettings(JSON.stringify(data, null, 2));
} catch (err) {
const message = err instanceof Error ? err.message : 'Failed to load settings';
setError(message);
toast.error(message);
} finally {
setLoading(false);
}
};
const handleSave = async () => {
try {
setSaving(true);
setError('');
let payload: Record<string, unknown>;
try {
payload = JSON.parse(settings);
} catch (e) {
const message = 'Invalid JSON: ' + (e instanceof Error ? e.message : String(e));
setError(message);
toast.error(message);
return;
}
['upstream_api_key', 'admin_password', 'nsec'].forEach((k) => {
if (payload && payload[k] === '[REDACTED]') {
delete payload[k];
}
});
const updatedData = await AdminService.updateSettings(payload);
setSettings(JSON.stringify(updatedData, null, 2));
toast.success('Settings saved successfully');
} catch (err) {
const message = err instanceof Error ? err.message : 'Failed to save settings';
setError(message);
toast.error(message);
} finally {
setSaving(false);
}
};
return (
<>
<div className='mb-6'>
<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.
</CardDescription>
</CardHeader>
<CardContent className='space-y-4'>
{loading ? (
<div className='flex items-center justify-center py-8 text-muted-foreground'>
Loading settings...
</div>
) : (
<>
{error && (
<Alert variant='destructive'>
<AlertCircle className='h-4 w-4' />
<AlertDescription>{error}</AlertDescription>
</Alert>
)}
<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'
placeholder='{}'
/>
</>
)}
</CardContent>
<CardFooter className='flex justify-between'>
<Button
variant='outline'
onClick={loadSettings}
disabled={loading || saving}
>
Reload
</Button>
<Button
onClick={handleSave}
disabled={loading || saving}
>
<Save className='mr-2 h-4 w-4' />
{saving ? 'Saving...' : 'Save Settings'}
</Button>
</CardFooter>
</Card>
</>
);
}
+101 -32
View File
@@ -303,6 +303,20 @@ export class AdminService {
};
}
static async getModel(
modelId: string,
providerId: number | null = null
): Promise<AdminModel> {
const model = await apiClient.post<AdminModel>('/admin/api/models/get', {
model_id: modelId,
provider_id: providerId,
});
return {
...model,
pricing: this.convertPricingToPerMillionTokens(model.pricing),
};
}
static async updateProviderModel(
providerId: number,
modelId: string,
@@ -437,9 +451,9 @@ export class AdminService {
static async createModel(
data: Record<string, unknown>
): Promise<AdminModelAsModel> {
if (!data.provider_id) {
throw new Error('provider_id is required to create a model');
}
const providerId = data.provider_id
? parseInt(data.provider_id as string)
: null;
const pricing = {
prompt: (data.input_cost as number) / 1000000,
@@ -450,8 +464,11 @@ export class AdminService {
internal_reasoning: 0,
};
const adminModel: AdminModel = {
id: (data.id as string) || (data.full_name as string),
const modelId = (data.id as string) || (data.full_name as string);
const payload = {
model_id: modelId,
provider_id: providerId,
name: (data.name as string) || (data.full_name as string),
description: (data.description as string) || '',
created: Math.floor(Date.now() / 1000),
@@ -463,28 +480,35 @@ export class AdminService {
tokenizer: '',
instruct_type: null,
},
pricing,
pricing: this.convertPricingToPerToken(pricing),
per_request_limits: null,
top_provider: null,
upstream_provider_id: parseInt(data.provider_id as string),
enabled: data.isEnabled !== false,
};
const providerId = parseInt(data.provider_id as string);
const created = await this.createProviderModel(providerId, adminModel);
return this.transformAdminModelToModel(created, data.provider as string);
const created = await apiClient.post<AdminModel>(
'/admin/api/models/create',
payload
);
return this.transformAdminModelToModel(
{
...created,
pricing: this.convertPricingToPerMillionTokens(created.pricing),
},
data.provider as string
);
}
static async updateModel(
modelId: string,
data: Record<string, unknown>
): Promise<AdminModelAsModel> {
if (!data.provider_id) {
throw new Error('provider_id is required to update a model');
}
const providerId = data.provider_id
? parseInt(data.provider_id as string)
: null;
const providerId = parseInt(data.provider_id as string);
const existingModel = await this.getProviderModel(providerId, modelId);
const existingModel = await this.getModel(modelId, providerId);
const pricing = {
prompt: (data.input_cost as number) / 1000000,
@@ -495,9 +519,13 @@ export class AdminService {
internal_reasoning: 0,
};
const payload: AdminModel = {
const payload: AdminModel & {
model_id: string;
provider_id: number | null;
} = {
...existingModel,
id: modelId,
model_id: modelId,
provider_id: providerId,
pricing,
};
@@ -508,22 +536,32 @@ export class AdminService {
if (data.isEnabled !== undefined)
payload.enabled = data.isEnabled as boolean;
const updated = await this.updateProviderModel(
providerId,
modelId,
payload
const updated = await apiClient.post<AdminModel>(
'/admin/api/models/update',
{
...payload,
pricing: this.convertPricingToPerToken(payload.pricing),
}
);
return this.transformAdminModelToModel(
{
...updated,
pricing: this.convertPricingToPerMillionTokens(updated.pricing),
},
data.provider as string
);
return this.transformAdminModelToModel(updated, data.provider as string);
}
static async deleteModel(
modelId: string,
providerId?: string
): Promise<{ message: string }> {
if (!providerId) {
throw new Error('provider_id is required to delete a model');
}
await this.deleteProviderModel(parseInt(providerId), modelId);
const providerIdNum = providerId ? parseInt(providerId) : null;
await apiClient.post('/admin/api/models/delete', {
model_id: modelId,
provider_id: providerIdNum,
});
return { message: 'Model deleted successfully' };
}
@@ -531,15 +569,17 @@ export class AdminService {
modelId: string,
providerId?: string
): Promise<{ message: string }> {
if (!providerId) {
throw new Error('provider_id is required to soft delete a model');
}
const providerIdNum = parseInt(providerId);
const model = await this.getProviderModel(providerIdNum, modelId);
await this.updateProviderModel(providerIdNum, modelId, {
const providerIdNum = providerId ? parseInt(providerId) : null;
const model = await this.getModel(modelId, providerIdNum);
await apiClient.post('/admin/api/models/update', {
model_id: modelId,
provider_id: providerIdNum,
...model,
enabled: false,
pricing: this.convertPricingToPerToken(model.pricing),
});
return { message: 'Model soft deleted successfully' };
}
@@ -693,4 +733,33 @@ export class AdminService {
pricing: this.convertPricingToPerMillionTokens(m.pricing),
}));
}
static async getSettings(): Promise<Record<string, unknown>> {
return await apiClient.get<Record<string, unknown>>('/admin/api/settings');
}
static async updateSettings(
settings: Record<string, unknown>
): Promise<Record<string, unknown>> {
return await apiClient.patch<Record<string, unknown>>(
'/admin/api/settings',
settings
);
}
static async login(password: string): Promise<{
ok: boolean;
token: string;
expires_in: number;
}> {
return await apiClient.post<{
ok: boolean;
token: string;
expires_in: number;
}>('/admin/api/login', { password });
}
static async logout(): Promise<{ ok: boolean }> {
return await apiClient.post<{ ok: boolean }>('/admin/api/logout', {});
}
}