408 lines · 15.2 KB · TypeScript
RawDownload
/** * ModelManifest — remote provider-model metadata with a bundled offline * fallback. * * Provider catalogs and legacy classification live in `model-manifest.json`. * The bundled copy ships with every release; at runtime the service refreshes * it from the same file on `main`. Preference order is remote, then the last * successful on-disk copy, then the bundle. A failed fetch never fails a * provider check. * * Providers with authoritative discovery can use only the classification * overlay. Providers with static catalogs can resolve presentation and * capabilities from `providers`, then decode their own allowlisted adapter * payload separately. */import {  ModelCapabilities,  TrimmedNonEmptyString,  type ProviderDriverKind,  type ServerProviderModel,} from "@t3tools/contracts";import * as Clock from "effect/Clock";import * as Context from "effect/Context";import * as Effect from "effect/Effect";import * as FileSystem from "effect/FileSystem";import * as Layer from "effect/Layer";import * as Path from "effect/Path";import * as Schema from "effect/Schema";import * as Semaphore from "effect/Semaphore";import { HttpClient, HttpClientResponse } from "effect/unstable/http"; import { ServerConfig } from "../config.ts";import * as ServerSettings from "../serverSettings.ts";import { hasValidClaudeManifestAdapters } from "./ClaudeModelManifest.ts";import bundledManifestJson from "./model-manifest.json" with { type: "json" };import type { ServerProviderDraft } from "./providerSnapshot.ts"; const MODEL_MANIFEST_URL =  "https://raw.githubusercontent.com/pingdotgg/t3code/main/apps/server/src/provider/model-manifest.json"; /** How long a fetched manifest stays fresh before the next probe re-fetches. */const MANIFEST_TTL_MS = 60 * 60 * 1000; /** Minimum gap between fetch attempts after a failure, so an offline server * does not pay a network timeout on every provider check. */const MANIFEST_RETRY_MS = 5 * 60 * 1000; const FETCH_TIMEOUT_MS = 10_000; const ManifestModelStatus = Schema.Literals(["current", "legacy"]); const ManifestModelProfile = Schema.Struct({  capabilities: Schema.optional(ModelCapabilities),  adapter: Schema.optional(Schema.Unknown),}); const ManifestProviderModel = Schema.Struct({  slug: TrimmedNonEmptyString,  name: TrimmedNonEmptyString,  shortName: Schema.optional(TrimmedNonEmptyString),  subProvider: Schema.optional(TrimmedNonEmptyString),  aliases: Schema.optional(Schema.Array(TrimmedNonEmptyString)),  status: ManifestModelStatus,  badge: Schema.optional(Schema.Literal("new")),  profile: Schema.optional(TrimmedNonEmptyString),  adapter: Schema.optional(Schema.Unknown),}); const ManifestProviderCatalog = Schema.Struct({  defaults: Schema.optional(    Schema.Struct({      chat: Schema.optional(TrimmedNonEmptyString),    }),  ),  profiles: Schema.Record(Schema.String, ManifestModelProfile),  models: Schema.Array(ManifestProviderModel),}); /** * `version` gates breaking schema changes. Provider catalogs are additive so * clients that only understand `currentModels` keep accepting this v1 file. */const ModelManifestEnvelopeSchema = Schema.Struct({  version: Schema.Literal(1),  /**   * ISO date of the last edit. A release bundles its manifest, and a disk   * cache of an older edit must not outrank it. Optional so older remote   * files still decode; they count as older than any dated bundle.   */  updatedAt: Schema.optional(Schema.String),  currentModels: Schema.Record(Schema.String, Schema.Array(Schema.String)),  providers: Schema.optional(Schema.Record(Schema.String, ManifestProviderCatalog)),}); const hasValidProviderCatalogReferences = (  manifest: typeof ModelManifestEnvelopeSchema.Type,): boolean =>  Object.values(manifest.providers ?? {}).every((catalog) => {    const slugs = new Set<string>();    const modelsAreValid = catalog.models.every((model) => {      if (slugs.has(model.slug)) return false;      slugs.add(model.slug);      return model.profile === undefined || catalog.profiles[model.profile] !== undefined;    });    return (      modelsAreValid && (catalog.defaults?.chat === undefined || slugs.has(catalog.defaults.chat))    );  }); const ModelManifestSchema = ModelManifestEnvelopeSchema.pipe(  Schema.check(    Schema.makeFilter(hasValidProviderCatalogReferences, {      expected: "unique model slugs and existing model and profile references",    }),    Schema.makeFilter(hasValidClaudeManifestAdapters, {      expected: "valid Claude adapter metadata",    }),  ),);export type ModelManifestData = typeof ModelManifestSchema.Type; export interface ResolvedManifestModel {  readonly model: ServerProviderModel;  readonly adapter: unknown;  readonly profileAdapter: unknown;} export interface ResolvedProviderCatalog {  readonly models: ReadonlyArray<ResolvedManifestModel>;  readonly defaults: {    readonly chat: string | undefined;  };} const decodeManifest = Schema.decodeUnknownEffect(ModelManifestSchema); export const BUNDLED_MODEL_MANIFEST: ModelManifestData =  Schema.decodeUnknownSync(ModelManifestSchema)(bundledManifestJson); /** Epoch millis of the manifest's `updatedAt`, or 0 when absent or unparsable. */function manifestUpdatedAtMs(manifest: ModelManifestData): number {  if (manifest.updatedAt === undefined) return 0;  const parsed = Date.parse(manifest.updatedAt);  return Number.isNaN(parsed) ? 0 : parsed;} /** Resolve provider-neutral model presentation and capability data. */export function resolveProviderCatalog(  manifest: ModelManifestData,  driverKind: ProviderDriverKind,): ResolvedProviderCatalog | null {  const catalog = manifest.providers?.[driverKind];  if (!catalog) return null;   const seen = new Set<string>();  const models: Array<ResolvedManifestModel> = [];  for (const entry of catalog.models) {    if (seen.has(entry.slug)) return null;    seen.add(entry.slug);     const profile = entry.profile ? catalog.profiles[entry.profile] : undefined;    if (entry.profile && !profile) return null;     models.push({      model: {        slug: entry.slug,        name: entry.name,        ...(entry.shortName ? { shortName: entry.shortName } : {}),        ...(entry.subProvider ? { subProvider: entry.subProvider } : {}),        ...(entry.aliases ? { aliases: entry.aliases } : {}),        ...(entry.badge ? { badge: entry.badge } : {}),        isCustom: false,        ...(catalog.defaults?.chat === entry.slug ? { isDefault: true } : {}),        ...(entry.status === "legacy" ? { isLegacy: true } : {}),        capabilities: profile?.capabilities ?? null,      },      adapter: entry.adapter,      profileAdapter: profile?.adapter,    });  }   if (catalog.defaults?.chat !== undefined && !seen.has(catalog.defaults.chat)) return null;   return {    models,    defaults: {      chat: catalog.defaults?.chat,    },  };} /** On-disk shape of the last successfully fetched manifest. */const ManifestCacheFile = Schema.Struct({  fetchedAtMs: Schema.Number,  manifest: ModelManifestSchema,});const decodeManifestCache = Schema.decodeUnknownEffect(  Schema.fromJsonString(    ManifestCacheFile as unknown as Schema.Codec<typeof ManifestCacheFile.Type>,  ),);/** Exported for tests that seed the disk cache. */export const encodeManifestCache = Schema.encodeEffect(  Schema.fromJsonString(    ManifestCacheFile as unknown as Schema.Codec<typeof ManifestCacheFile.Type>,  ),); /** True when the manifest classifies `slug` as legacy for `driverKind`. */function isLegacyModel(  manifest: ModelManifestData,  driverKind: ProviderDriverKind,  slug: string,): boolean {  const catalogModel = manifest.providers?.[driverKind]?.models.find(    (model) => model.slug === slug,  );  if (catalogModel) return catalogModel.status === "legacy";  const currentModels = manifest.currentModels[driverKind];  if (!currentModels) return false;  return !currentModels.includes(slug);} /** * Reclassifies every built-in model on a snapshot draft against the manifest. * Custom models are user-defined and never reclassified. */export function applyModelManifest(  draft: ServerProviderDraft,  manifest: ModelManifestData,  driverKind: ProviderDriverKind,): ServerProviderDraft {  return {    ...draft,    models: applyManifestDefault(      classifyModels(draft.models, manifest, driverKind),      manifest,      driverKind,    ),  };} /** The manifest's chat default for `driverKind`, when it names one. */export function manifestDefaultModel(  manifest: ModelManifestData,  driverKind: ProviderDriverKind,): string | undefined {  return manifest.providers?.[driverKind]?.defaults?.chat;} /** * Moves `isDefault` to the manifest's chat default when the catalog carries * it. Providers that learn their default from the runtime (Antigravity takes * Google's current model) can be overridden here without a release. Aliases * that pointed at the old default move with the flag so the shared * "provider default" alias keeps resolving. */export function applyManifestDefault(  models: ReadonlyArray<ServerProviderModel>,  manifest: ModelManifestData,  driverKind: ProviderDriverKind,): ReadonlyArray<ServerProviderModel> {  const slug = manifestDefaultModel(manifest, driverKind);  if (slug === undefined || !models.some((model) => model.slug === slug)) return models;  const previous = models.find((model) => model.isDefault && model.slug !== slug);  if (!previous) return models;  const movedAliases = previous.aliases ?? [];  return models.map((model) => {    if (model.slug === previous.slug) {      const { isDefault: _isDefault, aliases: _aliases, ...rest } = model;      return rest;    }    if (model.slug === slug) {      const aliases = [...new Set([...(model.aliases ?? []), ...movedAliases])];      return { ...model, isDefault: true, ...(aliases.length > 0 ? { aliases } : {}) };    }    return model;  });} /** Model-level half of `applyModelManifest`, exported for focused tests. */export function classifyModels(  models: ReadonlyArray<ServerProviderModel>,  manifest: ModelManifestData,  driverKind: ProviderDriverKind,): ReadonlyArray<ServerProviderModel> {  return models.map((model) => {    if (model.isCustom) return model;    if (isLegacyModel(manifest, driverKind, model.slug)) {      return model.isLegacy ? model : { ...model, isLegacy: true };    }    if (!model.isLegacy) return model;    const { isLegacy: _isLegacy, ...rest } = model;    return rest;  });} export class ModelManifest extends Context.Service<  ModelManifest,  {    /** Manifest already in memory (disk cache or bundle); never fetches.     * Snapshot classification reads this, so it never waits on the network. */    readonly current: Effect.Effect<ModelManifestData>;    /** Manifest after a TTL-gated remote refresh; never fails. */    readonly refresh: Effect.Effect<ModelManifestData>;    /** Forks `refresh` into the service's own scope. Drivers call this from     * provider checks: the fetch is process-shared state, so it must survive     * the teardown of whichever instance happened to trigger it. */    readonly refreshInBackground: Effect.Effect<void>;  }>()("t3/provider/ModelManifest") {} /** Constant service backing the bundled-data test layer. */const BundledOnlyModelManifest: ModelManifest["Service"] = {  current: Effect.succeed(BUNDLED_MODEL_MANIFEST),  refresh: Effect.succeed(BUNDLED_MODEL_MANIFEST),  refreshInBackground: Effect.void,}; export const layerTest = Layer.succeed(ModelManifest, BundledOnlyModelManifest); export const make = Effect.gen(function* () {  const fileSystem = yield* FileSystem.FileSystem;  const path = yield* Path.Path;  const config = yield* ServerConfig;  const settingsService = yield* ServerSettings.ServerSettingsService;  const httpClient = yield* HttpClient.HttpClient;  const serviceScope = yield* Effect.scope;   const cachePath = path.join(config.stateDir, "model-manifest.json");  let manifest = BUNDLED_MODEL_MANIFEST;  let fetchedAtMs: number | null = null;  let lastAttemptMs: number | null = null;  const refreshSemaphore = yield* Semaphore.make(1);   // `Effect.cached` makes concurrent first readers await the same disk load  // rather than racing a "loaded" flag. Only `refreshed` takes the fetch  // semaphore; `current` must never wait behind an in-flight network refresh.  const ensureDiskCacheLoaded = yield* Effect.cached(    Effect.gen(function* () {      const fromDisk = yield* fileSystem.readFileString(cachePath).pipe(        Effect.flatMap((raw) => decodeManifestCache(raw)),        Effect.catchCause(() => Effect.succeed(null)),      );      if (fromDisk === null) return;      // The disk copy is the last-seen remote manifest, so it outranks the      // bundle even when stale, unless the bundle's own edit date is newer      // than the cached manifest's. Then the release carries data the cache      // has not seen and the cache is dropped so the next refresh replaces      // it. Comparing edit dates, not fetch time, keeps this independent of      // when the cache was written relative to the release.      if (manifestUpdatedAtMs(BUNDLED_MODEL_MANIFEST) > manifestUpdatedAtMs(fromDisk.manifest)) {        return;      }      manifest = fromDisk.manifest;      fetchedAtMs = fromDisk.fetchedAtMs;    }),  );   const refresh = Effect.fn("ModelManifest.refresh")(function* () {    yield* ensureDiskCacheLoaded;    const now = yield* Clock.currentTimeMillis;    // A timestamp in the future means the wall clock moved backwards (the    // disk cache crosses restarts, so monotonic time cannot cover it). Treat    // it as expired: the refetch rewrites both timestamps and self-heals.    const isWithin = (sinceMs: number | null, windowMs: number) =>      sinceMs !== null && now >= sinceMs && now - sinceMs < windowMs;    if (isWithin(fetchedAtMs, MANIFEST_TTL_MS)) return manifest;    if (isWithin(lastAttemptMs, MANIFEST_RETRY_MS)) return manifest;     // The same switch that gates provider CLI update checks. It stops network    // fetches only: a manifest already cached on disk from an earlier fetch    // stays in effect, since the setting is about phoning home, not about    // discarding data the server already holds.    const settings = yield* settingsService.getSettings.pipe(      Effect.catchCause(() => Effect.succeed(null)),    );    if (settings !== null && !settings.enableProviderUpdateChecks) return manifest;     lastAttemptMs = now;    const fetched = yield* httpClient.get(MODEL_MANIFEST_URL).pipe(      Effect.flatMap(HttpClientResponse.filterStatusOk),      Effect.flatMap((response) => response.json),      Effect.flatMap((json) => decodeManifest(json)),      Effect.timeout(FETCH_TIMEOUT_MS),      Effect.catchCause(() => Effect.succeed(null)),    );    if (fetched === null) return manifest;     manifest = fetched;    fetchedAtMs = now;    yield* encodeManifestCache({ fetchedAtMs: now, manifest: fetched }).pipe(      Effect.flatMap((serialized) => fileSystem.writeFileString(cachePath, serialized)),      Effect.catchCause(() => Effect.void),    );    return manifest;  });   const guardedRefresh = refreshSemaphore.withPermits(1)(refresh());   return ModelManifest.of({    current: ensureDiskCacheLoaded.pipe(Effect.map(() => manifest)),    refresh: guardedRefresh,    refreshInBackground: Effect.forkIn(guardedRefresh, serviceScope).pipe(Effect.asVoid),  });}); export const layer = Layer.effect(ModelManifest, make);