@shop-os/modelkit 0.1.11 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -9,7 +9,27 @@ header in the release PR.
9
9
 
10
10
  ## [Unreleased]
11
11
 
12
- ## [0.1.11] - unreleased (publish pending)
12
+ ## [0.2.0] - 2026-09-14
13
+
14
+ ### Added
15
+ - `CatalogModelSchema` and `CatalogSchema` (`src/catalog/schema.ts`): Zod schema mirroring
16
+ `CatalogModel` field for field, tolerating unknown keys. Active rows need `display`, active
17
+ text rows need `capabilities.contextWindow`, `replacedBy` must not equal `id`, and ids and
18
+ aliases stay globally unique. `MODELS` passes it.
19
+ - `liveCatalog` (`src/catalog/live.ts`): the shared live catalog, starting as `MODELS`.
20
+ - `primeCatalog` and `resetLiveCatalog` (`src/catalog/load.ts`): fetch, validate and swap the
21
+ live catalog from `GET /models/catalog`, with `If-None-Match`, a `CatalogCache` last-good
22
+ fallback, the `MODELS` snapshot fallback, and a background refresh every `ttlMs`. Never
23
+ throws. Exported from both the node entry and `@shop-os/modelkit/catalog`.
24
+ - `fileCatalogCache` (`src/catalog/file-cache.ts`, node entry only): file-backed last-good
25
+ store, written atomically via `path + ".tmp"` plus rename.
26
+ - `Catalog.replace(rows)`: atomic swap of the id and alias maps.
27
+
28
+ ### Changed
29
+ - `createModelKit` reads `liveCatalog` when no `catalogOverride` is given, so a primed catalog
30
+ reaches an existing kit without a rebuild.
31
+
32
+ ## [0.1.11] - 2026-09-13
13
33
 
14
34
  ### Added
15
35
  - `display.logo` on every catalog row except `mmaudio` and `bg-remove`: text rows carry the
@@ -189,8 +209,9 @@ First publish, renamed from `@shop-os/model-sdk`.
189
209
  - Keys per tier (pro, enterprise with per-provider fallback); one retry classifier, one error
190
210
  type, one usage event with the canonical id.
191
211
 
192
- [Unreleased]: https://github.com/Shop-OS/shopos-backend-ts/compare/modelkit-0.1.11...dev
193
- [0.1.11]: https://github.com/Shop-OS/shopos-backend-ts/compare/f0981d9b9...dev
212
+ [Unreleased]: https://github.com/Shop-OS/shopos-backend-ts/compare/modelkit-0.2.0...dev
213
+ [0.2.0]: https://github.com/Shop-OS/shopos-backend-ts/compare/modelkit-0.1.11...modelkit-0.2.0
214
+ [0.1.11]: https://github.com/Shop-OS/shopos-backend-ts/compare/f0981d9b9...bb3b16354
194
215
  [0.1.10]: https://github.com/Shop-OS/shopos-backend-ts/compare/f17e822e2...f0981d9b9
195
216
  [0.1.9]: https://github.com/Shop-OS/shopos-backend-ts/compare/1958a5615...dev
196
217
  [0.1.8]: https://github.com/Shop-OS/shopos-backend-ts/commit/9661a109d
package/dist/catalog.cjs CHANGED
@@ -20,7 +20,10 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
20
20
  // src/catalog-entry.ts
21
21
  var catalog_entry_exports = {};
22
22
  __export(catalog_entry_exports, {
23
+ ADAPTER_IDS: () => ADAPTER_IDS,
23
24
  Catalog: () => Catalog,
25
+ CatalogModelSchema: () => CatalogModelSchema,
26
+ CatalogSchema: () => CatalogSchema,
24
27
  IMAGE_MODELS: () => IMAGE_MODELS,
25
28
  MODELS: () => MODELS,
26
29
  ModelKitError: () => ModelKitError,
@@ -28,7 +31,10 @@ __export(catalog_entry_exports, {
28
31
  TEXT_MODELS: () => TEXT_MODELS,
29
32
  VIDEO_MODELS: () => VIDEO_MODELS,
30
33
  isModelKitError: () => isModelKitError,
34
+ liveCatalog: () => liveCatalog,
31
35
  nearestAspectRatio: () => nearestAspectRatio,
36
+ primeCatalog: () => primeCatalog,
37
+ resetLiveCatalog: () => resetLiveCatalog,
32
38
  resolveAudioOptions: () => resolveAudioOptions,
33
39
  resolveImageOptions: () => resolveImageOptions,
34
40
  resolveOptions: () => resolveOptions,
@@ -131,6 +137,14 @@ var Catalog = class _Catalog {
131
137
  ...rows
132
138
  ]);
133
139
  }
140
+ /** Atomically swap every row. Builds the maps first so a bad batch throws before anything changes. */
141
+ replace(rows) {
142
+ const next = new _Catalog(rows);
143
+ this.byId.clear();
144
+ this.byAlias.clear();
145
+ for (const [id, model] of next.byId) this.byId.set(id, model);
146
+ for (const [alias, id] of next.byAlias) this.byAlias.set(alias, id);
147
+ }
134
148
  };
135
149
 
136
150
  // src/catalog/image.ts
@@ -1821,6 +1835,387 @@ var MODELS = [
1821
1835
  ...OTHER_MODELS
1822
1836
  ];
1823
1837
 
1838
+ // src/catalog/schema.ts
1839
+ var import_zod = require("zod");
1840
+ var ADAPTER_IDS = [
1841
+ "ai-sdk-text",
1842
+ "google-image",
1843
+ "openai-image",
1844
+ "google-genai",
1845
+ "ark",
1846
+ "fal",
1847
+ "replicate",
1848
+ "prodia",
1849
+ "higgsfield",
1850
+ "xai-video"
1851
+ ];
1852
+ var ModelTypeSchema = import_zod.z.enum(["text", "image", "video", "audio", "upscale", "bg-remove"]);
1853
+ var LifecycleSchema = import_zod.z.enum(["active", "deprecated", "retired"]);
1854
+ var AdapterSchema = import_zod.z.enum(ADAPTER_IDS);
1855
+ var AspectRatioSchema = import_zod.z.enum([
1856
+ "1:1",
1857
+ "16:9",
1858
+ "9:16",
1859
+ "4:3",
1860
+ "3:4",
1861
+ "3:2",
1862
+ "2:3",
1863
+ "4:5",
1864
+ "5:4",
1865
+ "21:9",
1866
+ "9:21",
1867
+ "97:30",
1868
+ "adaptive"
1869
+ ]);
1870
+ var ImageSizeSchema = import_zod.z.enum(["512", "1K", "2K", "4K"]);
1871
+ var VideoResolutionSchema = import_zod.z.enum(["360p", "480p", "720p", "768p", "1080p", "2k", "4k"]);
1872
+ var ImageQualitySchema = import_zod.z.enum(["auto", "low", "medium", "high", "xhigh", "max"]);
1873
+ var OutputFormatSchema = import_zod.z.enum(["png", "jpeg", "webp"]);
1874
+ var RequiredInputSchema = import_zod.z.enum(["firstFrame", "sourceVideo", "references"]);
1875
+ var SupportedOptionSchema = import_zod.z.enum([
1876
+ "seed",
1877
+ "quality",
1878
+ "negativePrompt",
1879
+ "cfgScale",
1880
+ "generateAudio",
1881
+ "promptExpansion",
1882
+ "lastFrame",
1883
+ "count",
1884
+ "outputFormat",
1885
+ "scaleFactor",
1886
+ "safetySettings",
1887
+ "temperature",
1888
+ "upscale",
1889
+ "watermark",
1890
+ "safetyChecker",
1891
+ "instrumental"
1892
+ ]);
1893
+ var UsageUnitSchema = import_zod.z.enum([
1894
+ "tokens",
1895
+ "images",
1896
+ "seconds",
1897
+ "reported_cost_usd",
1898
+ "compute_seconds"
1899
+ ]);
1900
+ var PricingKindSchema = import_zod.z.enum(["token", "image_size", "video_per_second", "flat"]);
1901
+ var ProviderOptionValueSchema = import_zod.z.lazy(
1902
+ () => import_zod.z.union([
1903
+ import_zod.z.string(),
1904
+ import_zod.z.number(),
1905
+ import_zod.z.boolean(),
1906
+ import_zod.z.array(ProviderOptionValueSchema),
1907
+ import_zod.z.record(import_zod.z.string(), ProviderOptionValueSchema)
1908
+ ])
1909
+ );
1910
+ var ProviderOptionsSchema = import_zod.z.record(import_zod.z.string(), import_zod.z.record(import_zod.z.string(), ProviderOptionValueSchema));
1911
+ var ReferenceLimitsSchema = import_zod.z.looseObject({
1912
+ images: import_zod.z.number().optional(),
1913
+ videos: import_zod.z.number().optional(),
1914
+ audios: import_zod.z.number().optional(),
1915
+ total: import_zod.z.number().optional(),
1916
+ audioOnly: import_zod.z.boolean().optional()
1917
+ });
1918
+ var ModelConstraintsSchema = import_zod.z.looseObject({
1919
+ aspectRatios: import_zod.z.array(AspectRatioSchema).optional(),
1920
+ sizes: import_zod.z.array(ImageSizeSchema).optional(),
1921
+ resolutions: import_zod.z.array(VideoResolutionSchema).optional(),
1922
+ durations: import_zod.z.union([import_zod.z.looseObject({ min: import_zod.z.number(), max: import_zod.z.number() }), import_zod.z.array(import_zod.z.number())]).optional(),
1923
+ durationPresets: import_zod.z.array(import_zod.z.number()).optional(),
1924
+ qualityOptions: import_zod.z.array(import_zod.z.string()).optional(),
1925
+ exactSizes: import_zod.z.record(import_zod.z.string(), import_zod.z.string()).optional(),
1926
+ references: ReferenceLimitsSchema.optional(),
1927
+ requires: import_zod.z.array(RequiredInputSchema).optional(),
1928
+ supports: import_zod.z.array(SupportedOptionSchema).optional(),
1929
+ ratioIgnoredWith: import_zod.z.array(import_zod.z.enum(["firstFrame", "sourceVideo"])).optional()
1930
+ });
1931
+ var MediaRefSchema = import_zod.z.union([
1932
+ import_zod.z.looseObject({ url: import_zod.z.string(), label: import_zod.z.string().optional() }),
1933
+ import_zod.z.looseObject({
1934
+ bytes: import_zod.z.instanceof(Uint8Array),
1935
+ mime: import_zod.z.string(),
1936
+ label: import_zod.z.string().optional()
1937
+ })
1938
+ ]);
1939
+ var ImageSafetySettingSchema = import_zod.z.looseObject({ category: import_zod.z.string(), threshold: import_zod.z.string() });
1940
+ var UpscaleOptionsSchema = import_zod.z.looseObject({
1941
+ enhanceModel: import_zod.z.string().optional(),
1942
+ subjectDetection: import_zod.z.string().optional(),
1943
+ faceEnhancement: import_zod.z.boolean().optional(),
1944
+ faceEnhancementCreativity: import_zod.z.number().optional(),
1945
+ faceEnhancementStrength: import_zod.z.number().optional()
1946
+ });
1947
+ var DefaultsSchema = import_zod.z.looseObject({
1948
+ references: import_zod.z.array(MediaRefSchema).optional(),
1949
+ aspectRatio: AspectRatioSchema.optional(),
1950
+ size: ImageSizeSchema.optional(),
1951
+ exactSize: import_zod.z.string().optional(),
1952
+ quality: ImageQualitySchema.optional(),
1953
+ seed: import_zod.z.number().optional(),
1954
+ count: import_zod.z.number().optional(),
1955
+ outputFormat: OutputFormatSchema.optional(),
1956
+ scaleFactor: import_zod.z.number().optional(),
1957
+ safetySettings: import_zod.z.array(ImageSafetySettingSchema).optional(),
1958
+ temperature: import_zod.z.number().optional(),
1959
+ upscale: UpscaleOptionsSchema.optional(),
1960
+ firstFrame: MediaRefSchema.optional(),
1961
+ lastFrame: MediaRefSchema.optional(),
1962
+ sourceVideo: MediaRefSchema.optional(),
1963
+ referenceImages: import_zod.z.array(MediaRefSchema).optional(),
1964
+ referenceVideos: import_zod.z.array(MediaRefSchema).optional(),
1965
+ referenceAudios: import_zod.z.array(MediaRefSchema).optional(),
1966
+ resolution: VideoResolutionSchema.optional(),
1967
+ durationSeconds: import_zod.z.number().optional(),
1968
+ generateAudio: import_zod.z.boolean().optional(),
1969
+ negativePrompt: import_zod.z.string().optional(),
1970
+ cfgScale: import_zod.z.number().optional(),
1971
+ promptExpansion: import_zod.z.enum(["fast", "balanced", "quality"]).optional(),
1972
+ watermark: import_zod.z.boolean().optional(),
1973
+ safetyChecker: import_zod.z.boolean().optional(),
1974
+ mode: import_zod.z.enum(["standard", "pro"]).optional(),
1975
+ instrumental: import_zod.z.boolean().optional()
1976
+ });
1977
+ var CatalogDisplaySchema = import_zod.z.looseObject({
1978
+ provider: import_zod.z.string(),
1979
+ description: import_zod.z.string(),
1980
+ logo: import_zod.z.string().optional(),
1981
+ costTier: import_zod.z.union([import_zod.z.literal(1), import_zod.z.literal(2), import_zod.z.literal(3)]).optional(),
1982
+ recommended: import_zod.z.boolean().optional(),
1983
+ sortOrder: import_zod.z.number().optional(),
1984
+ chat: import_zod.z.looseObject({
1985
+ providerOptions: ProviderOptionsSchema.optional(),
1986
+ temperature: import_zod.z.number().optional()
1987
+ }).optional()
1988
+ });
1989
+ var CatalogCapabilitiesSchema = import_zod.z.looseObject({
1990
+ acceptsSourceVideo: import_zod.z.boolean().optional(),
1991
+ framesExcludeReferences: import_zod.z.boolean().optional(),
1992
+ reasoning: import_zod.z.boolean().optional(),
1993
+ vision: import_zod.z.boolean().optional(),
1994
+ contextWindow: import_zod.z.number().optional()
1995
+ });
1996
+ var CatalogModelSchemaBase = import_zod.z.looseObject({
1997
+ id: import_zod.z.string().min(1),
1998
+ aliases: import_zod.z.array(import_zod.z.string()).default([]),
1999
+ label: import_zod.z.string().min(1),
2000
+ type: ModelTypeSchema,
2001
+ adapter: AdapterSchema,
2002
+ providerModelId: import_zod.z.string().min(1),
2003
+ lifecycle: LifecycleSchema,
2004
+ replacedBy: import_zod.z.string().optional(),
2005
+ exposure: import_zod.z.enum(["catalog", "internal"]),
2006
+ constraints: ModelConstraintsSchema,
2007
+ defaults: DefaultsSchema,
2008
+ providerOptions: ProviderOptionsSchema.optional(),
2009
+ usage: import_zod.z.looseObject({
2010
+ unit: UsageUnitSchema,
2011
+ slug: import_zod.z.string().optional(),
2012
+ providerLabel: import_zod.z.string().optional()
2013
+ }),
2014
+ pricingKind: PricingKindSchema,
2015
+ display: CatalogDisplaySchema.optional(),
2016
+ capabilities: CatalogCapabilitiesSchema.optional()
2017
+ }).superRefine((row, ctx) => {
2018
+ if (row.lifecycle === "active" && !row.display) {
2019
+ ctx.addIssue({
2020
+ code: "custom",
2021
+ path: ["display"],
2022
+ message: "active rows need display"
2023
+ });
2024
+ }
2025
+ if (row.lifecycle === "active" && row.type === "text" && !row.capabilities?.contextWindow) {
2026
+ ctx.addIssue({
2027
+ code: "custom",
2028
+ path: ["capabilities", "contextWindow"],
2029
+ message: "active text rows need contextWindow"
2030
+ });
2031
+ }
2032
+ if (row.replacedBy !== void 0 && row.replacedBy === row.id) {
2033
+ ctx.addIssue({
2034
+ code: "custom",
2035
+ path: ["replacedBy"],
2036
+ message: "replacedBy must not equal id"
2037
+ });
2038
+ }
2039
+ });
2040
+ var CatalogModelSchema = CatalogModelSchemaBase;
2041
+ var CatalogSchema = import_zod.z.array(CatalogModelSchemaBase).superRefine((rows, ctx) => {
2042
+ const seenIds = /* @__PURE__ */ new Map();
2043
+ rows.forEach((row, index) => {
2044
+ if (seenIds.has(row.id)) {
2045
+ ctx.addIssue({
2046
+ code: "custom",
2047
+ path: [index, "id"],
2048
+ message: `Duplicate model id "${row.id}"`
2049
+ });
2050
+ } else {
2051
+ seenIds.set(row.id, index);
2052
+ }
2053
+ });
2054
+ const seenAliases = /* @__PURE__ */ new Map();
2055
+ rows.forEach((row, index) => {
2056
+ row.aliases.forEach((alias, aliasIndex) => {
2057
+ const path = [index, "aliases", aliasIndex];
2058
+ if (alias === row.id || seenIds.has(alias)) {
2059
+ ctx.addIssue({
2060
+ code: "custom",
2061
+ path,
2062
+ message: `Alias "${alias}" collides with a model id`
2063
+ });
2064
+ return;
2065
+ }
2066
+ if (seenAliases.has(alias)) {
2067
+ ctx.addIssue({
2068
+ code: "custom",
2069
+ path,
2070
+ message: `Duplicate alias "${alias}"`
2071
+ });
2072
+ } else {
2073
+ seenAliases.set(alias, index);
2074
+ }
2075
+ });
2076
+ });
2077
+ });
2078
+
2079
+ // src/catalog/live.ts
2080
+ var liveCatalog = new Catalog(MODELS);
2081
+
2082
+ // src/catalog/load.ts
2083
+ var memo;
2084
+ var timer;
2085
+ var revision;
2086
+ var origin = "snapshot";
2087
+ function rowCount() {
2088
+ return liveCatalog.list({ lifecycle: ["active", "deprecated", "retired"] }).length;
2089
+ }
2090
+ function authHeaders(auth) {
2091
+ return "bearer" in auth ? { Authorization: `Bearer ${auth.bearer}` } : { "x-api-key": auth.apiKey };
2092
+ }
2093
+ function describeError(err) {
2094
+ return err instanceof Error ? err.message : String(err);
2095
+ }
2096
+ function parseEnvelope(json) {
2097
+ if (typeof json !== "object" || json === null || !("rows" in json)) {
2098
+ throw new Error("invalid catalog response: missing rows");
2099
+ }
2100
+ const rows = CatalogSchema.parse(json.rows);
2101
+ const rev = json.revision;
2102
+ if (typeof rev === "string") return { rows, revision: rev };
2103
+ return { rows };
2104
+ }
2105
+ function logPrimed(logger, result) {
2106
+ const { origin: loggedOrigin, rowCount: loggedCount, revision: loggedRevision } = result;
2107
+ logger.info("[modelkit] catalog primed", {
2108
+ origin: loggedOrigin,
2109
+ rowCount: loggedCount,
2110
+ revision: loggedRevision
2111
+ });
2112
+ }
2113
+ async function fetchRemote(source, logger) {
2114
+ const fetchImpl = source.fetch ?? globalThis.fetch;
2115
+ const headers = authHeaders(source.auth);
2116
+ if (revision !== void 0) headers["If-None-Match"] = `"${revision}"`;
2117
+ const res = await fetchImpl(`${source.baseUrl}/models/catalog`, {
2118
+ headers,
2119
+ signal: AbortSignal.timeout(source.timeoutMs ?? 1e4)
2120
+ });
2121
+ if (res.status === 304) return { kept: true };
2122
+ if (!res.ok) throw new Error(`catalog fetch failed with status ${res.status}`);
2123
+ const json = await res.json();
2124
+ const { rows, revision: rev } = parseEnvelope(json);
2125
+ if (rev === void 0) return { rows, fromCache: false };
2126
+ return { rows, revision: rev, fromCache: false };
2127
+ }
2128
+ async function writeCache(source, logger, rows, rev) {
2129
+ if (!source.cache) return;
2130
+ try {
2131
+ await source.cache.set(JSON.stringify({ rows, revision: rev }));
2132
+ } catch (err) {
2133
+ logger.warn("[modelkit] catalog cache write failed", err);
2134
+ }
2135
+ }
2136
+ async function prime(source, logger) {
2137
+ try {
2138
+ const fetched = await fetchRemote(source, logger);
2139
+ if ("kept" in fetched) {
2140
+ const result2 = { origin, rowCount: rowCount() };
2141
+ if (revision !== void 0) result2.revision = revision;
2142
+ logPrimed(logger, result2);
2143
+ return result2;
2144
+ }
2145
+ liveCatalog.replace(fetched.rows);
2146
+ origin = "remote";
2147
+ revision = fetched.revision;
2148
+ await writeCache(source, logger, fetched.rows, fetched.revision);
2149
+ const result = { origin, rowCount: rowCount() };
2150
+ if (fetched.revision !== void 0) result.revision = fetched.revision;
2151
+ logPrimed(logger, result);
2152
+ return result;
2153
+ } catch (err) {
2154
+ logger.warn("[modelkit] catalog remote failed, trying cache", err);
2155
+ const error = describeError(err);
2156
+ try {
2157
+ const cached = await source.cache?.get();
2158
+ if (!cached) throw new Error("no cached catalog");
2159
+ const { rows, revision: rev } = parseEnvelope(JSON.parse(cached));
2160
+ liveCatalog.replace(rows);
2161
+ origin = "last-good";
2162
+ revision = rev;
2163
+ const result = { origin, rowCount: rowCount(), error };
2164
+ if (rev !== void 0) result.revision = rev;
2165
+ logPrimed(logger, result);
2166
+ return result;
2167
+ } catch (cacheErr) {
2168
+ logger.warn("[modelkit] catalog cache failed, using snapshot", cacheErr);
2169
+ liveCatalog.replace([...MODELS]);
2170
+ origin = "snapshot";
2171
+ revision = void 0;
2172
+ const result = {
2173
+ origin,
2174
+ rowCount: rowCount(),
2175
+ error
2176
+ };
2177
+ logPrimed(logger, result);
2178
+ return result;
2179
+ }
2180
+ }
2181
+ }
2182
+ async function refresh(source, logger) {
2183
+ try {
2184
+ const fetched = await fetchRemote(source, logger);
2185
+ if ("kept" in fetched) return;
2186
+ liveCatalog.replace(fetched.rows);
2187
+ origin = "remote";
2188
+ revision = fetched.revision;
2189
+ await writeCache(source, logger, fetched.rows, fetched.revision);
2190
+ } catch (err) {
2191
+ logger.warn("[modelkit] catalog refresh failed, keeping current rows", err);
2192
+ }
2193
+ }
2194
+ function primeCatalog(source) {
2195
+ if (memo) return memo;
2196
+ const logger = source.logger ?? console;
2197
+ memo = prime(source, logger).then((result) => {
2198
+ const ttlMs = source.ttlMs ?? 3e5;
2199
+ if (ttlMs > 0 && timer === void 0) {
2200
+ timer = setInterval(() => void refresh(source, logger), ttlMs);
2201
+ const maybeUnref = timer;
2202
+ maybeUnref.unref?.();
2203
+ }
2204
+ return result;
2205
+ });
2206
+ return memo;
2207
+ }
2208
+ function resetLiveCatalog() {
2209
+ if (timer !== void 0) {
2210
+ clearInterval(timer);
2211
+ timer = void 0;
2212
+ }
2213
+ memo = void 0;
2214
+ revision = void 0;
2215
+ origin = "snapshot";
2216
+ liveCatalog.replace([...MODELS]);
2217
+ }
2218
+
1824
2219
  // src/resolve-options.ts
1825
2220
  var SIZE_ORDER = ["512", "1K", "2K", "4K"];
1826
2221
  var RESOLUTION_ORDER = ["360p", "480p", "720p", "768p", "1080p", "2k", "4k"];