@shop-os/modelkit 0.2.0 → 0.2.3

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,6 +9,44 @@ header in the release PR.
9
9
 
10
10
  ## [Unreleased]
11
11
 
12
+ ## [0.2.3] - 2026-09-15
13
+
14
+ ### Changed
15
+ - `display.sortOrder` sorts descending: a higher number shows first, so a new model takes the next number up. No published row set it before.
16
+
17
+ ### Fixed
18
+ - `primeCatalog` treats a remote response with zero rows as a failure and keeps the last-good or snapshot rows, so a pro-backend whose catalog table is not seeded yet cannot empty every consumer's pickers.
19
+
20
+ ## [0.2.2] - 2026-09-15
21
+
22
+ Option audit against provider docs, limited to options a product surface sets.
23
+
24
+ ### Added
25
+ - Veo 3.1, 3.1 Fast and 3.1 Lite accept `seed` (sent as `config.seed`).
26
+ - Seedream 5.0 Pro accepts `outputFormat` png/jpeg (sent as `output_format`; webp is coerced to png with a warning).
27
+ - Gemini 3 image rows declare `references.images: 14`, the provider cap.
28
+
29
+ ### Fixed
30
+ - `liveCatalog` and the prime state are one process-wide singleton (on `globalThis`). The root and `./catalog` bundles each carried their own copy, so a server that primed through the root entry still served the snapshot to code importing `@shop-os/modelkit/catalog`.
31
+
32
+ ### Changed
33
+ - gpt-image-2.5-sunburst/flare `qualityOptions` add `xhigh` and `max`.
34
+ - Veo 3.1 Fast and Lite durations add 6 s. Veo 3.1 Lite drops `references` (not supported for Lite).
35
+ - Gemini Omni rows allow 3 reference videos (was 1).
36
+ - Seedance rows declare `ratioIgnoredWith: ["firstFrame", "sourceVideo"]` to match the adapter.
37
+ - kling-v3-omni-video aspect ratios are 16:9, 9:16, 1:1, adaptive (provider list).
38
+ - seedream-4.5 drops the 1K tier (provider minimum 2K). gemini-2.5-flash-image is 1K only. muse-image-1.0 drops 4:5 and 5:4.
39
+ - qwen-image-2-pro no longer requires a reference image.
40
+ - Prodia nano-banana rows drop `seed` (the adapter never sent it).
41
+
42
+ ## [0.2.1] - 2026-09-15
43
+
44
+ ### Added
45
+ - `gpt-image-2`, `gpt-image-2.5-sunburst` and `gpt-image-2.5-flare` declare `outputFormat`; the `openai-image` adapter sends `output_format` (png or jpeg) and labels the result mime to match.
46
+
47
+ ### Changed
48
+ - `outputFormat: "webp"` on GPT image models is coerced to png with an `option_ignored` warning: the API returns PNG bytes for webp (openai/openai-node#1850).
49
+
12
50
  ## [0.2.0] - 2026-09-14
13
51
 
14
52
  ### Added
@@ -210,6 +248,8 @@ First publish, renamed from `@shop-os/model-sdk`.
210
248
  type, one usage event with the canonical id.
211
249
 
212
250
  [Unreleased]: https://github.com/Shop-OS/shopos-backend-ts/compare/modelkit-0.2.0...dev
251
+ [0.2.2]: https://github.com/Shop-OS/shopos-backend-ts/compare/modelkit-0.2.1...modelkit-0.2.2
252
+ [0.2.1]: https://github.com/Shop-OS/shopos-backend-ts/compare/modelkit-0.2.0...modelkit-0.2.1
213
253
  [0.2.0]: https://github.com/Shop-OS/shopos-backend-ts/compare/modelkit-0.1.11...modelkit-0.2.0
214
254
  [0.1.11]: https://github.com/Shop-OS/shopos-backend-ts/compare/f0981d9b9...bb3b16354
215
255
  [0.1.10]: https://github.com/Shop-OS/shopos-backend-ts/compare/f17e822e2...f0981d9b9
package/dist/catalog.cjs CHANGED
@@ -32,6 +32,7 @@ __export(catalog_entry_exports, {
32
32
  VIDEO_MODELS: () => VIDEO_MODELS,
33
33
  isModelKitError: () => isModelKitError,
34
34
  liveCatalog: () => liveCatalog,
35
+ liveState: () => liveState,
35
36
  nearestAspectRatio: () => nearestAspectRatio,
36
37
  primeCatalog: () => primeCatalog,
37
38
  resetLiveCatalog: () => resetLiveCatalog,
@@ -162,7 +163,13 @@ var GEMINI_RATIOS = [
162
163
  "5:4",
163
164
  "adaptive"
164
165
  ];
165
- function geminiImage(id, label, aliases, sizes, display, exposure) {
166
+ function geminiImage(id, label, aliases, sizes, display, exposure, references) {
167
+ const constraints = {
168
+ aspectRatios: [...GEMINI_RATIOS],
169
+ sizes,
170
+ supports: ["safetySettings", "temperature"]
171
+ };
172
+ if (references !== void 0) constraints.references = references;
166
173
  const model = {
167
174
  id,
168
175
  aliases,
@@ -172,11 +179,7 @@ function geminiImage(id, label, aliases, sizes, display, exposure) {
172
179
  providerModelId: id,
173
180
  lifecycle: "active",
174
181
  exposure,
175
- constraints: {
176
- aspectRatios: [...GEMINI_RATIOS],
177
- sizes,
178
- supports: ["safetySettings", "temperature"]
179
- },
182
+ constraints,
180
183
  defaults: { size: "1K", aspectRatio: "1:1" },
181
184
  usage: { ...IMAGE_USAGE, slug: "gemini" },
182
185
  pricingKind: "image_size",
@@ -195,7 +198,8 @@ var IMAGE_MODELS = [
195
198
  description: "Create stunning, production-quality visuals with exceptional realism, detail, and typography. Best for campaigns, e-commerce, product photography, and hero creatives.",
196
199
  logo: "https://d31vvfpk2ppdyq.cloudfront.net/assets/image/192485d2-a76a-4657-9c06-930b2c70badd_compressed.png"
197
200
  },
198
- "catalog"
201
+ "catalog",
202
+ { images: 14 }
199
203
  ),
200
204
  geminiImage(
201
205
  "gemini-3.1-flash-image-preview",
@@ -208,7 +212,8 @@ var IMAGE_MODELS = [
208
212
  description: "Generate high-quality images at speed. Perfect for brainstorming, moodboards, concept testing, creative exploration, and large batch workflows.",
209
213
  logo: "https://d31vvfpk2ppdyq.cloudfront.net/assets/image/192485d2-a76a-4657-9c06-930b2c70badd_compressed.png"
210
214
  },
211
- "catalog"
215
+ "catalog",
216
+ { images: 14 }
212
217
  ),
213
218
  geminiImage(
214
219
  "gemini-3.1-flash-lite-image",
@@ -220,14 +225,15 @@ var IMAGE_MODELS = [
220
225
  description: "The fastest and most affordable image model. Ideal for rapid drafts, bulk exploration, and iterative concept testing at 1K resolution.",
221
226
  logo: "https://d31vvfpk2ppdyq.cloudfront.net/assets/image/192485d2-a76a-4657-9c06-930b2c70badd_compressed.png"
222
227
  },
223
- "catalog"
228
+ "catalog",
229
+ { images: 14 }
224
230
  ),
225
231
  // trigger-workflows validates 1K only; pricing seeds 2K/4K rows so allow all three.
226
232
  geminiImage(
227
233
  "gemini-2.5-flash-image",
228
234
  "Gemini 2.5 Flash Image",
229
235
  [],
230
- ["1K", "2K", "4K"],
236
+ ["1K"],
231
237
  {
232
238
  provider: "google",
233
239
  description: "Nano Banana (legacy Gemini 2.5 Flash Image) native image generation and editing model for high-volume, low-latency creative workflows."
@@ -258,7 +264,7 @@ var IMAGE_MODELS = [
258
264
  sizes: ["1K", "2K", "4K"],
259
265
  qualityOptions: ["auto", "low", "medium", "high"],
260
266
  references: { images: 16 },
261
- supports: ["quality", "count"]
267
+ supports: ["quality", "count", "outputFormat"]
262
268
  },
263
269
  defaults: { size: "1K", aspectRatio: "adaptive", quality: "auto" },
264
270
  usage: { ...IMAGE_USAGE, slug: "gpt" },
@@ -278,7 +284,7 @@ var IMAGE_MODELS = [
278
284
  adapter: "openai-image",
279
285
  providerModelId: "gpt-image-2.5-sunburst",
280
286
  lifecycle: "active",
281
- exposure: "internal",
287
+ exposure: "catalog",
282
288
  constraints: {
283
289
  exactSizes: {
284
290
  "1:1": "1024x1024",
@@ -291,15 +297,16 @@ var IMAGE_MODELS = [
291
297
  },
292
298
  aspectRatios: ["1:1", "3:2", "2:3", "16:9", "9:16", "4:5", "5:4", "adaptive"],
293
299
  sizes: ["1K", "2K", "4K"],
294
- qualityOptions: ["auto", "low", "medium", "high"],
300
+ qualityOptions: ["auto", "low", "medium", "high", "xhigh", "max"],
295
301
  references: { images: 16 },
296
- supports: ["quality", "count"]
302
+ supports: ["quality", "count", "outputFormat"]
297
303
  },
298
304
  defaults: { size: "1K", aspectRatio: "adaptive", quality: "auto" },
299
305
  usage: { ...IMAGE_USAGE },
300
306
  pricingKind: "image_size",
301
307
  display: {
302
308
  provider: "openai",
309
+ sortOrder: 20,
303
310
  description: "OpenAI's most capable GPT Image 2.5 tier, built for editing precision; takes text and image input and outputs images in low, medium, high, xhigh, or max quality."
304
311
  }
305
312
  },
@@ -312,7 +319,7 @@ var IMAGE_MODELS = [
312
319
  adapter: "openai-image",
313
320
  providerModelId: "gpt-image-2.5-flare",
314
321
  lifecycle: "active",
315
- exposure: "internal",
322
+ exposure: "catalog",
316
323
  constraints: {
317
324
  exactSizes: {
318
325
  "1:1": "1024x1024",
@@ -325,15 +332,16 @@ var IMAGE_MODELS = [
325
332
  },
326
333
  aspectRatios: ["1:1", "3:2", "2:3", "16:9", "9:16", "4:5", "5:4", "adaptive"],
327
334
  sizes: ["1K", "2K", "4K"],
328
- qualityOptions: ["auto", "low", "medium", "high"],
335
+ qualityOptions: ["auto", "low", "medium", "high", "xhigh", "max"],
329
336
  references: { images: 16 },
330
- supports: ["quality", "count"]
337
+ supports: ["quality", "count", "outputFormat"]
331
338
  },
332
339
  defaults: { size: "1K", aspectRatio: "adaptive", quality: "auto" },
333
340
  usage: { ...IMAGE_USAGE },
334
341
  pricingKind: "image_size",
335
342
  display: {
336
343
  provider: "openai",
344
+ sortOrder: 10,
337
345
  description: "OpenAI's fastest GPT Image 2.5 tier for high-quality everyday image generation and editing; takes text and image input and outputs images."
338
346
  }
339
347
  },
@@ -357,8 +365,6 @@ var IMAGE_MODELS = [
357
365
  "2:3",
358
366
  "3:4",
359
367
  "9:16",
360
- "4:5",
361
- "5:4",
362
368
  "9:21",
363
369
  "adaptive"
364
370
  ],
@@ -390,7 +396,7 @@ var IMAGE_MODELS = [
390
396
  references: { images: 10 },
391
397
  // Ark's Images API has no output-format, quality or count field; only
392
398
  // the seed is forwarded, so anything else must warn, not vanish.
393
- supports: ["seed"]
399
+ supports: ["seed", "outputFormat"]
394
400
  },
395
401
  defaults: { size: "1K", aspectRatio: "1:1" },
396
402
  usage: { ...IMAGE_USAGE },
@@ -426,7 +432,7 @@ var IMAGE_MODELS = [
426
432
  "21:9",
427
433
  "9:21"
428
434
  ],
429
- sizes: ["1K", "2K", "4K"],
435
+ sizes: ["2K", "4K"],
430
436
  references: { images: 10 },
431
437
  supports: ["seed"]
432
438
  },
@@ -478,7 +484,7 @@ var IMAGE_MODELS = [
478
484
  aspectRatios: ["1:1", "2:3", "3:2", "4:3", "3:4", "4:5", "5:4", "9:16", "16:9", "21:9"],
479
485
  sizes: ["1K", "2K", "4K"],
480
486
  requires: ["references"],
481
- supports: ["seed"]
487
+ supports: []
482
488
  },
483
489
  defaults: { size: "1K", aspectRatio: "1:1" },
484
490
  usage: { ...IMAGE_USAGE },
@@ -503,7 +509,7 @@ var IMAGE_MODELS = [
503
509
  aspectRatios: ["1:1", "2:3", "3:4", "4:3", "9:16", "16:9", "21:9"],
504
510
  sizes: ["1K"],
505
511
  requires: ["references"],
506
- supports: ["seed"]
512
+ supports: []
507
513
  },
508
514
  defaults: { size: "1K", aspectRatio: "1:1" },
509
515
  usage: { ...IMAGE_USAGE },
@@ -555,7 +561,6 @@ var IMAGE_MODELS = [
555
561
  aspectRatios: ["1:1", "16:9", "9:16", "4:3", "3:4", "3:2", "2:3"],
556
562
  sizes: [],
557
563
  references: { images: 1 },
558
- requires: ["references"],
559
564
  supports: []
560
565
  },
561
566
  defaults: { aspectRatio: "1:1" },
@@ -1274,7 +1279,7 @@ var VIDEO_MODELS = [
1274
1279
  resolutions: ["720p", "1080p", "4k"],
1275
1280
  durations: [8],
1276
1281
  references: { images: 3 },
1277
- supports: ["lastFrame"]
1282
+ supports: ["lastFrame", "seed"]
1278
1283
  },
1279
1284
  defaults: { durationSeconds: 8, resolution: "720p", aspectRatio: "16:9" },
1280
1285
  usage: { ...VIDEO_USAGE, slug: "veo" },
@@ -1302,9 +1307,9 @@ var VIDEO_MODELS = [
1302
1307
  constraints: {
1303
1308
  aspectRatios: [...OMNI_RATIOS],
1304
1309
  resolutions: ["720p", "1080p", "4k"],
1305
- durations: [4, 8],
1310
+ durations: [4, 6, 8],
1306
1311
  references: { images: 3 },
1307
- supports: ["lastFrame"]
1312
+ supports: ["lastFrame", "seed"]
1308
1313
  },
1309
1314
  defaults: { durationSeconds: 8, resolution: "720p", aspectRatio: "16:9" },
1310
1315
  usage: { ...VIDEO_USAGE },
@@ -1331,9 +1336,8 @@ var VIDEO_MODELS = [
1331
1336
  constraints: {
1332
1337
  aspectRatios: [...OMNI_RATIOS],
1333
1338
  resolutions: ["720p", "1080p"],
1334
- durations: [4, 8],
1335
- references: { images: 3 },
1336
- supports: ["lastFrame"]
1339
+ durations: [4, 6, 8],
1340
+ supports: ["lastFrame", "seed"]
1337
1341
  },
1338
1342
  defaults: { durationSeconds: 8, resolution: "720p", aspectRatio: "16:9" },
1339
1343
  usage: { ...VIDEO_USAGE },
@@ -1365,7 +1369,7 @@ var VIDEO_MODELS = [
1365
1369
  // reference clip keeps its own framing.
1366
1370
  aspectRatios: [...OMNI_RATIOS],
1367
1371
  durations: [3, 4, 5, 6, 7, 8, 9, 10],
1368
- references: { images: 6, videos: 1 },
1372
+ references: { images: 6, videos: 3 },
1369
1373
  supports: []
1370
1374
  },
1371
1375
  defaults: {},
@@ -1391,7 +1395,7 @@ var VIDEO_MODELS = [
1391
1395
  aspectRatios: [...OMNI_RATIOS],
1392
1396
  resolutions: ["360p", "720p", "1080p", "4k"],
1393
1397
  durations: [3, 4, 5, 6, 7, 8, 9, 10],
1394
- references: { images: 6, videos: 1 },
1398
+ references: { images: 6, videos: 3 },
1395
1399
  supports: []
1396
1400
  },
1397
1401
  defaults: {},
@@ -1420,7 +1424,7 @@ var VIDEO_MODELS = [
1420
1424
  durations: { min: 4, max: 15 },
1421
1425
  durationPresets: [4, 6, 8, 10, 12, 15],
1422
1426
  references: { images: 9, videos: 3, audios: 3, audioOnly: false },
1423
- ratioIgnoredWith: ["firstFrame"],
1427
+ ratioIgnoredWith: ["firstFrame", "sourceVideo"],
1424
1428
  supports: ["generateAudio", "seed", "lastFrame", "watermark"]
1425
1429
  },
1426
1430
  defaults: {
@@ -1452,7 +1456,7 @@ var VIDEO_MODELS = [
1452
1456
  resolutions: ["480p", "720p"],
1453
1457
  durations: { min: 4, max: 15 },
1454
1458
  references: { images: 9, videos: 3, audios: 3, audioOnly: false },
1455
- ratioIgnoredWith: ["firstFrame"],
1459
+ ratioIgnoredWith: ["firstFrame", "sourceVideo"],
1456
1460
  supports: ["generateAudio", "seed", "lastFrame", "watermark"]
1457
1461
  },
1458
1462
  defaults: {
@@ -1485,7 +1489,7 @@ var VIDEO_MODELS = [
1485
1489
  resolutions: ["480p", "720p", "1080p"],
1486
1490
  durations: { min: 4, max: 30 },
1487
1491
  references: { images: 30, videos: 10, audios: 10 },
1488
- ratioIgnoredWith: ["firstFrame"],
1492
+ ratioIgnoredWith: ["firstFrame", "sourceVideo"],
1489
1493
  supports: ["generateAudio", "seed", "lastFrame", "watermark"]
1490
1494
  },
1491
1495
  defaults: {
@@ -1604,18 +1608,7 @@ var VIDEO_MODELS = [
1604
1608
  lifecycle: "active",
1605
1609
  exposure: "catalog",
1606
1610
  constraints: {
1607
- aspectRatios: [
1608
- "1:1",
1609
- "16:9",
1610
- "9:16",
1611
- "4:3",
1612
- "3:4",
1613
- "3:2",
1614
- "2:3",
1615
- "21:9",
1616
- "4:5",
1617
- "adaptive"
1618
- ],
1611
+ aspectRatios: [...KLING_RATIOS],
1619
1612
  resolutions: ["720p", "1080p"],
1620
1613
  durations: { min: 3, max: 15 },
1621
1614
  references: { images: 7 },
@@ -2077,30 +2070,34 @@ var CatalogSchema = import_zod.z.array(CatalogModelSchemaBase).superRefine((rows
2077
2070
  });
2078
2071
 
2079
2072
  // src/catalog/live.ts
2080
- var liveCatalog = new Catalog(MODELS);
2073
+ var KEY = /* @__PURE__ */ Symbol.for("@shop-os/modelkit/live-catalog");
2074
+ var store = globalThis;
2075
+ var liveState = store[KEY] ??= {
2076
+ catalog: new Catalog(MODELS),
2077
+ memo: void 0,
2078
+ timer: void 0,
2079
+ revision: void 0,
2080
+ origin: "snapshot"
2081
+ };
2082
+ var liveCatalog = liveState.catalog;
2081
2083
 
2082
2084
  // src/catalog/load.ts
2083
- var memo;
2084
- var timer;
2085
- var revision;
2086
- var origin = "snapshot";
2085
+ var import_zod2 = require("zod");
2087
2086
  function rowCount() {
2088
2087
  return liveCatalog.list({ lifecycle: ["active", "deprecated", "retired"] }).length;
2089
2088
  }
2090
2089
  function authHeaders(auth) {
2091
2090
  return "bearer" in auth ? { Authorization: `Bearer ${auth.bearer}` } : { "x-api-key": auth.apiKey };
2092
2091
  }
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 };
2092
+ var EnvelopeSchema = import_zod2.z.object({
2093
+ rows: CatalogSchema,
2094
+ revision: import_zod2.z.string().optional()
2095
+ });
2096
+ function toEnvelope(data) {
2097
+ if (data.rows.length === 0) throw new Error("invalid catalog response: no rows");
2098
+ const envelope = { rows: data.rows };
2099
+ if (data.revision !== void 0) envelope.revision = data.revision;
2100
+ return envelope;
2104
2101
  }
2105
2102
  function logPrimed(logger, result) {
2106
2103
  const { origin: loggedOrigin, rowCount: loggedCount, revision: loggedRevision } = result;
@@ -2113,7 +2110,7 @@ function logPrimed(logger, result) {
2113
2110
  async function fetchRemote(source, logger) {
2114
2111
  const fetchImpl = source.fetch ?? globalThis.fetch;
2115
2112
  const headers = authHeaders(source.auth);
2116
- if (revision !== void 0) headers["If-None-Match"] = `"${revision}"`;
2113
+ if (liveState.revision !== void 0) headers["If-None-Match"] = `"${liveState.revision}"`;
2117
2114
  const res = await fetchImpl(`${source.baseUrl}/models/catalog`, {
2118
2115
  headers,
2119
2116
  signal: AbortSignal.timeout(source.timeoutMs ?? 1e4)
@@ -2121,7 +2118,9 @@ async function fetchRemote(source, logger) {
2121
2118
  if (res.status === 304) return { kept: true };
2122
2119
  if (!res.ok) throw new Error(`catalog fetch failed with status ${res.status}`);
2123
2120
  const json = await res.json();
2124
- const { rows, revision: rev } = parseEnvelope(json);
2121
+ const parsed = EnvelopeSchema.safeParse(json);
2122
+ if (!parsed.success) throw new Error("invalid catalog response: missing rows");
2123
+ const { rows, revision: rev } = toEnvelope(parsed.data);
2125
2124
  if (rev === void 0) return { rows, fromCache: false };
2126
2125
  return { rows, revision: rev, fromCache: false };
2127
2126
  }
@@ -2137,40 +2136,46 @@ async function prime(source, logger) {
2137
2136
  try {
2138
2137
  const fetched = await fetchRemote(source, logger);
2139
2138
  if ("kept" in fetched) {
2140
- const result2 = { origin, rowCount: rowCount() };
2141
- if (revision !== void 0) result2.revision = revision;
2139
+ const result2 = { origin: liveState.origin, rowCount: rowCount() };
2140
+ if (liveState.revision !== void 0) result2.revision = liveState.revision;
2142
2141
  logPrimed(logger, result2);
2143
2142
  return result2;
2144
2143
  }
2145
2144
  liveCatalog.replace(fetched.rows);
2146
- origin = "remote";
2147
- revision = fetched.revision;
2145
+ liveState.origin = "remote";
2146
+ liveState.revision = fetched.revision;
2148
2147
  await writeCache(source, logger, fetched.rows, fetched.revision);
2149
- const result = { origin, rowCount: rowCount() };
2148
+ const result = { origin: liveState.origin, rowCount: rowCount() };
2150
2149
  if (fetched.revision !== void 0) result.revision = fetched.revision;
2151
2150
  logPrimed(logger, result);
2152
2151
  return result;
2153
2152
  } catch (err) {
2154
2153
  logger.warn("[modelkit] catalog remote failed, trying cache", err);
2155
- const error = describeError(err);
2154
+ const error = err instanceof Error ? err.message : String(err);
2156
2155
  try {
2157
2156
  const cached = await source.cache?.get();
2158
2157
  if (!cached) throw new Error("no cached catalog");
2159
- const { rows, revision: rev } = parseEnvelope(JSON.parse(cached));
2158
+ const cachedParsed = EnvelopeSchema.safeParse(JSON.parse(cached));
2159
+ if (!cachedParsed.success) throw new Error("invalid catalog response: missing rows");
2160
+ const { rows, revision: rev } = toEnvelope(cachedParsed.data);
2160
2161
  liveCatalog.replace(rows);
2161
- origin = "last-good";
2162
- revision = rev;
2163
- const result = { origin, rowCount: rowCount(), error };
2162
+ liveState.origin = "last-good";
2163
+ liveState.revision = rev;
2164
+ const result = {
2165
+ origin: liveState.origin,
2166
+ rowCount: rowCount(),
2167
+ error
2168
+ };
2164
2169
  if (rev !== void 0) result.revision = rev;
2165
2170
  logPrimed(logger, result);
2166
2171
  return result;
2167
2172
  } catch (cacheErr) {
2168
2173
  logger.warn("[modelkit] catalog cache failed, using snapshot", cacheErr);
2169
2174
  liveCatalog.replace([...MODELS]);
2170
- origin = "snapshot";
2171
- revision = void 0;
2175
+ liveState.origin = "snapshot";
2176
+ liveState.revision = void 0;
2172
2177
  const result = {
2173
- origin,
2178
+ origin: liveState.origin,
2174
2179
  rowCount: rowCount(),
2175
2180
  error
2176
2181
  };
@@ -2184,35 +2189,35 @@ async function refresh(source, logger) {
2184
2189
  const fetched = await fetchRemote(source, logger);
2185
2190
  if ("kept" in fetched) return;
2186
2191
  liveCatalog.replace(fetched.rows);
2187
- origin = "remote";
2188
- revision = fetched.revision;
2192
+ liveState.origin = "remote";
2193
+ liveState.revision = fetched.revision;
2189
2194
  await writeCache(source, logger, fetched.rows, fetched.revision);
2190
2195
  } catch (err) {
2191
2196
  logger.warn("[modelkit] catalog refresh failed, keeping current rows", err);
2192
2197
  }
2193
2198
  }
2194
2199
  function primeCatalog(source) {
2195
- if (memo) return memo;
2200
+ if (liveState.memo) return liveState.memo;
2196
2201
  const logger = source.logger ?? console;
2197
- memo = prime(source, logger).then((result) => {
2202
+ const pending = prime(source, logger).then((result) => {
2198
2203
  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?.();
2204
+ if (ttlMs > 0 && liveState.timer === void 0) {
2205
+ liveState.timer = setInterval(() => void refresh(source, logger), ttlMs);
2206
+ liveState.timer.unref?.();
2203
2207
  }
2204
2208
  return result;
2205
2209
  });
2206
- return memo;
2210
+ liveState.memo = pending;
2211
+ return pending;
2207
2212
  }
2208
2213
  function resetLiveCatalog() {
2209
- if (timer !== void 0) {
2210
- clearInterval(timer);
2211
- timer = void 0;
2214
+ if (liveState.timer !== void 0) {
2215
+ clearInterval(liveState.timer);
2216
+ liveState.timer = void 0;
2212
2217
  }
2213
- memo = void 0;
2214
- revision = void 0;
2215
- origin = "snapshot";
2218
+ liveState.memo = void 0;
2219
+ liveState.revision = void 0;
2220
+ liveState.origin = "snapshot";
2216
2221
  liveCatalog.replace([...MODELS]);
2217
2222
  }
2218
2223