1import { PROVIDER_DISPLAY_NAMES, type ServerProvider } from "@t3tools/contracts";
2import { compareSemverVersions } from "@t3tools/shared/semver";
3
4// Name the thing the user updates; "Claude" alone reads like the app or model.
5const RUNTIME_NAMES: Partial<Record<string, string>> = {
6 claudeAgent: "Claude Code",
7 codex: "the Codex CLI",
8};
9
10function formatModelList(names: ReadonlyArray<string>): string {
11 if (names.length <= 2) return names.join(" and ");
12 return `${names.slice(0, -1).join(", ")}, and ${names[names.length - 1]}`;
13}
14
15/**
16 * Model picker notice for models the manifest announces but the installed
17 * provider is too old to run, e.g. "Update Claude Code to v2.1.300 or newer to
18 * use Claude Opus 6." With a search query, only gated models it matches are
19 * named, so searching for one explains why it is missing. Null when nothing
20 * gated matches.
21 */
22export function formatProviderUpdateRequiredNotice(
23 provider: Pick<ServerProvider, "driver" | "updateRequiredModels">,
24 searchQuery = "",
25): string | null {
26 const query = searchQuery.trim().toLocaleLowerCase();
27 const models = (provider.updateRequiredModels ?? []).filter(
28 (model) =>
29 query.length === 0 ||
30 model.name.toLocaleLowerCase().includes(query) ||
31 model.slug.toLocaleLowerCase().includes(query),
32 );
33 if (models.length === 0) return null;
34 // The highest bar unlocks every listed model.
35 const minVersion = models
36 .map((model) => model.minVersion)
37 .reduce((highest, version) =>
38 compareSemverVersions(version, highest) > 0 ? version : highest,
39 );
40 const providerName =
41 RUNTIME_NAMES[provider.driver] ?? PROVIDER_DISPLAY_NAMES[provider.driver] ?? provider.driver;
42 const version = minVersion.startsWith("v") ? minVersion : `v${minVersion}`;
43 const names = formatModelList(models.map((model) => model.name));
44 return `Update ${providerName} to ${version} or newer to use ${names}.`;
45}