1/**
2 * Codex's view of its manifest catalog. Codex discovers models from its app
3 * server, so the catalog only reclassifies them, moves the default, and names
4 * models a newer Codex build would unlock.
5 *
6 * @module provider/codexModelCatalog
7 */
8import type { ServerProviderUpdateRequiredModel } from "@t3tools/contracts";
9import * as ModelCatalog from "@t3tools/provider-core/server/ModelCatalog";
10import type { ServerProviderDraft } from "@t3tools/provider-core/server/snapshotProbe";
11import { codexModelFamily } from "@t3tools/shared/model";
12import { compareSemverVersions, parseSemver } from "@t3tools/shared/semver";
13import * as Option from "effect/Option";
14import * as Schema from "effect/Schema";
15import { TrimmedNonEmptyString } from "@t3tools/contracts";
16
17/** Dated Codex builds match their base model's catalog entry. */
18const CODEX_MATCHING: ModelCatalog.CatalogMatching = { family: codexModelFamily };
19
20const CodexModelAdapter = Schema.Struct({
21 codex: Schema.optional(Schema.Struct({ minVersion: Schema.optional(TrimmedNonEmptyString) })),
22});
23const decodeCodexModelAdapter = Schema.decodeUnknownOption(CodexModelAdapter);
24
25/**
26 * Codex lists only the models its own build knows, so a model released after
27 * the installed CLI never shows up. A current catalog entry with
28 * `adapter.codex.minVersion` names that model, letting the picker say an update
29 * unlocks it instead of leaving users to wonder where it is.
30 */
31function codexUpdateRequiredModels(
32 catalog: ModelCatalog.ProviderCatalog | undefined,
33 draft: ServerProviderDraft,
34): ReadonlyArray<ServerProviderUpdateRequiredModel> {
35 const version = draft.version?.replace(/^v/, "");
36 if (!catalog || !version || parseSemver(version) === null) return [];
37 const discovered = new Set(draft.models.map((model) => codexModelFamily(model.slug)));
38 return catalog.models.flatMap((entry) => {
39 if (entry.status !== "current" || discovered.has(codexModelFamily(entry.slug))) return [];
40 const minVersion = Option.getOrUndefined(decodeCodexModelAdapter(entry.adapter ?? {}))?.codex
41 ?.minVersion;
42 if (!minVersion || parseSemver(minVersion) === null) return [];
43 if (compareSemverVersions(version, minVersion) >= 0) return [];
44 return [
45 {
46 slug: entry.slug,
47 name: entry.name,
48 ...(entry.badge ? { badge: entry.badge } : {}),
49 minVersion,
50 },
51 ];
52 });
53}
54
55/** Reclassifies a Codex snapshot draft's models against its catalog. */
56export function applyCodexModelCatalog(
57 draft: ServerProviderDraft,
58 catalog: ModelCatalog.ProviderCatalog | undefined,
59): ServerProviderDraft {
60 const updateRequiredModels = codexUpdateRequiredModels(catalog, draft);
61 return {
62 ...ModelCatalog.applyModelCatalog(draft, catalog, CODEX_MATCHING),
63 ...(updateRequiredModels.length > 0 ? { updateRequiredModels } : {}),
64 };
65}