main
 1export type ModelEntry = { provider: string; modelId: string };
 2export type RejectionReason = "masked" | "not-in-catalog" | "missing-credentials";
 3export type Rejection = { provider: string; modelId: string; reason: RejectionReason };
 4export type Resolution = {
 5	selected: { provider: string; modelId: string } | null;
 6	rejected: Rejection[];
 7};
 8
 9export type ResolveSubagentModelOptions = {
10	parentProvider?: string;
11	pinnedProvider?: string;
12	providerPreference: string[];
13	requestedModel: string;
14	modelFallbacks: Record<string, string[]>;
15	providerModelMasks: Record<string, string[]>;
16	catalog: ModelEntry[];
17	ready: ReadonlySet<string>;
18};
19
20const key = (provider: string, modelId: string) => `${provider}\u0000${modelId}`;
21
22function orderedUnique(values: Array<string | undefined>): string[] {
23	return [...new Set(values.filter((value): value is string => Boolean(value)))];
24}
25
26function catalogModelId(catalog: ModelEntry[], provider: string, requested: string): string | undefined {
27	return (
28		catalog.find((entry) => entry.provider === provider && entry.modelId === requested)?.modelId ??
29		catalog.find((entry) => entry.provider === provider && entry.modelId.startsWith(`${requested}@`))?.modelId
30	);
31}
32
33export function resolveSubagentModel(options: ResolveSubagentModelOptions): Resolution {
34	const providers = options.pinnedProvider
35		? [options.pinnedProvider]
36		: orderedUnique([options.parentProvider, ...options.providerPreference]);
37	const models = orderedUnique([options.requestedModel, ...(options.modelFallbacks[options.requestedModel] ?? [])]);
38	const rejected: Rejection[] = [];
39
40	for (const provider of providers) {
41		const masked = new Set(options.providerModelMasks[provider] ?? []);
42		for (const requestedModel of models) {
43			if (masked.has(requestedModel)) {
44				rejected.push({ provider, modelId: requestedModel, reason: "masked" });
45				continue;
46			}
47			const modelId = catalogModelId(options.catalog, provider, requestedModel);
48			if (!modelId) {
49				rejected.push({ provider, modelId: requestedModel, reason: "not-in-catalog" });
50				continue;
51			}
52			if (!options.ready.has(key(provider, modelId))) {
53				rejected.push({ provider, modelId, reason: "missing-credentials" });
54				continue;
55			}
56			return { selected: { provider, modelId }, rejected };
57		}
58	}
59
60	return { selected: null, rejected };
61}