Skip to content

Commit

Permalink
feat(frontend): support llm management (#187)
Browse files Browse the repository at this point in the history
Co-authored-by: ianthereal <[email protected]>
  • Loading branch information
634750802 and IANTHEREAL authored Jul 24, 2024
1 parent 87bb970 commit c0d7d05
Show file tree
Hide file tree
Showing 6 changed files with 579 additions and 1 deletion.
116 changes: 116 additions & 0 deletions frontend/app/src/api/llms.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
import { BASE_URL, buildUrlParams, handleErrors, handleResponse, opaqueCookieHeader, type Page, type PageParams, zodPage } from '@/lib/request';
import { zodJsonDate } from '@/lib/zod';
import { z, type ZodType, type ZodTypeDef } from 'zod';

export interface LLM {
id: number;
name: string;
provider: string;
model: string;
config?: any;
is_default: boolean;
created_at: Date | null;
updated_at: Date | null;
}

export interface LlmOption {
provider: string;
default_model: string;
model_description: string;
credentials_display_name: string;
credentials_description: string;
credentials_type: 'str' | 'dict';
default_credentials: any;
}

export interface CreateLLM {
name: string;
provider: string;
model: string;
config?: any;
is_default?: boolean;
credentials: string | object;
}

const llmSchema = z.object({
id: z.number(),
name: z.string(),
provider: z.string(),
model: z.string(),
config: z.any(),
is_default: z.boolean(),
created_at: zodJsonDate().nullable(),
updated_at: zodJsonDate().nullable(),
}) satisfies ZodType<LLM, ZodTypeDef, any>;

const llmOptionSchema = z.object({
provider: z.string(),
default_model: z.string(),
model_description: z.string(),
credentials_display_name: z.string(),
credentials_description: z.string(),
}).and(z.discriminatedUnion('credentials_type', [
z.object({
credentials_type: z.literal('str'),
default_credentials: z.string(),
}),
z.object({
credentials_type: z.literal('dict'),
default_credentials: z.object({}).passthrough(),
}),
])) satisfies ZodType<LlmOption>;

export async function listLlmOptions () {
return await fetch(`${BASE_URL}/api/v1/admin/llm-options`, {
headers: {
...await opaqueCookieHeader(),
},
})
.then(handleResponse(llmOptionSchema.array()));
}

export async function listLlms ({ page = 1, size = 10 }: PageParams = {}): Promise<Page<LLM>> {
return await fetch(BASE_URL + '/api/v1/admin/llms' + '?' + buildUrlParams({ page, size }), {
headers: await opaqueCookieHeader(),
})
.then(handleResponse(zodPage(llmSchema)));
}

export async function getLlm (id: number): Promise<LLM> {
return await fetch(BASE_URL + `/api/v1/admin/llms/${id}`, {
headers: await opaqueCookieHeader(),
}).then(handleResponse(llmSchema));
}

export async function createLlm (create: CreateLLM) {
return await fetch(BASE_URL + `/api/v1/admin/llms`, {
method: 'POST',
body: JSON.stringify(create),
headers: {
'Content-Type': 'application/json',
...await opaqueCookieHeader(),
},
}).then(handleResponse(llmSchema));
}

export async function deleteLlm (id: number) {
await fetch(BASE_URL + `/api/v1/admin/llms/${id}`, {
method: 'DELETE',
headers: await opaqueCookieHeader(),
}).then(handleErrors);
}

export async function testLlm (createLLM: CreateLLM) {
return await fetch(`${BASE_URL}/api/v1/admin/llms/test`, {
method: 'POST',
body: JSON.stringify(createLLM),
headers: {
'Content-Type': 'application/json',
...await opaqueCookieHeader(),
},
})
.then(handleResponse(z.object({
success: z.boolean(),
error: z.string().optional(),
})));
}
53 changes: 53 additions & 0 deletions frontend/app/src/app/(main)/(admin)/llms/[id]/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
'use client';

import { deleteLlm, getLlm } from '@/api/llms';
import { AdminPageHeading } from '@/components/admin-page-heading';
import { DangerousActionButton } from '@/components/dangerous-action-button';
import { DateFormat } from '@/components/date-format';
import { OptionDetail } from '@/components/option-detail';
import { Loader2Icon } from 'lucide-react';
import { useRouter } from 'next/navigation';
import { useTransition } from 'react';
import useSWR from 'swr';

export default function Page ({ params }: { params: { id: string } }) {
const router = useRouter();
const { data } = useSWR(`api.llms.get?id=${params.id}`, () => getLlm(parseInt(params.id)));
const [transitioning, startTransition] = useTransition();

return (
<>
<AdminPageHeading
breadcrumbs={[
{ title: 'LLMs', url: '/llms' },
{ title: data ? data.name : <Loader2Icon className="size-4 animate-spin repeat-infinite" /> },
]}
/>
<div className="max-w-screen-sm space-y-4">
<div className="space-y-2 text-sm rounded p-4 border">
<OptionDetail title="ID" value={data?.id} />
<OptionDetail title="Name" value={data?.name} />
<OptionDetail title="Provider" value={data?.provider} />
<OptionDetail title="Model" value={data?.model} />
<OptionDetail title="Is Default" value={data?.is_default ? 'Yes' : 'No'} valueClassName={data?.is_default ? 'text-green-500' : 'text-muted-foreground'} />
<OptionDetail title="Created At" value={<DateFormat date={data?.created_at} />} />
<OptionDetail title="Updated At" value={<DateFormat date={data?.updated_at} />} />
</div>
<div>
<DangerousActionButton
variant="destructive"
disabled={transitioning}
action={async () => {
await deleteLlm(parseInt(params.id));
startTransition(() => {
router.push('/llms');
});
}}
>
Delete
</DangerousActionButton>
</div>
</div>
</>
);
}
Loading

0 comments on commit c0d7d05

Please sign in to comment.