support allow list and blocklist

This commit is contained in:
miloschwartz
2026-08-07 10:59:35 -04:00
parent 07f628b928
commit 9e7b4afaec
34 changed files with 891 additions and 470 deletions
@@ -9,11 +9,14 @@ import logger from "@server/logger";
import { fromError } from "zod-validation-error";
import { OpenAPITags, registry } from "@server/openApi";
import {
assertPublicAllowlistApiEligible,
assertModelsBelongToPublicAllowlistProviders
assertPublicModelListApiEligible,
assertPublicResourceModelEntriesValid,
modelListTypeSchema
} from "@server/lib/aiInferenceResource";
const addAiModelToResourceBodySchema = z.strictObject({
modelId: z.int().positive()
modelId: z.number().int().positive(),
listType: modelListTypeSchema.optional().default("allow")
});
const addAiModelToResourceParamsSchema = z.strictObject({
@@ -24,7 +27,7 @@ registry.registerPath({
method: "post",
path: "/resource/{resourceId}/ai-models/add",
description:
"Add a single catalog model to an inference resource allowlist. Requires at least one attached AI provider in allowlist mode. The model must belong to a provider attached in allowlist mode.",
"Add a single model to an inference resource allow/block selection. Requires at least one attached AI provider in select mode. The model must belong to a select-mode provider and its listType must match the provider catalog entry. listType defaults to allow.",
tags: [OpenAPITags.PublicResource],
request: {
params: addAiModelToResourceParamsSchema,
@@ -70,7 +73,7 @@ export async function addAiModelToResource(
);
}
const { modelId } = parsedBody.data;
const { modelId, listType } = parsedBody.data;
const parsedParams = addAiModelToResourceParamsSchema.safeParse(
req.params
@@ -98,15 +101,15 @@ export async function addAiModelToResource(
);
}
const eligibleError = await assertPublicAllowlistApiEligible(resource);
const eligibleError = await assertPublicModelListApiEligible(resource);
if (eligibleError) {
return next(createHttpError(HttpCode.BAD_REQUEST, eligibleError));
}
const modelError = await assertModelsBelongToPublicAllowlistProviders({
const modelError = await assertPublicResourceModelEntriesValid({
orgId: resource.orgId,
resourceId,
modelIds: [modelId]
models: [{ modelId, listType }]
});
if (modelError) {
return next(createHttpError(HttpCode.BAD_REQUEST, modelError));
@@ -131,7 +134,9 @@ export async function addAiModelToResource(
);
}
await db.insert(resourceAiModels).values({ resourceId, modelId });
await db
.insert(resourceAiModels)
.values({ resourceId, modelId, listType });
return response(res, {
data: {},
@@ -11,14 +11,12 @@ import { OpenAPITags, registry } from "@server/openApi";
import {
isInferenceFieldsError,
listPublicResourceAiProviders,
modelAccessModeSchema,
resolveProviderAttachments,
setPublicResourceAiProviders
} from "@server/lib/aiInferenceResource";
const addAiProviderToResourceBodySchema = z.strictObject({
providerId: z.number().int().positive(),
modelAccessMode: modelAccessModeSchema.optional()
providerId: z.number().int().positive()
});
const addAiProviderToResourceParamsSchema = z.strictObject({
@@ -29,7 +27,7 @@ registry.registerPath({
method: "post",
path: "/resource/{resourceId}/ai-providers/add",
description:
"Add or replace a single AI provider attachment on an inference resource.",
"Add or replace a single AI provider attachment on an inference resource. The provider is attached in inherit mode, using its own allow/block lists.",
tags: [OpenAPITags.PublicResource],
request: {
params: addAiProviderToResourceParamsSchema,
@@ -77,7 +75,7 @@ export async function addAiProviderToResource(
);
}
const { providerId, modelAccessMode } = parsedBody.data;
const { providerId } = parsedBody.data;
const parsedParams = addAiProviderToResourceParamsSchema.safeParse(
req.params
@@ -120,18 +118,21 @@ export async function addAiProviderToResource(
.filter((a) => a.providerId !== providerId)
.map((a) => ({
providerId: a.providerId,
modelAccessMode: a.modelAccessMode
accessMode: a.accessMode
})),
{ providerId, modelAccessMode }
{ providerId, accessMode: "inherit" as const }
];
const attachments = await resolveProviderAttachments({
orgId: resource.orgId,
attachments: nextAttachments,
requireAtLeastOne: true
requireAtLeastOne: true,
resourceId
});
if (isInferenceFieldsError(attachments)) {
return next(createHttpError(HttpCode.BAD_REQUEST, attachments.error));
return next(
createHttpError(HttpCode.BAD_REQUEST, attachments.error)
);
}
await setPublicResourceAiProviders(resourceId, attachments);
+7 -2
View File
@@ -109,7 +109,7 @@ const createHttpResourceSchema = z
.array(resourceAiProviderAttachmentSchema)
.optional()
.describe(
"For inference-mode resources: AI providers to attach. Each entry may set modelAccessMode (catalog or allowlist); defaults to catalog. Model keys must be unique across attached catalog providers."
"For inference-mode resources: AI providers to attach. Providers are attached in inherit mode, using each provider's own allow/block lists. Effective allow model keys must be unique across attached providers."
)
})
.refine(
@@ -391,9 +391,14 @@ async function createHttpResource(
let providerAttachments: ResourceAiProviderAttachment[] = [];
if (effectiveMode === "inference") {
// A new resource has no model selections yet, so providers always start
// in inherit mode; select can be enabled afterwards.
const resolved = await resolveProviderAttachments({
orgId,
attachments: aiProviderInputs ?? [],
attachments: (aiProviderInputs ?? []).map((p) => ({
providerId: p.providerId,
accessMode: "inherit" as const
})),
requireAtLeastOne: false
});
if (isInferenceFieldsError(resolved)) {
@@ -19,7 +19,9 @@ async function query(resourceId: number) {
modelId: aiModels.modelId,
modelKey: aiModels.modelKey,
name: aiModels.name,
enabled: aiModels.enabled
providerId: aiModels.providerId,
enabled: aiModels.enabled,
listType: resourceAiModels.listType
})
.from(resourceAiModels)
.innerJoin(aiModels, eq(resourceAiModels.modelId, aiModels.modelId))
@@ -34,7 +36,7 @@ registry.registerPath({
method: "get",
path: "/resource/{resourceId}/ai-models",
description:
"List catalog models on this resource's allowlist. Only enforced when modelAccessMode=allowlist; an empty allowlist denies all models.",
"List the models this resource has selected from its select-mode providers' allow/block lists. Providers in inherit mode are not represented here; they use their own lists.",
tags: [OpenAPITags.PublicResource],
request: {
params: listResourceAiModelsParamsSchema
@@ -21,7 +21,8 @@ export type ListResourceAiProvidersResponse = {
registry.registerPath({
method: "get",
path: "/resource/{resourceId}/ai-providers",
description: "List AI providers attached to an inference resource.",
description:
"List AI providers attached to an inference resource, including each attachment's accessMode.",
tags: [OpenAPITags.PublicResource],
request: {
params: listResourceAiProvidersParamsSchema
@@ -8,7 +8,7 @@ import createHttpError from "http-errors";
import logger from "@server/logger";
import { fromError } from "zod-validation-error";
import { OpenAPITags, registry } from "@server/openApi";
import { assertPublicAllowlistApiEligible } from "@server/lib/aiInferenceResource";
import { assertPublicModelListApiEligible } from "@server/lib/aiInferenceResource";
const removeAiModelFromResourceBodySchema = z.strictObject({
modelId: z.int().positive()
@@ -22,7 +22,7 @@ registry.registerPath({
method: "post",
path: "/resource/{resourceId}/ai-models/remove",
description:
"Remove a single catalog model from an inference resource allowlist. Requires at least one attached AI provider in allowlist mode.",
"Remove a single model from an inference resource allow/block list. Requires at least one attached AI provider.",
tags: [OpenAPITags.PublicResource],
request: {
params: removeAiModelFromResourceParamsSchema,
@@ -98,7 +98,7 @@ export async function removeAiModelFromResource(
);
}
const eligibleError = await assertPublicAllowlistApiEligible(resource);
const eligibleError = await assertPublicModelListApiEligible(resource);
if (eligibleError) {
return next(createHttpError(HttpCode.BAD_REQUEST, eligibleError));
}
@@ -127,13 +127,14 @@ export async function removeAiProviderFromResource(
.filter((a) => a.providerId !== providerId)
.map((a) => ({
providerId: a.providerId,
modelAccessMode: a.modelAccessMode
accessMode: a.accessMode
}));
const attachments = await resolveProviderAttachments({
orgId: resource.orgId,
attachments: remaining,
requireAtLeastOne: false
requireAtLeastOne: false,
resourceId
});
if (isInferenceFieldsError(attachments)) {
return next(
+24 -14
View File
@@ -9,12 +9,13 @@ import logger from "@server/logger";
import { fromError } from "zod-validation-error";
import { OpenAPITags, registry } from "@server/openApi";
import {
assertPublicAllowlistApiEligible,
assertModelsBelongToPublicAllowlistProviders
assertPublicModelListApiEligible,
assertPublicResourceModelEntriesValid,
resourceAiModelEntrySchema
} from "@server/lib/aiInferenceResource";
const setResourceAiModelsBodySchema = z.strictObject({
modelIds: z.array(z.int().positive())
models: z.array(resourceAiModelEntrySchema)
});
const setResourceAiModelsParamsSchema = z.strictObject({
@@ -25,7 +26,7 @@ registry.registerPath({
method: "post",
path: "/resource/{resourceId}/ai-models",
description:
"Replace the allowlist of catalog models for an inference resource. Requires at least one attached AI provider in allowlist mode. Models must belong to a provider attached in allowlist mode. An empty array denies all models.",
"Replace the allow/block model selection for an inference resource. Requires at least one attached AI provider in select mode. Models must belong to a select-mode provider and their listType must match the provider catalog entry. An empty array clears the selection, which denies all models for select-mode providers.",
tags: [OpenAPITags.PublicResource],
request: {
params: setResourceAiModelsParamsSchema,
@@ -71,7 +72,7 @@ export async function setResourceAiModels(
);
}
const { modelIds } = parsedBody.data;
const { models } = parsedBody.data;
const parsedParams = setResourceAiModelsParamsSchema.safeParse(
req.params
@@ -99,15 +100,22 @@ export async function setResourceAiModels(
);
}
const eligibleError = await assertPublicAllowlistApiEligible(resource);
const eligibleError = await assertPublicModelListApiEligible(resource);
if (eligibleError) {
return next(createHttpError(HttpCode.BAD_REQUEST, eligibleError));
}
const modelError = await assertModelsBelongToPublicAllowlistProviders({
const byModelId = new Map(
models.map((m) => [m.modelId, m.listType] as const)
);
const uniqueModels = [...byModelId.entries()].map(
([modelId, listType]) => ({ modelId, listType })
);
const modelError = await assertPublicResourceModelEntriesValid({
orgId: resource.orgId,
resourceId,
modelIds
models: uniqueModels
});
if (modelError) {
return next(createHttpError(HttpCode.BAD_REQUEST, modelError));
@@ -118,12 +126,14 @@ export async function setResourceAiModels(
.delete(resourceAiModels)
.where(eq(resourceAiModels.resourceId, resourceId));
if (modelIds.length > 0) {
await trx
.insert(resourceAiModels)
.values(
modelIds.map((modelId) => ({ resourceId, modelId }))
);
if (uniqueModels.length > 0) {
await trx.insert(resourceAiModels).values(
uniqueModels.map((m) => ({
resourceId,
modelId: m.modelId,
listType: m.listType
}))
);
}
});
@@ -27,7 +27,7 @@ registry.registerPath({
method: "post",
path: "/resource/{resourceId}/ai-providers",
description:
"Replace the AI providers attached to an inference resource. An empty list clears all providers. Model keys must be unique across attached catalog providers.",
"Replace the AI providers attached to an inference resource. Each provider uses accessMode inherit (default, uses the provider's own allow/block lists) or select (uses the resource's selected subset of that provider's catalog). An empty list clears all providers. Effective allow model keys must be unique across attached providers.",
tags: [OpenAPITags.PublicResource],
request: {
params: setResourceAiProvidersParamsSchema,
@@ -113,7 +113,8 @@ export async function setResourceAiProviders(
const attachments = await resolveProviderAttachments({
orgId: resource.orgId,
attachments: providers,
requireAtLeastOne: false
requireAtLeastOne: false,
resourceId
});
if (isInferenceFieldsError(attachments)) {
return next(