import { defaultModelCapabilityConfig, type ModelCapabilityConfig, type VideoCapabilityConfig } from "@/lib/model-capabilities"; import { modelProtocolCapability, protocolForModelCatalog, type ModelProtocol } from "@/lib/model-protocols"; import type { ModelChannel } from "@/stores/use-config-store"; export type ChannelModelCatalogOption = { value: string; label?: string }; export type ChannelModelCatalogItem = { id: string; displayName?: string; modelType?: "text" | "image" | "video" | "audio"; supportedEndpointTypes?: string[]; defaultParameters?: { aspectRatio?: string; durationSeconds?: string; resolution?: string; }; options?: { aspectRatio?: ChannelModelCatalogOption[]; durationSeconds?: ChannelModelCatalogOption[]; resolution?: ChannelModelCatalogOption[]; }; supportsImages?: boolean; minImages?: number; maxImages?: number; }; type ChannelModelCost = NonNullable[number]; export function sanitizeChannelModelCatalogItem(value: unknown): ChannelModelCatalogItem | null { if (!value || typeof value !== "object") return null; const record = value as Record; const id = stringValue(record.id); if (!id) return null; const modelType = stringValue(record.modelType).toLowerCase(); const defaultParameters = objectValue(record.defaultParameters); const options = objectValue(record.options); const normalizedDefaults = defaultParameters ? { ...(stringValue(defaultParameters.aspectRatio) ? { aspectRatio: stringValue(defaultParameters.aspectRatio) } : {}), ...(stringValue(defaultParameters.durationSeconds) ? { durationSeconds: stringValue(defaultParameters.durationSeconds) } : {}), ...(stringValue(defaultParameters.resolution) ? { resolution: stringValue(defaultParameters.resolution) } : {}), } : undefined; const normalizedOptions = options ? { ...(catalogOptions(options.aspectRatio).length ? { aspectRatio: catalogOptions(options.aspectRatio) } : {}), ...(catalogOptions(options.durationSeconds).length ? { durationSeconds: catalogOptions(options.durationSeconds) } : {}), ...(catalogOptions(options.resolution).length ? { resolution: catalogOptions(options.resolution) } : {}), } : undefined; return compactCatalogItem({ id, displayName: stringValue(record.displayName), modelType: ["text", "image", "video", "audio"].includes(modelType) ? (modelType as ChannelModelCatalogItem["modelType"]) : undefined, supportedEndpointTypes: stringArray(record.supportedEndpointTypes), defaultParameters: normalizedDefaults, options: normalizedOptions, supportsImages: typeof record.supportsImages === "boolean" ? record.supportsImages : undefined, minImages: nonNegativeInteger(record.minImages), maxImages: nonNegativeInteger(record.maxImages), }); } export function mergeFetchedChannelModelCosts(channel: ModelChannel, catalog: ChannelModelCatalogItem[]): ChannelModelCost[] { const existingByModel = new Map((channel.modelCosts || []).map((cost) => [cost.model, cost])); const next: ChannelModelCost[] = []; for (const item of catalog) { const existing = existingByModel.get(item.id); const catalogBackedCapability = hasCatalogCapabilityMetadata(item); const inferredProtocol = protocolForModelCatalog(item.supportedEndpointTypes, item.modelType); if (existing && !catalogBackedCapability && !inferredProtocol) { next.push({ ...existing, ...(item.displayName ? { displayName: item.displayName } : {}), }); continue; } const protocol = inferredProtocol || existing?.protocol || channel.interfaceType; const capability = modelProtocolCapability(protocol) || item.modelType; if (!protocol || !capability) continue; const protocolChanged = Boolean(existing && inferredProtocol && existing.protocol !== inferredProtocol); const replaceCapabilityConfig = !existing || catalogBackedCapability || protocolChanged; const capabilityConfig = replaceCapabilityConfig && (capability === "image" || capability === "video") ? (catalogBackedCapability ? catalogCapabilityConfig(item, protocol, capability) : defaultModelCapabilityConfig(protocol, item.id)) : undefined; if (existing) { next.push({ ...existing, ...(item.displayName ? { displayName: item.displayName } : {}), capability, protocol, ...(replaceCapabilityConfig ? { capabilityConfig } : {}), }); continue; } next.push({ model: item.id, ...(item.displayName ? { displayName: item.displayName } : {}), capability, protocol, billingMode: "fixed_request", unitPriceMicrocredits: 0, ...(capabilityConfig ? { capabilityConfig } : {}), }); } return next; } function hasCatalogCapabilityMetadata(item: ChannelModelCatalogItem) { return Boolean(item.modelType || item.supportedEndpointTypes?.length || item.defaultParameters || item.options || item.supportsImages !== undefined || item.minImages !== undefined || item.maxImages !== undefined); } function catalogCapabilityConfig(item: ChannelModelCatalogItem, protocol: ModelProtocol, capability: "image" | "video"): ModelCapabilityConfig { const config = defaultModelCapabilityConfig(protocol, item.id); if (capability === "image") { if (item.supportsImages === false) config.image!.references.maxImages = 0; else if (item.maxImages !== undefined) config.image!.references.maxImages = item.maxImages; return config; } const video = config.video!; const durations = uniqueNumbers(optionValues(item.options?.durationSeconds)); const defaultDuration = positiveNumber(item.defaultParameters?.durationSeconds); if (durations.length) { video.duration = { selection: "enum", values: durations, default: durations.includes(defaultDuration) ? defaultDuration : durations[0]! }; } else if (defaultDuration > 0) { video.duration = { selection: "enum", values: [defaultDuration], default: defaultDuration }; } const ratios = optionValues(item.options?.aspectRatio); const defaultRatio = item.defaultParameters?.aspectRatio?.trim() || ""; if (ratios.length) { video.ratios = ratios; video.defaultRatio = ratios.includes(defaultRatio) ? defaultRatio : ratios[0]!; } else if (defaultRatio) { video.ratios = [defaultRatio]; video.defaultRatio = defaultRatio; } // A catalog-backed video may only emit resolution_name when the provider // explicitly advertises compatible values. An empty list means omit it. const resolutions = optionValues(item.options?.resolution); const defaultResolution = item.defaultParameters?.resolution?.trim() || ""; video.resolutions = resolutions; video.defaultResolution = resolutions.includes(defaultResolution) ? defaultResolution : resolutions[0] || ""; if (item.supportsImages === false) { video.references.maxImages = 0; video.operations = video.operations.filter((operation) => operation !== "image_to_video"); video.defaultOperation = "text_to_video"; } else if (item.maxImages !== undefined) { video.references.maxImages = item.maxImages; } return config; } function compactCatalogItem(item: ChannelModelCatalogItem): ChannelModelCatalogItem { const defaultParameters = item.defaultParameters && Object.values(item.defaultParameters).some(Boolean) ? item.defaultParameters : undefined; const options = item.options && Object.values(item.options).some((values) => values?.length) ? item.options : undefined; return { id: item.id, ...(item.displayName ? { displayName: item.displayName } : {}), ...(item.modelType ? { modelType: item.modelType } : {}), ...(item.supportedEndpointTypes?.length ? { supportedEndpointTypes: item.supportedEndpointTypes } : {}), ...(defaultParameters ? { defaultParameters } : {}), ...(options ? { options } : {}), ...(item.supportsImages !== undefined ? { supportsImages: item.supportsImages } : {}), ...(item.minImages !== undefined ? { minImages: item.minImages } : {}), ...(item.maxImages !== undefined ? { maxImages: item.maxImages } : {}), }; } function objectValue(value: unknown) { return value && typeof value === "object" && !Array.isArray(value) ? (value as Record) : null; } function stringValue(value: unknown) { return typeof value === "string" ? value.trim() : ""; } function stringArray(value: unknown) { return Array.isArray(value) ? Array.from(new Set(value.map(stringValue).filter(Boolean))) : []; } function catalogOptions(value: unknown): ChannelModelCatalogOption[] { if (!Array.isArray(value)) return []; const seen = new Set(); const options: ChannelModelCatalogOption[] = []; for (const raw of value) { const record = objectValue(raw); const optionValue = stringValue(record?.value); if (!optionValue || seen.has(optionValue)) continue; seen.add(optionValue); const label = stringValue(record?.label); options.push({ value: optionValue, ...(label ? { label } : {}) }); } return options; } function optionValues(options: ChannelModelCatalogOption[] | undefined) { return Array.from(new Set((options || []).map((option) => option.value.trim()).filter(Boolean))); } function positiveNumber(value: unknown) { const parsed = Number(value); return Number.isFinite(parsed) && parsed > 0 ? parsed : 0; } function uniqueNumbers(values: string[]) { return Array.from(new Set(values.map(positiveNumber).filter((value) => value > 0))); } function nonNegativeInteger(value: unknown) { return typeof value === "number" && Number.isFinite(value) && value >= 0 ? Math.floor(value) : undefined; }