improve model provider selection algorithm

This commit is contained in:
miloschwartz
2026-08-11 16:10:34 -04:00
parent 75c6af3b11
commit c42df737b0
11 changed files with 408 additions and 200 deletions
-161
View File
@@ -10,7 +10,6 @@ import {
type Transaction
} from "@server/db";
import { z } from "zod";
import { modelKeysConflict } from "@server/lib/aiModelKeyMatch";
type DbOrTrx = Transaction | typeof db;
@@ -100,142 +99,6 @@ function normalizeAttachments(
);
}
type EffectiveAllowRow = {
providerId: number;
modelKey: string;
};
/**
* Ensure effective allow modelKeys do not conflict across attached providers.
* inherit uses provider allows; select uses resource-selected allows (or the
* optional override map). Block patterns are ignored for overlap checks.
*/
export async function assertNoOverlappingModelKeys(
attachments: ResourceAiProviderAttachment[],
options: {
trx?: DbOrTrx;
resourceId?: number;
siteResourceId?: number;
selectedAllowsByProvider?: Map<number, string[]>;
} = {}
): Promise<InferenceFieldsError | null> {
const trx = options.trx ?? db;
const activeAttachments = attachments.filter((a) => a.enabled);
if (activeAttachments.length < 2) {
return null;
}
const inheritProviderIds = activeAttachments
.filter((a) => a.accessMode === "inherit")
.map((a) => a.providerId);
const selectProviderIds = activeAttachments
.filter((a) => a.accessMode === "select")
.map((a) => a.providerId);
const effectiveAllows: EffectiveAllowRow[] = [];
if (inheritProviderIds.length > 0) {
const providerAllows = await trx
.select({
providerId: aiModels.providerId,
modelKey: aiModels.modelKey
})
.from(aiModels)
.where(
and(
inArray(aiModels.providerId, inheritProviderIds),
eq(aiModels.enabled, true),
eq(aiModels.listType, "allow")
)
);
effectiveAllows.push(...providerAllows);
}
if (selectProviderIds.length > 0) {
if (options.selectedAllowsByProvider) {
for (const providerId of selectProviderIds) {
const keys =
options.selectedAllowsByProvider.get(providerId) ?? [];
for (const modelKey of keys) {
effectiveAllows.push({ providerId, modelKey });
}
}
} else if (options.resourceId !== undefined) {
const rows = await trx
.select({
providerId: aiModels.providerId,
modelKey: aiModels.modelKey
})
.from(resourceAiModels)
.innerJoin(
aiModels,
eq(resourceAiModels.modelId, aiModels.modelId)
)
.where(
and(
eq(resourceAiModels.resourceId, options.resourceId),
inArray(aiModels.providerId, selectProviderIds),
eq(resourceAiModels.listType, "allow"),
eq(aiModels.enabled, true)
)
);
effectiveAllows.push(...rows);
} else if (options.siteResourceId !== undefined) {
const rows = await trx
.select({
providerId: aiModels.providerId,
modelKey: aiModels.modelKey
})
.from(siteResourceAiModels)
.innerJoin(
aiModels,
eq(siteResourceAiModels.modelId, aiModels.modelId)
)
.where(
and(
eq(
siteResourceAiModels.siteResourceId,
options.siteResourceId
),
inArray(aiModels.providerId, selectProviderIds),
eq(siteResourceAiModels.listType, "allow"),
eq(aiModels.enabled, true)
)
);
effectiveAllows.push(...rows);
}
}
const conflictPairs: string[] = [];
for (let i = 0; i < effectiveAllows.length; i++) {
for (let j = i + 1; j < effectiveAllows.length; j++) {
const left = effectiveAllows[i];
const right = effectiveAllows[j];
if (left.providerId === right.providerId) {
continue;
}
if (!modelKeysConflict(left.modelKey, right.modelKey)) {
continue;
}
const pair = [left.modelKey, right.modelKey].sort().join(" vs ");
if (!conflictPairs.includes(pair)) {
conflictPairs.push(pair);
}
}
}
if (conflictPairs.length === 0) {
return null;
}
conflictPairs.sort();
return {
error: `Model keys must be unique across providers on a resource. Overlapping keys: ${conflictPairs.join(", ")}`
};
}
/**
* Validate provider attachments for an org.
*/
@@ -243,8 +106,6 @@ export async function resolveProviderAttachments(input: {
orgId: string;
attachments: ResourceAiProviderInput[];
requireAtLeastOne: boolean;
resourceId?: number;
siteResourceId?: number;
}): Promise<ResourceAiProviderAttachment[] | InferenceFieldsError> {
const attachments = normalizeAttachments(input.attachments);
@@ -286,14 +147,6 @@ export async function resolveProviderAttachments(input: {
};
}
const overlapError = await assertNoOverlappingModelKeys(attachments, {
resourceId: input.resourceId,
siteResourceId: input.siteResourceId
});
if (overlapError) {
return overlapError;
}
return attachments;
}
@@ -830,7 +683,6 @@ async function assertModelEntriesValid(input: {
const catalogRows = await db
.select({
modelId: aiModels.modelId,
modelKey: aiModels.modelKey,
listType: aiModels.listType,
providerId: aiModels.providerId,
enabled: aiModels.enabled
@@ -850,7 +702,6 @@ async function assertModelEntriesValid(input: {
}
const catalogById = new Map(catalogRows.map((row) => [row.modelId, row]));
const selectedAllowsByProvider = new Map<number, string[]>();
for (const entry of input.modelEntries) {
const catalog = catalogById.get(entry.modelId);
if (!catalog) {
@@ -862,18 +713,6 @@ async function assertModelEntriesValid(input: {
if (!catalog.enabled) {
return `Model ${entry.modelId} is disabled on its provider`;
}
if (entry.listType === "allow") {
const keys = selectedAllowsByProvider.get(catalog.providerId) ?? [];
keys.push(catalog.modelKey);
selectedAllowsByProvider.set(catalog.providerId, keys);
}
}
const overlapError = await assertNoOverlappingModelKeys(input.attachments, {
selectedAllowsByProvider
});
if (overlapError) {
return overlapError.error;
}
return null;
-23
View File
@@ -61,29 +61,6 @@ export function compareModelKeySpecificity(a: string, b: string): number {
return literalLength(b) - literalLength(a);
}
/**
* Attach-time conflict check. Detects identical keys and exact-vs-pattern
* matches. Does not attempt full glob intersection.
*/
export function modelKeysConflict(a: string, b: string): boolean {
if (a === b) {
return true;
}
const aIsPattern = isModelKeyPattern(a);
const bIsPattern = isModelKeyPattern(b);
if (aIsPattern === bIsPattern) {
return false;
}
if (aIsPattern) {
return modelKeyMatches(a, b);
}
return modelKeyMatches(b, a);
}
/**
* Provider-layer policy: empty allowlist denies all. Blocklist only applies
* after an allow match.
+97
View File
@@ -0,0 +1,97 @@
import {
aiModelCatalog,
getCatalogProviderForType,
type CatalogProvider
} from "@server/lib/aiModelCatalog";
import type { AiProviderType } from "@server/lib/aiProviderDefaults";
function stripVendorPrefix(modelId: string): string | null {
const idx = modelId.indexOf("/");
if (idx === -1 || idx === modelId.length - 1) {
return null;
}
return modelId.slice(idx + 1);
}
function modelKeysToTry(modelId: string): string[] {
const keys = [modelId];
const stripped = stripVendorPrefix(modelId);
if (stripped) {
keys.push(stripped);
}
return keys;
}
function catalogOwnsModel(
catalogProvider: CatalogProvider,
modelId: string
): boolean {
for (const key of modelKeysToTry(modelId)) {
if (aiModelCatalog.get(catalogProvider, key)) {
return true;
}
}
return false;
}
function modelKnownInAnyCatalog(modelId: string): boolean {
for (const key of modelKeysToTry(modelId)) {
if (aiModelCatalog.listByKey(key).length > 0) {
return true;
}
}
return false;
}
/**
* How strongly a provider "owns" a requested model id via the known catalog.
*
* 2 - Typed provider whose catalog contains the model
* 1 - Aggregator/custom that can proxy a catalog-known model
* 0 - No ownership signal (typed miss, or unknown model on aggregator/custom)
*/
export function catalogOwnershipScore(
type: AiProviderType,
modelId: string
): number {
const catalogProvider = getCatalogProviderForType(type);
if (catalogProvider != null) {
return catalogOwnsModel(catalogProvider, modelId) ? 2 : 0;
}
return modelKnownInAnyCatalog(modelId) ? 1 : 0;
}
/**
* Prefer native vendor providers over aggregators over custom when catalog
* ownership is tied.
*
* 2 - Native typed provider (openai, anthropic, gemini, ...)
* 1 - Aggregator gateway (openRouter, vercelAiGateway)
* 0 - Custom
*/
export function providerClassRank(type: AiProviderType): number {
if (type === "custom") {
return 0;
}
if (type === "openRouter" || type === "vercelAiGateway") {
return 1;
}
return 2;
}
export function keepBestScored<T>(
items: T[],
scoreFn: (item: T) => number
): T[] {
if (items.length <= 1) {
return items;
}
let best = Number.NEGATIVE_INFINITY;
for (const item of items) {
const score = scoreFn(item);
if (score > best) {
best = score;
}
}
return items.filter((item) => scoreFn(item) === best);
}