| 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 | */ |
| 8 | import type { ServerProviderUpdateRequiredModel } from "@t3tools/contracts"; |
| 9 | import * as ModelCatalog from "@t3tools/provider-core/server/ModelCatalog"; |
| 10 | import type { ServerProviderDraft } from "@t3tools/provider-core/server/snapshotProbe"; |
| 11 | import { codexModelFamily } from "@t3tools/shared/model"; |
| 12 | import { compareSemverVersions, parseSemver } from "@t3tools/shared/semver"; |
| 13 | import * as Option from "effect/Option"; |
| 14 | import * as Schema from "effect/Schema"; |
| 15 | import { TrimmedNonEmptyString } from "@t3tools/contracts"; |
| 16 | |
| 17 | /** Dated Codex builds match their base model's catalog entry. */ |
| 18 | const CODEX_MATCHING: ModelCatalog.CatalogMatching = { family: codexModelFamily }; |
| 19 | |
| 20 | const CodexModelAdapter = Schema.Struct({ |
| 21 | codex: Schema.optional(Schema.Struct({ minVersion: Schema.optional(TrimmedNonEmptyString) })), |
| 22 | }); |
| 23 | const 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 | */ |
| 31 | function 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. */ |
| 56 | export 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 | } |