add basic ui for private inference resource

This commit is contained in:
miloschwartz
2026-08-04 16:54:23 -04:00
parent c085de1e9e
commit 7759d87835
19 changed files with 828 additions and 208 deletions
@@ -69,6 +69,10 @@ export default async function AiProviderLayout({ children, params }: Props) {
title: t("aiProviderNetworkSettings"),
href: "/{orgId}/settings/ai-providers/{providerId}/network"
},
{
title: t("aiProviderModels"),
href: "/{orgId}/settings/ai-providers/{providerId}/models"
},
{
title: t("aiProviderAuthSettings"),
href: "/{orgId}/settings/ai-providers/{providerId}/authentication"
@@ -0,0 +1,150 @@
"use client";
import {
SettingsContainer,
SettingsSection,
SettingsSectionBody,
SettingsSectionDescription,
SettingsSectionFooter,
SettingsSectionForm,
SettingsSectionHeader,
SettingsSectionTitle
} from "@app/components/Settings";
import { TagInput, type Tag } from "@app/components/tags/tag-input";
import { Button } from "@app/components/ui/button";
import { useAiProviderContext } from "@app/hooks/useAiProviderContext";
import { useEnvContext } from "@app/hooks/useEnvContext";
import { toast } from "@app/hooks/useToast";
import { createApiClient, formatAxiosError } from "@app/lib/api";
import { aiProviderQueries } from "@app/lib/queries";
import { useQuery, useQueryClient } from "@tanstack/react-query";
import { useTranslations } from "next-intl";
import { useEffect, useState } from "react";
export default function AiProviderModelsPage() {
const { provider } = useAiProviderContext();
const { env } = useEnvContext();
const api = createApiClient({ env });
const queryClient = useQueryClient();
const t = useTranslations();
const [saveLoading, setSaveLoading] = useState(false);
const [tags, setTags] = useState<Tag[]>([]);
const [activeTagIndex, setActiveTagIndex] = useState<number | null>(null);
const modelsQuery = useQuery(
aiProviderQueries.providerModels({ providerId: provider.providerId })
);
useEffect(() => {
if (!modelsQuery.data) return;
setTags(
modelsQuery.data.map((model) => ({
id: String(model.modelId),
text: model.modelKey
}))
);
}, [modelsQuery.data]);
async function onSave() {
setSaveLoading(true);
try {
const existing = modelsQuery.data ?? [];
const existingByKey = new Map(
existing.map((model) => [model.modelKey, model])
);
const nextKeys = new Set(
tags.map((tag) => tag.text.trim()).filter(Boolean)
);
const toCreate = [...nextKeys].filter(
(key) => !existingByKey.has(key)
);
const toDelete = existing.filter(
(model) => !nextKeys.has(model.modelKey)
);
await Promise.all([
...toCreate.map((modelKey) =>
api.put(`/ai-provider/${provider.providerId}/model`, {
modelKey,
name: modelKey
})
),
...toDelete.map((model) =>
api.delete(`/ai-model/${model.modelId}`)
)
]);
await queryClient.invalidateQueries(
aiProviderQueries.providerModels({
providerId: provider.providerId
})
);
toast({
title: t("success"),
description: t("aiProviderModelsUpdated")
});
} catch (e) {
toast({
variant: "destructive",
title: t("aiProviderModelsErrorUpdate"),
description: formatAxiosError(
e,
t("aiProviderModelsErrorUpdate")
)
});
} finally {
setSaveLoading(false);
}
}
return (
<SettingsContainer>
<SettingsSection>
<SettingsSectionHeader>
<SettingsSectionTitle>
{t("aiProviderModels")}
</SettingsSectionTitle>
<SettingsSectionDescription>
{t("aiProviderModelsDescription")}
</SettingsSectionDescription>
</SettingsSectionHeader>
<SettingsSectionBody>
<SettingsSectionForm variant="half">
<TagInput
activeTagIndex={activeTagIndex}
setActiveTagIndex={setActiveTagIndex}
placeholder={t("aiProviderModelsPlaceholder")}
size="sm"
tags={tags}
setTags={(newTags) => {
const next =
typeof newTags === "function"
? newTags(tags)
: newTags;
setTags(next as Tag[]);
}}
allowDuplicates={false}
sortTags
delimiterList={[",", "Enter"]}
disabled={modelsQuery.isLoading || saveLoading}
/>
</SettingsSectionForm>
</SettingsSectionBody>
<SettingsSectionFooter>
<Button
type="button"
loading={saveLoading}
disabled={saveLoading || modelsQuery.isLoading}
onClick={onSave}
>
{t("saveSettings")}
</Button>
</SettingsSectionFooter>
</SettingsSection>
</SettingsContainer>
);
}
@@ -24,7 +24,9 @@ import {
} from "@app/components/ui/form";
import { Input } from "@app/components/ui/input";
import { SwitchInput } from "@app/components/SwitchInput";
import { PrivateResourceAliasField } from "@app/components/PrivateResourceDestinationFields";
import { createGeneralFormSchema } from "@app/lib/privateResourceForm";
import { asAnyControl, asAnyWatch } from "@app/lib/formControlUtils";
import { zodResolver } from "@hookform/resolvers/zod";
import { useTranslations } from "next-intl";
import { useActionState, useMemo } from "react";
@@ -35,8 +37,12 @@ import { useSaveSiteResource } from "@app/hooks/useSaveSiteResource";
export default function PrivateResourceGeneralPage() {
const t = useTranslations();
const { save, siteResource } = useSaveSiteResource();
const isInference = siteResource.mode === "inference";
const formSchema = useMemo(() => createGeneralFormSchema(t), [t]);
const formSchema = useMemo(
() => createGeneralFormSchema(t, { requireAlias: isInference }),
[t, isInference]
);
type FormValues = z.infer<typeof formSchema>;
const form = useForm<FormValues>({
@@ -44,7 +50,8 @@ export default function PrivateResourceGeneralPage() {
defaultValues: {
name: siteResource.name,
niceId: siteResource.niceId,
enabled: siteResource.enabled
enabled: siteResource.enabled,
alias: siteResource.alias ?? null
}
});
@@ -56,7 +63,13 @@ export default function PrivateResourceGeneralPage() {
await save({
name: data.name,
niceId: data.niceId,
enabled: data.enabled
enabled: data.enabled,
...(isInference
? {
mode: "inference" as const,
alias: data.alias
}
: {})
});
}, null);
@@ -152,6 +165,17 @@ export default function PrivateResourceGeneralPage() {
)}
/>
</SettingsFormCell>
{isInference && (
<SettingsFormCell span="half">
<PrivateResourceAliasField
control={asAnyControl(
form.control
)}
watch={asAnyWatch(form.watch)}
labelPrefix="edit"
/>
</SettingsFormCell>
)}
</SettingsFormGrid>
</form>
</Form>
@@ -1,105 +1,15 @@
"use client";
import type { Metadata } from "next";
import { redirect } from "next/navigation";
import {
SettingsContainer,
SettingsFormCell,
SettingsFormGrid,
SettingsSection,
SettingsSectionBody,
SettingsSectionDescription,
SettingsSectionFooter,
SettingsSectionForm,
SettingsSectionHeader,
SettingsSectionTitle
} from "@app/components/Settings";
import { Button } from "@app/components/ui/button";
import { Form } from "@app/components/ui/form";
import { createInferenceFormSchema } from "@app/lib/privateResourceForm";
import { zodResolver } from "@hookform/resolvers/zod";
import { useTranslations } from "next-intl";
import { useActionState, useMemo, useState } from "react";
import { useForm } from "react-hook-form";
import { z } from "zod";
import { PrivateResourceInferenceDestinationFields } from "@app/components/PrivateResourceDestinationFields";
import { useSaveSiteResource } from "@app/hooks/useSaveSiteResource";
import {
asAnyControl,
asAnySetValue,
asAnyWatch
} from "@app/lib/formControlUtils";
import { buildSelectedSitesForResource } from "@app/lib/privateResourceUtils";
export const metadata: Metadata = {
title: "Private Resource"
};
export default function PrivateResourceInferencePage() {
const t = useTranslations();
const { save, siteResource } = useSaveSiteResource();
const [selectedSites, setSelectedSites] = useState(() =>
buildSelectedSitesForResource(siteResource)
);
const formSchema = useMemo(() => createInferenceFormSchema(t), [t]);
type FormValues = z.infer<typeof formSchema>;
const form = useForm<FormValues>({
resolver: zodResolver(formSchema),
defaultValues: {
mode: "inference",
alias: siteResource.alias ?? null
}
});
const [, formAction, saveLoading] = useActionState(async () => {
const isValid = await form.trigger();
if (!isValid) return;
const data = form.getValues();
await save({
mode: "inference",
alias: data.alias
});
}, null);
return (
<SettingsContainer>
<SettingsSection>
<SettingsSectionHeader>
<SettingsSectionTitle>
{t("hostSettings")}
</SettingsSectionTitle>
<SettingsSectionDescription>
{t("editInternalResourceDialogDestinationDescription")}
</SettingsSectionDescription>
</SettingsSectionHeader>
<SettingsSectionBody>
<SettingsSectionForm variant="half">
<Form {...form}>
<form
action={formAction}
id="private-resource-host-form"
>
<SettingsFormGrid>
<SettingsFormCell span="full">
<PrivateResourceInferenceDestinationFields
control={asAnyControl(form.control)}
watch={asAnyWatch(form.watch)}
/>
</SettingsFormCell>
</SettingsFormGrid>
</form>
</Form>
</SettingsSectionForm>
</SettingsSectionBody>
<SettingsSectionFooter>
<Button
type="submit"
form="private-resource-host-form"
loading={saveLoading}
>
{t("saveSettings")}
</Button>
</SettingsSectionFooter>
</SettingsSection>
</SettingsContainer>
export default async function PrivateResourceInferencePage(props: {
params: Promise<{ niceId: string; orgId: string }>;
}) {
const params = await props.params;
redirect(
`/${params.orgId}/settings/resources/private/${params.niceId}/providers`
);
}
@@ -55,20 +55,37 @@ export default async function PrivateResourceLayout(
| "sshSettings"
| "inferenceSettings";
const navItems = [
{
title: t("general"),
href: `/{orgId}/settings/resources/private/{niceId}/general`
},
{
title: t(modeSettingsKey),
href: `/{orgId}/settings/resources/private/{niceId}/${siteResource.mode}`
},
{
title: t("authentication"),
href: `/{orgId}/settings/resources/private/{niceId}/access`
}
];
const isInference = siteResource.mode === "inference";
const navItems = isInference
? [
{
title: t("general"),
href: `/{orgId}/settings/resources/private/{niceId}/general`
},
{
title: t("aiResourceProviders"),
href: `/{orgId}/settings/resources/private/{niceId}/providers`
},
{
title: t("authentication"),
href: `/{orgId}/settings/resources/private/{niceId}/access`
}
]
: [
{
title: t("general"),
href: `/{orgId}/settings/resources/private/{niceId}/general`
},
{
title: t(modeSettingsKey),
href: `/{orgId}/settings/resources/private/{niceId}/${siteResource.mode}`
},
{
title: t("authentication"),
href: `/{orgId}/settings/resources/private/{niceId}/access`
}
];
return (
<>
@@ -0,0 +1,232 @@
"use client";
import {
SettingsContainer,
SettingsFormCell,
SettingsFormGrid,
SettingsSection,
SettingsSectionBody,
SettingsSectionDescription,
SettingsSectionFooter,
SettingsSectionForm,
SettingsSectionHeader,
SettingsSectionTitle
} from "@app/components/Settings";
import {
AiProvidersSelector,
type SelectedAiProvider
} from "@app/components/AiProvidersSelector";
import { Button } from "@app/components/ui/button";
import {
Form,
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
FormMessage
} from "@app/components/ui/form";
import { useEnvContext } from "@app/hooks/useEnvContext";
import { useSiteResourceContext } from "@app/hooks/useSiteResourceContext";
import { toast } from "@app/hooks/useToast";
import { createApiClient, formatAxiosError } from "@app/lib/api";
import { resourceQueries } from "@app/lib/queries";
import { zodResolver } from "@hookform/resolvers/zod";
import { useQuery, useQueryClient } from "@tanstack/react-query";
import { useTranslations } from "next-intl";
import { useRouter } from "next/navigation";
import { useActionState, useEffect, useMemo, useState } from "react";
import { useForm } from "react-hook-form";
import { z } from "zod";
export default function PrivateResourceProvidersPage() {
const t = useTranslations();
const router = useRouter();
const { env } = useEnvContext();
const api = createApiClient({ env });
const queryClient = useQueryClient();
const { siteResource } = useSiteResourceContext();
useEffect(() => {
if (siteResource.mode !== "inference") {
router.replace(
`/${siteResource.orgId}/settings/resources/private/${siteResource.niceId}/general`
);
}
}, [router, siteResource.mode, siteResource.niceId, siteResource.orgId]);
const formSchema = useMemo(
() =>
z.object({
providerIds: z
.array(z.number().int().positive())
.min(1, t("aiResourceProvidersRequired"))
}),
[t]
);
type FormValues = z.infer<typeof formSchema>;
const [selectedProviders, setSelectedProviders] = useState<
SelectedAiProvider[]
>([]);
const attachedQuery = useQuery({
...resourceQueries.siteResourceAiProviders({
siteResourceId: siteResource.id
}),
enabled: siteResource.mode === "inference"
});
const form = useForm<FormValues>({
resolver: zodResolver(formSchema),
defaultValues: {
providerIds: []
}
});
useEffect(() => {
if (!attachedQuery.data) return;
const providers = attachedQuery.data.map((provider) => ({
id: String(provider.providerId),
text: provider.name
}));
setSelectedProviders(providers);
form.reset({
providerIds: attachedQuery.data.map((p) => p.providerId)
});
}, [attachedQuery.data, form]);
const [, formAction, saveLoading] = useActionState(async () => {
const isValid = await form.trigger();
if (!isValid) return;
const data = form.getValues();
try {
await api.post(`/site-resource/${siteResource.id}/ai-providers`, {
providers: data.providerIds.map((providerId) => ({
providerId,
modelAccessMode: "catalog"
}))
});
await queryClient.invalidateQueries(
resourceQueries.siteResourceAiProviders({
siteResourceId: siteResource.id
})
);
toast({
title: t("success"),
description: t("aiResourceProvidersUpdated")
});
} catch (error) {
toast({
variant: "destructive",
title: t("aiResourceProvidersErrorUpdate"),
description: formatAxiosError(
error,
t("aiResourceProvidersErrorUpdate")
)
});
}
}, null);
if (siteResource.mode !== "inference") {
return null;
}
return (
<SettingsContainer>
<SettingsSection>
<SettingsSectionHeader>
<SettingsSectionTitle>
{t("aiResourceProviders")}
</SettingsSectionTitle>
<SettingsSectionDescription>
{t("aiResourceProvidersDescription")}
</SettingsSectionDescription>
</SettingsSectionHeader>
<SettingsSectionBody>
<SettingsSectionForm variant="half">
<Form {...form}>
<form
action={formAction}
id="private-resource-providers-form"
>
<SettingsFormGrid>
<SettingsFormCell span="full">
<FormField
control={form.control}
name="providerIds"
render={() => (
<FormItem>
<FormLabel>
{t(
"aiResourceProviders"
)}
</FormLabel>
<FormControl>
<AiProvidersSelector
orgId={
siteResource.orgId
}
selectedProviders={
selectedProviders
}
disabled={
attachedQuery.isLoading ||
saveLoading
}
onSelectProviders={(
providers
) => {
setSelectedProviders(
providers
);
form.setValue(
"providerIds",
providers.map(
(p) =>
parseInt(
p.id,
10
)
),
{
shouldValidate: true
}
);
}}
/>
</FormControl>
<FormDescription>
{t(
"aiResourceProvidersHelp"
)}
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
</SettingsFormCell>
</SettingsFormGrid>
</form>
</Form>
</SettingsSectionForm>
</SettingsSectionBody>
<SettingsSectionFooter>
<Button
type="submit"
form="private-resource-providers-form"
loading={saveLoading}
disabled={attachedQuery.isLoading}
>
{t("saveSettings")}
</Button>
</SettingsSectionFooter>
</SettingsSection>
</SettingsContainer>
);
}
@@ -64,6 +64,10 @@ import {
asAnySetValue,
asAnyWatch
} from "@app/lib/formControlUtils";
import {
AiProvidersSelector,
type SelectedAiProvider
} from "@app/components/AiProvidersSelector";
export default function CreatePrivateResourcePage() {
const params = useParams();
@@ -88,6 +92,9 @@ export default function CreatePrivateResourcePage() {
: null;
const [selectedSites, setSelectedSites] = useState<Selectedsite[]>([]);
const [selectedProviders, setSelectedProviders] = useState<
SelectedAiProvider[]
>([]);
const formSchema = useMemo(() => createCreateFormSchema(t), [t]);
type FormValues = z.infer<typeof formSchema>;
@@ -112,7 +119,8 @@ export default function CreatePrivateResourcePage() {
pamMode: "passthrough",
tcpPortRangeString: "*",
udpPortRangeString: "*",
disableIcmp: false
disableIcmp: false,
providerIds: []
}
});
@@ -196,7 +204,9 @@ export default function CreatePrivateResourcePage() {
}
router.push(
`/${orgId}/settings/resources/private/${created.niceId}/${created.mode}`
created.mode === "inference"
? `/${orgId}/settings/resources/private/${created.niceId}/general`
: `/${orgId}/settings/resources/private/${created.niceId}/${created.mode}`
);
} catch (error) {
toast({
@@ -336,6 +346,13 @@ export default function CreatePrivateResourcePage() {
"destinationPort",
null
);
form.setValue(
"providerIds",
[]
);
setSelectedProviders(
[]
);
} else {
form.setValue(
"destinationPort",
@@ -637,6 +654,76 @@ export default function CreatePrivateResourcePage() {
</SettingsSection>
)}
{mode === "inference" && (
<SettingsSection>
<SettingsSectionHeader>
<SettingsSectionTitle>
{t("aiResourceProviders")}
</SettingsSectionTitle>
<SettingsSectionDescription>
{t("aiResourceProvidersDescription")}
</SettingsSectionDescription>
</SettingsSectionHeader>
<SettingsSectionBody>
<SettingsSectionForm variant="half">
<SettingsFormGrid>
<SettingsFormCell span="full">
<FormField
control={form.control}
name="providerIds"
render={() => (
<FormItem>
<FormLabel>
{t(
"aiResourceProviders"
)}
</FormLabel>
<FormControl>
<AiProvidersSelector
orgId={orgId}
selectedProviders={
selectedProviders
}
onSelectProviders={(
providers
) => {
setSelectedProviders(
providers
);
form.setValue(
"providerIds",
providers.map(
(
p
) =>
parseInt(
p.id,
10
)
),
{
shouldValidate: true
}
);
}}
/>
</FormControl>
<FormDescription>
{t(
"aiResourceProvidersHelp"
)}
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
</SettingsFormCell>
</SettingsFormGrid>
</SettingsSectionForm>
</SettingsSectionBody>
</SettingsSection>
)}
<div className="flex justify-end space-x-2 mt-8">
<Button
type="button"
+61
View File
@@ -0,0 +1,61 @@
"use client";
import { aiProviderQueries } from "@app/lib/queries";
import { MultiSelectTagInput } from "@app/components/multi-select/multi-select-tag-input";
import { useQuery } from "@tanstack/react-query";
import { useTranslations } from "next-intl";
import { useState } from "react";
import { useDebounce } from "use-debounce";
export type SelectedAiProvider = {
id: string;
text: string;
};
export type AiProvidersSelectorProps = {
orgId: string;
selectedProviders?: SelectedAiProvider[];
onSelectProviders: (providers: SelectedAiProvider[]) => void;
disabled?: boolean;
buttonText?: string;
};
export function AiProvidersSelector({
orgId,
selectedProviders = [],
onSelectProviders,
disabled,
buttonText
}: AiProvidersSelectorProps) {
const t = useTranslations();
const [searchQuery, setSearchQuery] = useState("");
const [debouncedValue] = useDebounce(searchQuery, 150);
const { data: providers = [] } = useQuery(
aiProviderQueries.orgProviders({
orgId,
query: debouncedValue || undefined
})
);
const options: SelectedAiProvider[] = providers
.filter((provider) => provider.enabled)
.map((provider) => ({
id: String(provider.providerId),
text: provider.name
}));
return (
<MultiSelectTagInput
buttonText={buttonText ?? t("aiResourceProvidersSelect")}
emptyPlaceholder={t("aiResourceProvidersEmpty")}
searchPlaceholder={t("aiProvidersSearch")}
searchQuery={searchQuery}
options={options}
value={selectedProviders}
onChange={onSelectProviders}
onSearch={setSearchQuery}
disabled={disabled}
/>
);
}
+72 -20
View File
@@ -42,6 +42,7 @@ export type PrivateResourceFormValues = {
roles?: PrivateResourceAccessTag[];
users?: PrivateResourceAccessTag[];
clients?: PrivateResourceClient[];
providerIds?: number[];
};
export type SiteResourceAccess = {
@@ -220,7 +221,11 @@ export function buildCreateSiteResourcePayload(
typeof data.alias === "string" &&
data.alias.trim()
? data.alias
: undefined
: undefined,
aiProviders: (data.providerIds ?? []).map((providerId) => ({
providerId,
modelAccessMode: "catalog" as const
}))
}),
...((data.mode === "host" || data.mode === "cidr") && {
tcpPortRangeString: data.tcpPortRangeString,
@@ -353,19 +358,33 @@ export function siteResourceToFormValues(
};
}
export function createGeneralFormSchema(t: TranslateFn) {
return z.object({
name: z
.string()
.min(1, t("editInternalResourceDialogNameRequired"))
.max(255, t("editInternalResourceDialogNameMaxLength")),
niceId: z
.string()
.min(1)
.max(255)
.regex(/^[a-zA-Z0-9-]+$/),
enabled: z.boolean()
});
export function createGeneralFormSchema(
t: TranslateFn,
options?: { requireAlias?: boolean }
) {
return z
.object({
name: z
.string()
.min(1, t("editInternalResourceDialogNameRequired"))
.max(255, t("editInternalResourceDialogNameMaxLength")),
niceId: z
.string()
.min(1)
.max(255)
.regex(/^[a-zA-Z0-9-]+$/),
enabled: z.boolean(),
alias: z.string().nullish()
})
.superRefine((data, ctx) => {
if (options?.requireAlias && !data.alias?.trim()) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: t("aiResourceAliasRequired"),
path: ["alias"]
});
}
});
}
export function createAccessFormSchema() {
@@ -422,7 +441,8 @@ export function createCreateFormSchema(t: TranslateFn) {
pamMode: z.enum(["passthrough", "push"]).optional().nullable(),
tcpPortRangeString: createPortRangeStringSchema(t),
udpPortRangeString: createPortRangeStringSchema(t),
disableIcmp: z.boolean().optional()
disableIcmp: z.boolean().optional(),
providerIds: z.array(z.number().int().positive()).optional()
})
.superRefine((data, ctx) => {
const isNativeSsh =
@@ -434,12 +454,27 @@ export function createCreateFormSchema(t: TranslateFn) {
) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: t(
"createInternalResourceDialogPleaseSelectSite"
),
message: t("createInternalResourceDialogPleaseSelectSite"),
path: ["siteIds"]
});
}
if (data.mode === "inference") {
const trimmedAlias = data.alias?.trim();
if (!trimmedAlias) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: t("aiResourceAliasRequired"),
path: ["alias"]
});
}
if (!data.providerIds || data.providerIds.length < 1) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: t("aiResourceProvidersRequired"),
path: ["providerIds"]
});
}
}
if (
data.mode !== "ssh" &&
data.mode !== "inference" &&
@@ -592,9 +627,26 @@ export function createInferenceFormSchema(t: TranslateFn) {
return z
.object({
mode: z.literal("inference"),
alias: z.string().nullish()
alias: z.string().nullish(),
providerIds: z.array(z.number().int().positive()).optional()
})
.superRefine((data, ctx) => destinationRefine(data, ctx, t));
.superRefine((data, ctx) => {
const trimmedAlias = data.alias?.trim();
if (!trimmedAlias) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: t("aiResourceAliasRequired"),
path: ["alias"]
});
}
if (!data.providerIds || data.providerIds.length < 1) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: t("aiResourceProvidersRequired"),
path: ["providerIds"]
});
}
});
}
export function createCidrFormSchema(t: TranslateFn) {
+58
View File
@@ -57,6 +57,10 @@ import type {
} from "@server/routers/siteResource";
import type { GetSiteResourceResponse } from "@server/routers/siteResource/getSiteResource";
import type { ListTargetsResponse } from "@server/routers/target";
import type {
ListAiModelsResponse,
ListAiProvidersResponse
} from "@server/routers/aiProvider/types";
import type { ListUsersResponse } from "@server/routers/user";
import type ResponseT from "@server/types/Response";
import {
@@ -1174,6 +1178,36 @@ export const aiProviderQueries = {
return res.data.data.targets;
}
}),
providerModels: ({ providerId }: { providerId: number }) =>
queryOptions({
queryKey: ["AI_PROVIDERS", providerId, "MODELS"] as const,
queryFn: async ({ signal, meta }) => {
const res = await meta!.api.get<
AxiosResponse<ListAiModelsResponse>
>(`/ai-provider/${providerId}/models`, {
params: { page: 1, pageSize: 1000 },
signal
});
return res.data.data.models;
}
}),
orgProviders: ({ orgId, query }: { orgId: string; query?: string }) =>
queryOptions({
queryKey: ["AI_PROVIDERS", orgId, "LIST", query ?? ""] as const,
queryFn: async ({ signal, meta }) => {
const res = await meta!.api.get<
AxiosResponse<ListAiProvidersResponse>
>(`/org/${orgId}/ai-providers`, {
params: {
page: 1,
pageSize: 100,
...(query ? { query } : {})
},
signal
});
return res.data.data.providers;
}
})
};
@@ -1242,6 +1276,30 @@ export const resourceQueries = {
return res.data.data.clients;
}
}),
siteResourceAiProviders: ({ siteResourceId }: { siteResourceId: number }) =>
queryOptions({
queryKey: [
"SITE_RESOURCES",
siteResourceId,
"AI_PROVIDERS"
] as const,
queryFn: async ({ signal, meta }) => {
const res = await meta!.api.get<
AxiosResponse<{
providers: Array<{
providerId: number;
modelAccessMode: "catalog" | "allowlist";
name: string;
type: string;
enabled: boolean;
}>;
}>
>(`/site-resource/${siteResourceId}/ai-providers`, {
signal
});
return res.data.data.providers;
}
}),
resourceTargets: ({ resourceId }: { resourceId: number }) =>
queryOptions({
queryKey: ["RESOURCES", resourceId, "TARGETS"] as const,