|
| 1 | +import { normalizeLowercaseStringOrEmpty } from "../shared/string-coerce.js"; |
1 | 2 | import { normalizeModelCatalogProviderRows } from "./normalize.js"; |
2 | | -import { normalizeModelCatalogProviderId } from "./refs.js"; |
3 | | -import type { ModelCatalog, ModelCatalogDiscovery, NormalizedModelCatalogRow } from "./types.js"; |
| 3 | +import { buildModelCatalogMergeKey, normalizeModelCatalogProviderId } from "./refs.js"; |
| 4 | +import type { |
| 5 | + ModelCatalog, |
| 6 | + ModelCatalogAlias, |
| 7 | + ModelCatalogDiscovery, |
| 8 | + NormalizedModelCatalogRow, |
| 9 | +} from "./types.js"; |
4 | 10 |
|
5 | 11 | export type ManifestModelCatalogPlugin = { |
6 | 12 | id: string; |
7 | | - modelCatalog?: Pick<ModelCatalog, "providers" | "discovery">; |
| 13 | + providers?: readonly string[]; |
| 14 | + modelCatalog?: Pick<ModelCatalog, "providers" | "aliases" | "suppressions" | "discovery">; |
8 | 15 | }; |
9 | 16 |
|
10 | 17 | export type ManifestModelCatalogRegistry = { |
@@ -33,6 +40,18 @@ export type ManifestModelCatalogPlan = { |
33 | 40 | conflicts: readonly ManifestModelCatalogConflict[]; |
34 | 41 | }; |
35 | 42 |
|
| 43 | +export type ManifestModelCatalogSuppressionEntry = { |
| 44 | + pluginId: string; |
| 45 | + provider: string; |
| 46 | + model: string; |
| 47 | + mergeKey: string; |
| 48 | + reason?: string; |
| 49 | +}; |
| 50 | + |
| 51 | +export type ManifestModelCatalogSuppressionPlan = { |
| 52 | + suppressions: readonly ManifestModelCatalogSuppressionEntry[]; |
| 53 | +}; |
| 54 | + |
36 | 55 | export function planManifestModelCatalogRows(params: { |
37 | 56 | registry: ManifestModelCatalogRegistry; |
38 | 57 | providerFilter?: string; |
@@ -94,29 +113,147 @@ function planManifestModelCatalogPluginEntries(params: { |
94 | 113 | return []; |
95 | 114 | } |
96 | 115 |
|
| 116 | + const aliasesByTargetProvider = buildModelCatalogProviderAliasTargets(params.plugin); |
| 117 | + |
97 | 118 | return Object.entries(providers).flatMap(([provider, providerCatalog]) => { |
98 | 119 | const normalizedProvider = normalizeModelCatalogProviderId(provider); |
99 | | - if ( |
100 | | - !normalizedProvider || |
101 | | - (params.providerFilter && normalizedProvider !== params.providerFilter) |
102 | | - ) { |
| 120 | + if (!normalizedProvider) { |
103 | 121 | return []; |
104 | 122 | } |
105 | | - const rows = normalizeModelCatalogProviderRows({ |
106 | | - provider: normalizedProvider, |
107 | | - providerCatalog, |
108 | | - source: "manifest", |
109 | | - }); |
110 | | - if (rows.length === 0) { |
| 123 | + const providerAliases = aliasesByTargetProvider.get(normalizedProvider) ?? []; |
| 124 | + const plannedProviders = params.providerFilter |
| 125 | + ? providerAliases.includes(params.providerFilter) || |
| 126 | + normalizedProvider === params.providerFilter |
| 127 | + ? [params.providerFilter] |
| 128 | + : [] |
| 129 | + : [normalizedProvider]; |
| 130 | + if (plannedProviders.length === 0) { |
111 | 131 | return []; |
112 | 132 | } |
113 | | - return [ |
114 | | - { |
115 | | - pluginId: params.plugin.id, |
116 | | - provider: normalizedProvider, |
117 | | - discovery: params.plugin.modelCatalog?.discovery?.[normalizedProvider], |
118 | | - rows, |
119 | | - }, |
120 | | - ]; |
| 133 | + return plannedProviders.flatMap((plannedProvider) => { |
| 134 | + const rows = normalizeModelCatalogProviderRows({ |
| 135 | + provider: plannedProvider, |
| 136 | + providerCatalog, |
| 137 | + source: "manifest", |
| 138 | + }); |
| 139 | + if (rows.length === 0) { |
| 140 | + return []; |
| 141 | + } |
| 142 | + return [ |
| 143 | + { |
| 144 | + pluginId: params.plugin.id, |
| 145 | + provider: plannedProvider, |
| 146 | + discovery: params.plugin.modelCatalog?.discovery?.[normalizedProvider], |
| 147 | + rows: applyModelCatalogAliasOverrides({ |
| 148 | + rows, |
| 149 | + alias: params.plugin.modelCatalog?.aliases?.[plannedProvider], |
| 150 | + }), |
| 151 | + }, |
| 152 | + ]; |
| 153 | + }); |
121 | 154 | }); |
122 | 155 | } |
| 156 | + |
| 157 | +function buildOwnedProviderSet(plugin: ManifestModelCatalogPlugin): ReadonlySet<string> { |
| 158 | + return new Set((plugin.providers ?? []).map(normalizeModelCatalogProviderId).filter(Boolean)); |
| 159 | +} |
| 160 | + |
| 161 | +function buildModelCatalogProviderAliasTargets( |
| 162 | + plugin: ManifestModelCatalogPlugin, |
| 163 | +): ReadonlyMap<string, readonly string[]> { |
| 164 | + const ownedProviders = buildOwnedProviderSet(plugin); |
| 165 | + const aliasesByTargetProvider = new Map<string, string[]>(); |
| 166 | + for (const [rawAlias, alias] of Object.entries(plugin.modelCatalog?.aliases ?? {})) { |
| 167 | + const aliasProvider = normalizeModelCatalogProviderId(rawAlias); |
| 168 | + const targetProvider = normalizeModelCatalogProviderId(alias.provider); |
| 169 | + if (!aliasProvider || !targetProvider || !ownedProviders.has(targetProvider)) { |
| 170 | + continue; |
| 171 | + } |
| 172 | + const aliases = aliasesByTargetProvider.get(targetProvider) ?? []; |
| 173 | + aliases.push(aliasProvider); |
| 174 | + aliasesByTargetProvider.set(targetProvider, aliases); |
| 175 | + } |
| 176 | + return aliasesByTargetProvider; |
| 177 | +} |
| 178 | + |
| 179 | +function applyModelCatalogAliasOverrides(params: { |
| 180 | + rows: readonly NormalizedModelCatalogRow[]; |
| 181 | + alias?: ModelCatalogAlias; |
| 182 | +}): readonly NormalizedModelCatalogRow[] { |
| 183 | + if (!params.alias) { |
| 184 | + return params.rows; |
| 185 | + } |
| 186 | + return params.rows.map((row) => ({ |
| 187 | + ...row, |
| 188 | + ...(params.alias.api ? { api: params.alias.api } : {}), |
| 189 | + ...(params.alias.baseUrl ? { baseUrl: params.alias.baseUrl } : {}), |
| 190 | + })); |
| 191 | +} |
| 192 | + |
| 193 | +function pluginOwnsModelCatalogProviderRef(params: { |
| 194 | + plugin: ManifestModelCatalogPlugin; |
| 195 | + provider: string; |
| 196 | +}): boolean { |
| 197 | + const provider = normalizeModelCatalogProviderId(params.provider); |
| 198 | + if (!provider) { |
| 199 | + return false; |
| 200 | + } |
| 201 | + const ownedProviders = buildOwnedProviderSet(params.plugin); |
| 202 | + if (ownedProviders.has(provider)) { |
| 203 | + return true; |
| 204 | + } |
| 205 | + return Object.entries(params.plugin.modelCatalog?.aliases ?? {}).some(([rawAlias, alias]) => { |
| 206 | + const aliasProvider = normalizeModelCatalogProviderId(rawAlias); |
| 207 | + const targetProvider = normalizeModelCatalogProviderId(alias.provider); |
| 208 | + return ( |
| 209 | + aliasProvider === provider && Boolean(targetProvider) && ownedProviders.has(targetProvider) |
| 210 | + ); |
| 211 | + }); |
| 212 | +} |
| 213 | + |
| 214 | +export function planManifestModelCatalogSuppressions(params: { |
| 215 | + registry: ManifestModelCatalogRegistry; |
| 216 | + providerFilter?: string; |
| 217 | + modelFilter?: string; |
| 218 | +}): ManifestModelCatalogSuppressionPlan { |
| 219 | + const providerFilter = params.providerFilter |
| 220 | + ? normalizeModelCatalogProviderId(params.providerFilter) |
| 221 | + : undefined; |
| 222 | + const modelFilter = params.modelFilter |
| 223 | + ? normalizeLowercaseStringOrEmpty(params.modelFilter) |
| 224 | + : undefined; |
| 225 | + const suppressions: ManifestModelCatalogSuppressionEntry[] = []; |
| 226 | + for (const plugin of params.registry.plugins) { |
| 227 | + for (const suppression of plugin.modelCatalog?.suppressions ?? []) { |
| 228 | + const provider = normalizeModelCatalogProviderId(suppression.provider); |
| 229 | + const model = normalizeLowercaseStringOrEmpty(suppression.model); |
| 230 | + if (!provider || !model) { |
| 231 | + continue; |
| 232 | + } |
| 233 | + if (providerFilter && provider !== providerFilter) { |
| 234 | + continue; |
| 235 | + } |
| 236 | + if (modelFilter && model !== modelFilter) { |
| 237 | + continue; |
| 238 | + } |
| 239 | + if (!pluginOwnsModelCatalogProviderRef({ plugin, provider })) { |
| 240 | + continue; |
| 241 | + } |
| 242 | + suppressions.push({ |
| 243 | + pluginId: plugin.id, |
| 244 | + provider, |
| 245 | + model, |
| 246 | + mergeKey: buildModelCatalogMergeKey(provider, model), |
| 247 | + ...(suppression.reason ? { reason: suppression.reason } : {}), |
| 248 | + }); |
| 249 | + } |
| 250 | + } |
| 251 | + return { |
| 252 | + suppressions: suppressions.toSorted( |
| 253 | + (left, right) => |
| 254 | + left.provider.localeCompare(right.provider) || |
| 255 | + left.model.localeCompare(right.model) || |
| 256 | + left.pluginId.localeCompare(right.pluginId), |
| 257 | + ), |
| 258 | + }; |
| 259 | +} |
0 commit comments