mirror of
https://github.com/fosrl/pangolin.git
synced 2026-08-09 05:58:17 +02:00
support allow list and blocklist
This commit is contained in:
@@ -12,6 +12,7 @@ import {
|
||||
} from "@app/components/Settings";
|
||||
import { TagInput, type Tag } from "@app/components/tags/tag-input";
|
||||
import { Button } from "@app/components/ui/button";
|
||||
import { Label } from "@app/components/ui/label";
|
||||
import { useAiProviderContext } from "@app/hooks/useAiProviderContext";
|
||||
import { useEnvContext } from "@app/hooks/useEnvContext";
|
||||
import { toast } from "@app/hooks/useToast";
|
||||
@@ -21,6 +22,8 @@ import { useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
type ModelListType = "allow" | "block";
|
||||
|
||||
export default function AiProviderModelsPage() {
|
||||
const { provider } = useAiProviderContext();
|
||||
const { env } = useEnvContext();
|
||||
@@ -28,8 +31,14 @@ export default function AiProviderModelsPage() {
|
||||
const queryClient = useQueryClient();
|
||||
const t = useTranslations();
|
||||
const [saveLoading, setSaveLoading] = useState(false);
|
||||
const [tags, setTags] = useState<Tag[]>([]);
|
||||
const [activeTagIndex, setActiveTagIndex] = useState<number | null>(null);
|
||||
const [allowTags, setAllowTags] = useState<Tag[]>([]);
|
||||
const [blockTags, setBlockTags] = useState<Tag[]>([]);
|
||||
const [activeAllowTagIndex, setActiveAllowTagIndex] = useState<
|
||||
number | null
|
||||
>(null);
|
||||
const [activeBlockTagIndex, setActiveBlockTagIndex] = useState<
|
||||
number | null
|
||||
>(null);
|
||||
|
||||
const modelsQuery = useQuery(
|
||||
aiProviderQueries.providerModels({ providerId: provider.providerId })
|
||||
@@ -37,11 +46,21 @@ export default function AiProviderModelsPage() {
|
||||
|
||||
useEffect(() => {
|
||||
if (!modelsQuery.data) return;
|
||||
setTags(
|
||||
modelsQuery.data.map((model) => ({
|
||||
id: String(model.modelId),
|
||||
text: model.modelKey
|
||||
}))
|
||||
setAllowTags(
|
||||
modelsQuery.data
|
||||
.filter((model) => (model.listType ?? "allow") === "allow")
|
||||
.map((model) => ({
|
||||
id: String(model.modelId),
|
||||
text: model.modelKey
|
||||
}))
|
||||
);
|
||||
setBlockTags(
|
||||
modelsQuery.data
|
||||
.filter((model) => model.listType === "block")
|
||||
.map((model) => ({
|
||||
id: String(model.modelId),
|
||||
text: model.modelKey
|
||||
}))
|
||||
);
|
||||
}, [modelsQuery.data]);
|
||||
|
||||
@@ -52,27 +71,74 @@ export default function AiProviderModelsPage() {
|
||||
const existingByKey = new Map(
|
||||
existing.map((model) => [model.modelKey, model])
|
||||
);
|
||||
const nextKeys = new Set(
|
||||
tags.map((tag) => tag.text.trim()).filter(Boolean)
|
||||
|
||||
const nextAllow = new Set(
|
||||
allowTags.map((tag) => tag.text.trim()).filter(Boolean)
|
||||
);
|
||||
const nextBlock = new Set(
|
||||
blockTags.map((tag) => tag.text.trim()).filter(Boolean)
|
||||
);
|
||||
|
||||
const toCreate = [...nextKeys].filter(
|
||||
(key) => !existingByKey.has(key)
|
||||
);
|
||||
const toDelete = existing.filter(
|
||||
(model) => !nextKeys.has(model.modelKey)
|
||||
);
|
||||
const overlap = [...nextAllow].filter((key) => nextBlock.has(key));
|
||||
if (overlap.length > 0) {
|
||||
toast({
|
||||
variant: "destructive",
|
||||
title: t("aiProviderModelsErrorUpdate"),
|
||||
description: t("aiProviderModelsOverlapError", {
|
||||
keys: overlap.join(", ")
|
||||
})
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const desired = new Map<string, ModelListType>();
|
||||
for (const key of nextAllow) {
|
||||
desired.set(key, "allow");
|
||||
}
|
||||
for (const key of nextBlock) {
|
||||
desired.set(key, "block");
|
||||
}
|
||||
|
||||
const toCreate: { modelKey: string; listType: ModelListType }[] =
|
||||
[];
|
||||
const toUpdate: {
|
||||
modelId: number;
|
||||
listType: ModelListType;
|
||||
}[] = [];
|
||||
const toDelete: number[] = [];
|
||||
|
||||
for (const [modelKey, listType] of desired) {
|
||||
const existingModel = existingByKey.get(modelKey);
|
||||
if (!existingModel) {
|
||||
toCreate.push({ modelKey, listType });
|
||||
continue;
|
||||
}
|
||||
if ((existingModel.listType ?? "allow") !== listType) {
|
||||
toUpdate.push({
|
||||
modelId: existingModel.modelId,
|
||||
listType
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
for (const model of existing) {
|
||||
if (!desired.has(model.modelKey)) {
|
||||
toDelete.push(model.modelId);
|
||||
}
|
||||
}
|
||||
|
||||
await Promise.all([
|
||||
...toCreate.map((modelKey) =>
|
||||
...toCreate.map(({ modelKey, listType }) =>
|
||||
api.put(`/ai-provider/${provider.providerId}/model`, {
|
||||
modelKey,
|
||||
name: modelKey
|
||||
name: modelKey,
|
||||
listType
|
||||
})
|
||||
),
|
||||
...toDelete.map((model) =>
|
||||
api.delete(`/ai-model/${model.modelId}`)
|
||||
)
|
||||
...toUpdate.map(({ modelId, listType }) =>
|
||||
api.post(`/ai-model/${modelId}`, { listType })
|
||||
),
|
||||
...toDelete.map((modelId) => api.delete(`/ai-model/${modelId}`))
|
||||
]);
|
||||
|
||||
await queryClient.invalidateQueries(
|
||||
@@ -113,24 +179,59 @@ export default function AiProviderModelsPage() {
|
||||
|
||||
<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}
|
||||
/>
|
||||
<div className="space-y-2">
|
||||
<Label>{t("aiProviderModelsAllow")}</Label>
|
||||
<TagInput
|
||||
activeTagIndex={activeAllowTagIndex}
|
||||
setActiveTagIndex={setActiveAllowTagIndex}
|
||||
placeholder={t(
|
||||
"aiProviderModelsAllowPlaceholder"
|
||||
)}
|
||||
size="sm"
|
||||
tags={allowTags}
|
||||
setTags={(newTags) => {
|
||||
const next =
|
||||
typeof newTags === "function"
|
||||
? newTags(allowTags)
|
||||
: newTags;
|
||||
setAllowTags(next as Tag[]);
|
||||
}}
|
||||
allowDuplicates={false}
|
||||
sortTags
|
||||
delimiterList={[",", "Enter"]}
|
||||
disabled={modelsQuery.isLoading || saveLoading}
|
||||
/>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{t("aiProviderModelsAllowDescription")}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label>{t("aiProviderModelsBlock")}</Label>
|
||||
<TagInput
|
||||
activeTagIndex={activeBlockTagIndex}
|
||||
setActiveTagIndex={setActiveBlockTagIndex}
|
||||
placeholder={t(
|
||||
"aiProviderModelsBlockPlaceholder"
|
||||
)}
|
||||
size="sm"
|
||||
tags={blockTags}
|
||||
setTags={(newTags) => {
|
||||
const next =
|
||||
typeof newTags === "function"
|
||||
? newTags(blockTags)
|
||||
: newTags;
|
||||
setBlockTags(next as Tag[]);
|
||||
}}
|
||||
allowDuplicates={false}
|
||||
sortTags
|
||||
delimiterList={[",", "Enter"]}
|
||||
disabled={modelsQuery.isLoading || saveLoading}
|
||||
/>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{t("aiProviderModelsBlockDescription")}
|
||||
</p>
|
||||
</div>
|
||||
</SettingsSectionForm>
|
||||
</SettingsSectionBody>
|
||||
|
||||
|
||||
@@ -130,8 +130,7 @@ export default function PrivateResourceInferencePage() {
|
||||
|
||||
await api.post(`/site-resource/${siteResource.id}/ai-providers`, {
|
||||
providers: data.providerIds.map((providerId) => ({
|
||||
providerId,
|
||||
modelAccessMode: "catalog"
|
||||
providerId
|
||||
}))
|
||||
});
|
||||
|
||||
@@ -258,12 +257,10 @@ export default function PrivateResourceInferencePage() {
|
||||
cols={2}
|
||||
hideFreeDomain
|
||||
defaultSubdomain={
|
||||
httpConfigSubdomain ??
|
||||
undefined
|
||||
httpConfigSubdomain ?? undefined
|
||||
}
|
||||
defaultDomainId={
|
||||
httpConfigDomainId ??
|
||||
undefined
|
||||
httpConfigDomainId ?? undefined
|
||||
}
|
||||
defaultFullDomain={
|
||||
httpConfigFullDomain ??
|
||||
|
||||
@@ -102,8 +102,7 @@ export default function PublicResourceInferencePage() {
|
||||
try {
|
||||
await api.post(`/resource/${resource.resourceId}/ai-providers`, {
|
||||
providers: data.providerIds.map((providerId) => ({
|
||||
providerId,
|
||||
modelAccessMode: "catalog"
|
||||
providerId
|
||||
}))
|
||||
});
|
||||
|
||||
|
||||
@@ -497,8 +497,7 @@ export default function Page() {
|
||||
if (resourceType === "inference") {
|
||||
Object.assign(payload, {
|
||||
aiProviders: selectedProviders.map((provider) => ({
|
||||
providerId: parseInt(provider.id, 10),
|
||||
modelAccessMode: "catalog"
|
||||
providerId: parseInt(provider.id, 10)
|
||||
}))
|
||||
});
|
||||
} else if (resourceType === "ssh") {
|
||||
|
||||
@@ -217,8 +217,7 @@ export function buildCreateSiteResourcePayload(
|
||||
}),
|
||||
...(data.mode === "inference" && {
|
||||
aiProviders: (data.providerIds ?? []).map((providerId) => ({
|
||||
providerId,
|
||||
modelAccessMode: "catalog" as const
|
||||
providerId
|
||||
})),
|
||||
ssl: data.ssl ?? false,
|
||||
domainId: data.httpConfigDomainId
|
||||
|
||||
+2
-2
@@ -1288,10 +1288,10 @@ export const resourceQueries = {
|
||||
AxiosResponse<{
|
||||
providers: Array<{
|
||||
providerId: number;
|
||||
modelAccessMode: "catalog" | "allowlist";
|
||||
name: string;
|
||||
type: string;
|
||||
enabled: boolean;
|
||||
accessMode: "inherit" | "select";
|
||||
}>;
|
||||
}>
|
||||
>(`/site-resource/${siteResourceId}/ai-providers`, {
|
||||
@@ -1308,10 +1308,10 @@ export const resourceQueries = {
|
||||
AxiosResponse<{
|
||||
providers: Array<{
|
||||
providerId: number;
|
||||
modelAccessMode: "catalog" | "allowlist";
|
||||
name: string;
|
||||
type: string;
|
||||
enabled: boolean;
|
||||
accessMode: "inherit" | "select";
|
||||
}>;
|
||||
}>
|
||||
>(`/resource/${resourceId}/ai-providers`, {
|
||||
|
||||
Reference in New Issue
Block a user