1import type {
2 ModelCapabilities,
3 ProviderDriverKind,
4 ServerProviderModel,
5} from "@t3tools/contracts";
6import * as Context from "effect/Context";
7import type * as Effect from "effect/Effect";
8
9import type { ServerProviderDraft } from "./snapshotProbe.ts";
10
11export interface ProviderCatalogModel {
12 readonly slug: string;
13 readonly name: string;
14 readonly shortName?: string;
15 readonly subProvider?: string;
16 readonly aliases?: ReadonlyArray<string>;
17 readonly status: "current" | "legacy";
18 readonly badge?: "new";
19 readonly capabilities: ModelCapabilities | null;
20 readonly adapter: unknown;
21 readonly profileAdapter: unknown;
22}
23
24export interface ProviderCatalog {
25 readonly models: ReadonlyArray<ProviderCatalogModel>;
26 readonly defaultChatModel: string | undefined;
27}
28
29export class ModelCatalog extends Context.Service<
30 ModelCatalog,
31 {
32 readonly current: (
33 driverKind: ProviderDriverKind,
34 ) => Effect.Effect<ProviderCatalog | undefined>;
35 readonly bundled: (driverKind: ProviderDriverKind) => ProviderCatalog | undefined;
36 readonly refreshInBackground: Effect.Effect<void>;
37 }
38>()("@t3tools/provider-core/server/ModelCatalog") {}
39
40export const catalogServerModel = (
41 catalog: ProviderCatalog,
42 entry: ProviderCatalogModel,
43): ServerProviderModel => ({
44 slug: entry.slug,
45 name: entry.name,
46 ...(entry.shortName ? { shortName: entry.shortName } : {}),
47 ...(entry.subProvider ? { subProvider: entry.subProvider } : {}),
48 ...(entry.aliases ? { aliases: entry.aliases } : {}),
49 ...(entry.badge ? { badge: entry.badge } : {}),
50 isCustom: false,
51 ...(catalog.defaultChatModel === entry.slug ? { isDefault: true } : {}),
52 ...(entry.status === "legacy" ? { isLegacy: true } : {}),
53 capabilities: entry.capabilities,
54});
55
56export interface CatalogMatching {
57 readonly family?: (slug: string) => string;
58}
59
60const familyOf = (matching: CatalogMatching | undefined, slug: string) =>
61 matching?.family?.(slug) ?? slug;
62
63export function classifyCatalogModels(
64 models: ReadonlyArray<ServerProviderModel>,
65 catalog: ProviderCatalog | undefined,
66 matching?: CatalogMatching,
67): ReadonlyArray<ServerProviderModel> {
68 return models.map((model) => {
69 if (model.isCustom) return model;
70 const entry =
71 catalog?.models.find((candidate) => candidate.slug === model.slug) ??
72 catalog?.models.find((candidate) => candidate.slug === familyOf(matching, model.slug));
73 if (entry?.status === "legacy") return model.isLegacy ? model : { ...model, isLegacy: true };
74 if (!model.isLegacy) return model;
75 const { isLegacy: _isLegacy, ...rest } = model;
76 return rest;
77 });
78}
79
80export function applyCatalogDefault(
81 models: ReadonlyArray<ServerProviderModel>,
82 catalog: ProviderCatalog | undefined,
83 matching?: CatalogMatching,
84): ReadonlyArray<ServerProviderModel> {
85 const requestedSlug = catalog?.defaultChatModel;
86 if (requestedSlug === undefined) return models;
87 const slug =
88 models.find((model) => model.slug === requestedSlug)?.slug ??
89 (matching?.family === undefined
90 ? undefined
91 : models.find(
92 (model) =>
93 !model.isCustom && familyOf(matching, model.slug) === familyOf(matching, requestedSlug),
94 )?.slug);
95 if (slug === undefined) return models;
96 const previous = models.find((model) => model.isDefault && model.slug !== slug);
97 if (!previous) return models;
98 const movedAliases = previous.aliases ?? [];
99 return models.map((model) => {
100 if (model.slug === previous.slug) {
101 const { isDefault: _isDefault, aliases: _aliases, ...rest } = model;
102 return rest;
103 }
104 if (model.slug === slug) {
105 const aliases = [...new Set([...(model.aliases ?? []), ...movedAliases])];
106 return { ...model, isDefault: true, ...(aliases.length > 0 ? { aliases } : {}) };
107 }
108 return model;
109 });
110}
111
112export function applyModelCatalog(
113 draft: ServerProviderDraft,
114 catalog: ProviderCatalog | undefined,
115 matching?: CatalogMatching,
116): ServerProviderDraft {
117 const { updateRequiredModels: _previous, ...rest } = draft;
118 return {
119 ...rest,
120 models: applyCatalogDefault(
121 classifyCatalogModels(draft.models, catalog, matching),
122 catalog,
123 matching,
124 ),
125 };
126}