add model picker to create provider wizard

This commit is contained in:
miloschwartz
2026-08-13 15:08:10 -04:00
parent d9a9ae14fd
commit 9b10292e02
10 changed files with 341 additions and 81 deletions
+30
View File
@@ -284,6 +284,36 @@ export class AiModelCatalog {
export const aiModelCatalog = new AiModelCatalog();
export function listCatalogModelsForType(
type: AiProviderType,
query?: string
): { model: string }[] {
const catalogProvider = getCatalogProviderForType(type);
let models = catalogProvider
? aiModelCatalog.list(catalogProvider).map((entry) => ({
model: entry.model
}))
: [];
if (query) {
const q = query.toLowerCase();
models = models.filter((m) => m.model.toLowerCase().includes(q));
}
const seen = new Set<string>();
models = models.filter((m) => {
if (seen.has(m.model)) {
return false;
}
seen.add(m.model);
return true;
});
models.sort((a, b) => a.model.localeCompare(b.model));
return models;
}
/**
* Loads the AI model pricing catalog into memory and schedules periodic
* background refreshes. Call once at server startup.
+1
View File
@@ -6,6 +6,7 @@ export * from "./deleteAiProvider";
export * from "./createAiModel";
export * from "./listAiModels";
export * from "./listCatalogModels";
export * from "./listCatalogModelsByType";
export * from "./getAiModel";
export * from "./updateAiModel";
export * from "./deleteAiModel";
+4 -31
View File
@@ -8,10 +8,7 @@ import logger from "@server/logger";
import { fromError } from "zod-validation-error";
import { OpenAPITags, registry } from "@server/openApi";
import { eq } from "drizzle-orm";
import {
aiModelCatalog,
getCatalogProviderForType
} from "@server/lib/aiModelCatalog";
import { listCatalogModelsForType } from "@server/lib/aiModelCatalog";
import type { AiProviderType } from "@server/lib/aiProviderDefaults";
import type { ListCatalogModelsResponse } from "@server/routers/aiProvider/types";
@@ -86,35 +83,11 @@ export async function listCatalogModels(
);
}
const catalogProvider = getCatalogProviderForType(
provider.type as AiProviderType
const models = listCatalogModelsForType(
provider.type as AiProviderType,
parsedQuery.data.query
);
let models = catalogProvider
? aiModelCatalog.list(catalogProvider).map((entry) => ({
model: entry.model
}))
: [];
const { query } = parsedQuery.data;
if (query) {
const q = query.toLowerCase();
models = models.filter((m) => m.model.toLowerCase().includes(q));
}
// Deduplicate model keys (catalog may have duplicates after provider
// normalization, e.g. bedrock + bedrock_converse).
const seen = new Set<string>();
models = models.filter((m) => {
if (seen.has(m.model)) {
return false;
}
seen.add(m.model);
return true;
});
models.sort((a, b) => a.model.localeCompare(b.model));
return response<ListCatalogModelsResponse>(res, {
data: { models },
success: true,
@@ -0,0 +1,81 @@
import { Request, Response, NextFunction } from "express";
import { z } from "zod";
import response from "@server/lib/response";
import HttpCode from "@server/types/HttpCode";
import createHttpError from "http-errors";
import logger from "@server/logger";
import { fromError } from "zod-validation-error";
import { OpenAPITags, registry } from "@server/openApi";
import { listCatalogModelsForType } from "@server/lib/aiModelCatalog";
import type { ListCatalogModelsResponse } from "@server/routers/aiProvider/types";
import { aiProviderTypeSchema } from "@server/routers/aiProvider/validation";
const paramsSchema = z.strictObject({
orgId: z.string().nonempty()
});
const querySchema = z.strictObject({
type: aiProviderTypeSchema,
query: z.string().optional()
});
registry.registerPath({
method: "get",
path: "/org/{orgId}/ai-catalog-models",
description:
"List known catalog models for an AI provider type. Used for model key suggestions before a provider exists.",
tags: [OpenAPITags.AiModel],
request: {
params: paramsSchema,
query: querySchema
},
responses: {
200: {
description: "Successful response"
}
}
});
export async function listCatalogModelsByType(
req: Request,
res: Response,
next: NextFunction
): Promise<any> {
try {
const parsedParams = paramsSchema.safeParse(req.params);
if (!parsedParams.success) {
return next(
createHttpError(
HttpCode.BAD_REQUEST,
fromError(parsedParams.error).toString()
)
);
}
const parsedQuery = querySchema.safeParse(req.query);
if (!parsedQuery.success) {
return next(
createHttpError(
HttpCode.BAD_REQUEST,
fromError(parsedQuery.error).toString()
)
);
}
const { type, query } = parsedQuery.data;
const models = listCatalogModelsForType(type, query);
return response<ListCatalogModelsResponse>(res, {
data: { models },
success: true,
error: false,
message: "Catalog models retrieved successfully",
status: HttpCode.OK
});
} catch (error) {
logger.error(error);
return next(
createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred")
);
}
}
+7
View File
@@ -1645,6 +1645,13 @@ authenticated.get(
aiProvider.listCatalogModels
);
authenticated.get(
"/org/:orgId/ai-catalog-models",
verifyOrgAccess,
verifyUserHasAction(ActionsEnum.listAiModels),
aiProvider.listCatalogModelsByType
);
authenticated.get(
"/ai-model/:modelId",
verifyAiModelAccess,