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
+4 -4
View File
@@ -232,9 +232,9 @@ export const resourceAiProviders = pgTable(
.notNull()
.references(() => aiProviders.providerId, { onDelete: "cascade" }),
modelAccessMode: varchar("modelAccessMode")
.$type<"passthrough" | "catalog" | "allowlist">()
.$type<"catalog" | "allowlist">()
.notNull()
.default("passthrough")
.default("catalog")
},
(t) => [primaryKey({ columns: [t.resourceId, t.providerId] })]
);
@@ -523,9 +523,9 @@ export const siteResourceAiProviders = pgTable(
.notNull()
.references(() => aiProviders.providerId, { onDelete: "cascade" }),
modelAccessMode: varchar("modelAccessMode")
.$type<"passthrough" | "catalog" | "allowlist">()
.$type<"catalog" | "allowlist">()
.notNull()
.default("passthrough")
.default("catalog")
},
(t) => [primaryKey({ columns: [t.siteResourceId, t.providerId] })]
);
+4 -4
View File
@@ -229,9 +229,9 @@ export const resourceAiProviders = sqliteTable(
.notNull()
.references(() => aiProviders.providerId, { onDelete: "cascade" }),
modelAccessMode: text("modelAccessMode")
.$type<"passthrough" | "catalog" | "allowlist">()
.$type<"catalog" | "allowlist">()
.notNull()
.default("passthrough")
.default("catalog")
},
(t) => [primaryKey({ columns: [t.resourceId, t.providerId] })]
);
@@ -508,9 +508,9 @@ export const siteResourceAiProviders = sqliteTable(
.notNull()
.references(() => aiProviders.providerId, { onDelete: "cascade" }),
modelAccessMode: text("modelAccessMode")
.$type<"passthrough" | "catalog" | "allowlist">()
.$type<"catalog" | "allowlist">()
.notNull()
.default("passthrough")
.default("catalog")
},
(t) => [primaryKey({ columns: [t.siteResourceId, t.providerId] })]
);
+60 -19
View File
@@ -13,11 +13,7 @@ import { z } from "zod";
type DbOrTrx = Transaction | typeof db;
export const modelAccessModeSchema = z.enum([
"passthrough",
"catalog",
"allowlist"
]);
export const modelAccessModeSchema = z.enum(["catalog", "allowlist"]);
export type ModelAccessMode = z.infer<typeof modelAccessModeSchema>;
@@ -50,10 +46,7 @@ function normalizeAttachments(
): ResourceAiProviderAttachment[] {
const byProvider = new Map<number, ModelAccessMode>();
for (const input of inputs) {
byProvider.set(
input.providerId,
input.modelAccessMode ?? "passthrough"
);
byProvider.set(input.providerId, input.modelAccessMode ?? "catalog");
}
return [...byProvider.entries()].map(([providerId, modelAccessMode]) => ({
providerId,
@@ -61,9 +54,61 @@ function normalizeAttachments(
}));
}
/**
* Ensure enabled catalog modelKeys are unique across attached providers.
* Catalog attachments contribute all enabled models on the provider.
* Allowlist attachments contribute nothing until models are allowlisted
* (those are checked when the allowlist is set).
*/
export async function assertNoOverlappingModelKeys(
attachments: ResourceAiProviderAttachment[],
trx: DbOrTrx = db
): Promise<InferenceFieldsError | null> {
const catalogProviderIds = attachments
.filter((a) => a.modelAccessMode === "catalog")
.map((a) => a.providerId);
if (catalogProviderIds.length < 2) {
return null;
}
const models = await trx
.select({
providerId: aiModels.providerId,
modelKey: aiModels.modelKey
})
.from(aiModels)
.where(
and(
inArray(aiModels.providerId, catalogProviderIds),
eq(aiModels.enabled, true)
)
);
const keyToProviders = new Map<string, number[]>();
for (const model of models) {
const existing = keyToProviders.get(model.modelKey) ?? [];
if (!existing.includes(model.providerId)) {
existing.push(model.providerId);
}
keyToProviders.set(model.modelKey, existing);
}
const overlaps = [...keyToProviders.entries()].filter(
([, providerIds]) => providerIds.length > 1
);
if (overlaps.length === 0) {
return null;
}
const keys = overlaps.map(([key]) => key).sort();
return {
error: `Model keys must be unique across providers on a resource. Overlapping keys: ${keys.join(", ")}`
};
}
/**
* Validate provider attachments for an org.
* At most one passthrough provider is allowed per resource.
*/
export async function resolveProviderAttachments(input: {
orgId: string;
@@ -78,15 +123,6 @@ export async function resolveProviderAttachments(input: {
};
}
const passthroughCount = attachments.filter(
(a) => a.modelAccessMode === "passthrough"
).length;
if (passthroughCount > 1) {
return {
error: "A resource may have at most one AI provider in passthrough mode"
};
}
if (attachments.length === 0) {
return [];
}
@@ -119,6 +155,11 @@ export async function resolveProviderAttachments(input: {
};
}
const overlapError = await assertNoOverlappingModelKeys(attachments);
if (overlapError) {
return overlapError;
}
return attachments;
}
@@ -331,10 +331,6 @@ async function providerMatchesModel(
requestedModel: string,
allowedModelIds: number[]
): Promise<boolean> {
if (attachment.modelAccessMode === "passthrough") {
return true;
}
const [matchedModel] = await db
.select({
modelId: aiModels.modelId,
@@ -366,26 +362,7 @@ async function selectProvider(
allowedModelIds: number[],
requestedModel: string | undefined
): Promise<ProviderSelection> {
const passthroughAttachments = attachments.filter(
(a) => a.modelAccessMode === "passthrough"
);
const hasRestricted = attachments.some(
(a) =>
a.modelAccessMode === "catalog" || a.modelAccessMode === "allowlist"
);
if (!requestedModel) {
if (hasRestricted) {
return {
ok: false,
status: HttpCode.FORBIDDEN,
message:
"This resource restricts access to specific models; a model must be specified"
};
}
if (passthroughAttachments.length === 1) {
return { ok: true, provider: passthroughAttachments[0].provider };
}
return {
ok: false,
status: HttpCode.FORBIDDEN,
@@ -395,10 +372,6 @@ async function selectProvider(
const candidates: ProviderAttachment[] = [];
for (const attachment of attachments) {
if (attachment.modelAccessMode === "passthrough") {
candidates.push(attachment);
continue;
}
if (
await providerMatchesModel(
attachment,
@@ -422,11 +395,6 @@ async function selectProvider(
};
}
// Zero candidates: fall back to a single passthrough attachment if present
if (passthroughAttachments.length === 1) {
return { ok: true, provider: passthroughAttachments[0].provider };
}
return {
ok: false,
status: HttpCode.FORBIDDEN,
+2 -4
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 (passthrough, catalog, or allowlist); defaults to passthrough. At most one passthrough provider is allowed."
"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."
)
})
.refine(
@@ -397,9 +397,7 @@ async function createHttpResource(
requireAtLeastOne: true
});
if (isInferenceFieldsError(resolved)) {
return next(
createHttpError(HttpCode.BAD_REQUEST, resolved.error)
);
return next(createHttpError(HttpCode.BAD_REQUEST, resolved.error));
}
providerAttachments = resolved;
} else if (aiProviderInputs && aiProviderInputs.length > 0) {
@@ -27,7 +27,7 @@ registry.registerPath({
method: "post",
path: "/resource/{resourceId}/ai-providers",
description:
"Replace the AI providers attached to an inference resource. At least one provider is required. At most one may use passthrough mode.",
"Replace the AI providers attached to an inference resource. At least one provider is required. Model keys must be unique across attached catalog providers.",
tags: [OpenAPITags.PublicResource],
request: {
params: setResourceAiProvidersParamsSchema,
@@ -116,7 +116,9 @@ export async function setResourceAiProviders(
requireAtLeastOne: true
});
if (isInferenceFieldsError(attachments)) {
return next(createHttpError(HttpCode.BAD_REQUEST, attachments.error));
return next(
createHttpError(HttpCode.BAD_REQUEST, attachments.error)
);
}
await setPublicResourceAiProviders(resourceId, attachments);
@@ -90,7 +90,7 @@ const createSiteResourceSchema = z
.array(resourceAiProviderAttachmentSchema)
.optional()
.describe(
"For inference-mode site resources: AI providers to attach. Each entry may set modelAccessMode (passthrough, catalog, or allowlist); defaults to passthrough. At most one passthrough provider is allowed."
"For inference-mode site 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."
)
})
.strict()
@@ -27,7 +27,7 @@ registry.registerPath({
method: "post",
path: "/site-resource/{siteResourceId}/ai-providers",
description:
"Replace the AI providers attached to an inference site resource. At least one provider is required. At most one may use passthrough mode.",
"Replace the AI providers attached to an inference site resource. At least one provider is required. Model keys must be unique across attached catalog providers.",
tags: [OpenAPITags.PrivateResource],
request: {
params: setSiteResourceAiProvidersParamsSchema,
@@ -118,7 +118,9 @@ export async function setSiteResourceAiProviders(
requireAtLeastOne: true
});
if (isInferenceFieldsError(attachments)) {
return next(createHttpError(HttpCode.BAD_REQUEST, attachments.error));
return next(
createHttpError(HttpCode.BAD_REQUEST, attachments.error)
);
}
await replaceAttachments(siteResourceId, attachments);