@x12i/ai-tools 1.0.2 → 1.0.4

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.
Files changed (66) hide show
  1. package/README.md +114 -12
  2. package/dist/{AiModelsCatalogClient-CSVlKql5.d.cts → AiModelsCatalogClient-CNeqFiFs.d.cts} +2 -1
  3. package/dist/{AiModelsCatalogClient-B-dNLXX0.d.ts → AiModelsCatalogClient-nwFoEaqL.d.ts} +2 -1
  4. package/dist/aliases/index.d.cts +4 -3
  5. package/dist/aliases/index.d.ts +4 -3
  6. package/dist/catalog/index.cjs +30 -0
  7. package/dist/catalog/index.cjs.map +1 -0
  8. package/dist/catalog/index.d.cts +100 -0
  9. package/dist/catalog/index.d.ts +100 -0
  10. package/dist/catalog/index.js +30 -0
  11. package/dist/catalog/index.js.map +1 -0
  12. package/dist/catalox/index.cjs +2 -2
  13. package/dist/catalox/index.d.cts +7 -19
  14. package/dist/catalox/index.d.ts +7 -19
  15. package/dist/catalox/index.js +1 -1
  16. package/dist/chunk-C3H7RTFR.cjs +1 -0
  17. package/dist/chunk-C3H7RTFR.cjs.map +1 -0
  18. package/dist/{chunk-ONA73BU6.cjs → chunk-DKHGWHXP.cjs} +21 -12
  19. package/dist/chunk-DKHGWHXP.cjs.map +1 -0
  20. package/dist/{chunk-HHNHWYTP.cjs → chunk-FGP3QXWL.cjs} +94 -36
  21. package/dist/chunk-FGP3QXWL.cjs.map +1 -0
  22. package/dist/chunk-HS74X2OJ.cjs +172 -0
  23. package/dist/chunk-HS74X2OJ.cjs.map +1 -0
  24. package/dist/chunk-HYGXZY25.js +163 -0
  25. package/dist/chunk-HYGXZY25.js.map +1 -0
  26. package/dist/chunk-M5TMA73F.js +1 -0
  27. package/dist/chunk-M5TMA73F.js.map +1 -0
  28. package/dist/chunk-MX3AMQFC.js +172 -0
  29. package/dist/chunk-MX3AMQFC.js.map +1 -0
  30. package/dist/{chunk-MLRHYOCD.js → chunk-VRFVF5RH.js} +21 -12
  31. package/dist/chunk-VRFVF5RH.js.map +1 -0
  32. package/dist/cli/index.cjs +133 -30
  33. package/dist/cli/index.cjs.map +1 -1
  34. package/dist/cli/index.js +134 -31
  35. package/dist/cli/index.js.map +1 -1
  36. package/dist/cost/index.d.cts +4 -3
  37. package/dist/cost/index.d.ts +4 -3
  38. package/dist/index.cjs +17 -6
  39. package/dist/index.cjs.map +1 -1
  40. package/dist/index.d.cts +10 -16
  41. package/dist/index.d.ts +10 -16
  42. package/dist/index.js +22 -11
  43. package/dist/{modelNameResolver-Bn8QnkSj.d.ts → modelNameResolver-D9V_GfUK.d.cts} +3 -27
  44. package/dist/{modelNameResolver-bZD-eBSJ.d.cts → modelNameResolver-DqFt7g6W.d.ts} +3 -27
  45. package/dist/models/index.d.cts +3 -2
  46. package/dist/models/index.d.ts +3 -2
  47. package/dist/sync/index.cjs +3 -3
  48. package/dist/sync/index.d.cts +6 -3
  49. package/dist/sync/index.d.ts +6 -3
  50. package/dist/sync/index.js +2 -2
  51. package/dist/syncAiModelsCatalog-CnXRLm2c.d.cts +32 -0
  52. package/dist/syncAiModelsCatalog-DpkN_w7S.d.ts +32 -0
  53. package/dist/types-BYXnCvKx.d.cts +137 -0
  54. package/dist/types-BYXnCvKx.d.ts +137 -0
  55. package/dist/types-CX6QFNNy.d.cts +144 -0
  56. package/dist/types-CuiPDcVs.d.ts +144 -0
  57. package/dist/upsertAiModelRecord-C831wOIF.d.ts +35 -0
  58. package/dist/upsertAiModelRecord-CjY-sny0.d.cts +35 -0
  59. package/package.json +8 -1
  60. package/dist/chunk-HHNHWYTP.cjs.map +0 -1
  61. package/dist/chunk-ML2FRR4L.js +0 -105
  62. package/dist/chunk-ML2FRR4L.js.map +0 -1
  63. package/dist/chunk-MLRHYOCD.js.map +0 -1
  64. package/dist/chunk-ONA73BU6.cjs.map +0 -1
  65. package/dist/types-DdGB3YaA.d.cts +0 -278
  66. package/dist/types-DdGB3YaA.d.ts +0 -278
@@ -0,0 +1,172 @@
1
+ import {
2
+ OpenRouterSyncProvider,
3
+ ensureAiModelsCatalog,
4
+ syncAiModelsCatalog
5
+ } from "./chunk-VRFVF5RH.js";
6
+ import {
7
+ decodeModelFirestoreDocId,
8
+ encodeModelFirestoreDocId
9
+ } from "./chunk-HYGXZY25.js";
10
+ import {
11
+ AiModelsCatalogClient
12
+ } from "./chunk-KQOALKKX.js";
13
+ import {
14
+ AI_MODELS_CATALOG_ID,
15
+ AI_MODELS_DESCRIPTOR,
16
+ AI_TOOLS_APP_ID,
17
+ invalidateModelsCache
18
+ } from "./chunk-DJ5SWJDY.js";
19
+
20
+ // src/catalog/verifyAiModelsCatalog.ts
21
+ function sample(ids, limit) {
22
+ return ids.length <= limit ? ids : ids.slice(0, limit);
23
+ }
24
+ async function verifyAiModelsCatalog(options) {
25
+ const start = Date.now();
26
+ const appId = options.appId ?? AI_TOOLS_APP_ID;
27
+ const catalogId = options.catalogId ?? AI_MODELS_CATALOG_ID;
28
+ const sampleLimit = options.sampleLimit ?? 20;
29
+ if (options.ensureDescriptor !== false) {
30
+ await ensureAiModelsCatalog(options.catalox, { appId, catalogId });
31
+ }
32
+ const orIds = options.expectedModelIds ?? new Set(
33
+ (options.openRouterModels ?? await new OpenRouterSyncProvider({
34
+ apiKey: options.openRouterApiKey,
35
+ query: options.openRouterQuery ?? { output_modalities: "all" }
36
+ }).fetchModels()).map((m) => m.modelId)
37
+ );
38
+ invalidateModelsCache(appId);
39
+ const client = new AiModelsCatalogClient({ catalox: options.catalox, appId, catalogId });
40
+ const catMap = await client.getAllModels();
41
+ const catIds = new Set(catMap.keys());
42
+ const missingInCatalox = [...orIds].filter((id) => !catIds.has(id));
43
+ const extraInCatalox = [...catIds].filter((id) => !orIds.has(id));
44
+ let supportsReasoningMissing = 0;
45
+ let openRouterMirrorMissing = 0;
46
+ for (const m of catMap.values()) {
47
+ if (m.supportsReasoning === void 0) supportsReasoningMissing++;
48
+ if (!m.openRouter?.id) openRouterMirrorMissing++;
49
+ }
50
+ const descriptor = await options.catalox.getCatalogDescriptor(
51
+ { appId, superAdmin: true },
52
+ catalogId
53
+ );
54
+ const remoteKeys = descriptor?.queryableFields?.map((f) => f.key) ?? [];
55
+ const localKeys = AI_MODELS_DESCRIPTOR.queryableFields.map((f) => f.key);
56
+ const descriptorKeysMatch = remoteKeys.length === localKeys.length && remoteKeys.every((k, i) => k === localKeys[i]);
57
+ const ok = missingInCatalox.length === 0 && extraInCatalox.length === 0 && supportsReasoningMissing === 0 && openRouterMirrorMissing === 0 && descriptorKeysMatch;
58
+ return {
59
+ ok,
60
+ openRouterCount: orIds.size,
61
+ cataloxCount: catIds.size,
62
+ missingInCatalox: sample(missingInCatalox, sampleLimit),
63
+ extraInCatalox: sample(extraInCatalox, sampleLimit),
64
+ supportsReasoningMissing,
65
+ openRouterMirrorMissing,
66
+ descriptorKeysMatch,
67
+ durationMs: Date.now() - start
68
+ };
69
+ }
70
+
71
+ // src/catalog/pruneStaleCatalogModels.ts
72
+ import {
73
+ flatNativeItemsCollectionRef,
74
+ legacyNativeItemsCollectionRef,
75
+ resolveNativeItemsLayout
76
+ } from "@x12i/catalox/firebase";
77
+ var DELETE_BATCH = 400;
78
+ async function pruneStaleCatalogModels(options) {
79
+ const { firestore, catalogId, activeModelIds, dryRun } = options;
80
+ const layout = await resolveNativeItemsLayout(firestore, catalogId);
81
+ const col = layout === "legacy" ? legacyNativeItemsCollectionRef(firestore, catalogId) : flatNativeItemsCollectionRef(firestore, catalogId);
82
+ const snapshot = await col.get();
83
+ const toDelete = [];
84
+ for (const doc of snapshot.docs) {
85
+ const modelId = decodeModelFirestoreDocId(doc.id);
86
+ if (!activeModelIds.has(modelId)) {
87
+ toDelete.push(modelId);
88
+ }
89
+ }
90
+ if (!dryRun && toDelete.length > 0) {
91
+ for (let i = 0; i < toDelete.length; i += DELETE_BATCH) {
92
+ const chunk = toDelete.slice(i, i + DELETE_BATCH);
93
+ const batch = firestore.batch();
94
+ for (const modelId of chunk) {
95
+ batch.delete(col.doc(encodeModelFirestoreDocId(modelId)));
96
+ }
97
+ await batch.commit();
98
+ }
99
+ }
100
+ return {
101
+ scanned: snapshot.size,
102
+ pruned: toDelete.length,
103
+ prunedModelIds: toDelete
104
+ };
105
+ }
106
+
107
+ // src/catalog/runAiModelsCatalogSync.ts
108
+ var CatalogSyncJobError = class extends Error {
109
+ constructor(message, sync, verify) {
110
+ super(message);
111
+ this.sync = sync;
112
+ this.verify = verify;
113
+ this.name = "CatalogSyncJobError";
114
+ }
115
+ sync;
116
+ verify;
117
+ };
118
+ async function runAiModelsCatalogSync(options) {
119
+ const verifyAfter = options.verifyAfter !== false;
120
+ const failOnVerifyError = options.failOnVerifyError !== false;
121
+ const appId = options.appId ?? AI_TOOLS_APP_ID;
122
+ const catalogId = options.catalogId ?? AI_MODELS_CATALOG_ID;
123
+ const ctx = { appId, superAdmin: true };
124
+ const sync = await syncAiModelsCatalog(options);
125
+ let prune;
126
+ if (options.pruneStale && !options.dryRun) {
127
+ prune = await pruneStaleCatalogModels({
128
+ firestore: options.firestore,
129
+ catalogId,
130
+ context: ctx,
131
+ activeModelIds: new Set(sync.syncedModelIds),
132
+ dryRun: options.dryRun
133
+ });
134
+ invalidateModelsCache(appId);
135
+ }
136
+ let verify;
137
+ if (verifyAfter) {
138
+ verify = await verifyAiModelsCatalog({
139
+ catalox: options.catalox,
140
+ appId,
141
+ catalogId,
142
+ expectedModelIds: new Set(sync.syncedModelIds),
143
+ ensureDescriptor: false
144
+ });
145
+ }
146
+ const syncFailed = sync.errors.length > 0 || sync.upserted < sync.fetched;
147
+ const verifyFailed = verify !== void 0 && !verify.ok;
148
+ const ok = !syncFailed && !verifyFailed;
149
+ if (!ok && failOnVerifyError) {
150
+ const parts = [];
151
+ if (syncFailed) {
152
+ parts.push(
153
+ `sync incomplete: upserted ${sync.upserted}/${sync.fetched}, ${sync.errors.length} errors`
154
+ );
155
+ }
156
+ if (verifyFailed && verify) {
157
+ parts.push(
158
+ `verify failed: catalox ${verify.cataloxCount} vs openrouter ${verify.openRouterCount}, missing ${verify.missingInCatalox.length}, extra ${verify.extraInCatalox.length}`
159
+ );
160
+ }
161
+ throw new CatalogSyncJobError(parts.join("; "), sync, verify);
162
+ }
163
+ return { sync, verify, prune, ok };
164
+ }
165
+
166
+ export {
167
+ verifyAiModelsCatalog,
168
+ pruneStaleCatalogModels,
169
+ CatalogSyncJobError,
170
+ runAiModelsCatalogSync
171
+ };
172
+ //# sourceMappingURL=chunk-MX3AMQFC.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/catalog/verifyAiModelsCatalog.ts","../src/catalog/pruneStaleCatalogModels.ts","../src/catalog/runAiModelsCatalogSync.ts"],"sourcesContent":["import type { Catalox } from \"@x12i/catalox\";\nimport { invalidateModelsCache } from \"../cache/modelCache.js\";\nimport { AiModelsCatalogClient } from \"../catalox/AiModelsCatalogClient.js\";\nimport type { AiModelRecord } from \"../models/types.js\";\nimport { OpenRouterSyncProvider } from \"../sync/OpenRouterSyncProvider.js\";\nimport type { OpenRouterModelsQuery } from \"../models/openrouter.types.js\";\nimport {\n AI_MODELS_CATALOG_ID,\n AI_MODELS_DESCRIPTOR,\n AI_TOOLS_APP_ID,\n} from \"./aiModelsCatalogDescriptor.js\";\nimport { ensureAiModelsCatalog } from \"./ensureAiModelsCatalog.js\";\n\nexport type CatalogVerifyOptions = {\n catalox: Catalox;\n appId?: string;\n catalogId?: string;\n /** When set, compare against this id set (e.g. from a sync that just ran). */\n expectedModelIds?: Set<string>;\n /** When set, compare against this list instead of fetching OpenRouter. */\n openRouterModels?: AiModelRecord[];\n openRouterApiKey?: string;\n openRouterQuery?: OpenRouterModelsQuery;\n /** Ensure descriptor is registered before reading (default true). */\n ensureDescriptor?: boolean;\n /** Max model ids to include in report samples (default 20). */\n sampleLimit?: number;\n};\n\nexport type CatalogVerifyReport = {\n ok: boolean;\n openRouterCount: number;\n cataloxCount: number;\n missingInCatalox: string[];\n extraInCatalox: string[];\n supportsReasoningMissing: number;\n openRouterMirrorMissing: number;\n descriptorKeysMatch: boolean;\n durationMs: number;\n};\n\nfunction sample(ids: string[], limit: number): string[] {\n return ids.length <= limit ? ids : ids.slice(0, limit);\n}\n\n/**\n * Compare the live OpenRouter catalog with Catalox/Firestore ai-models items.\n * Use after sync in CI/cron, or standalone health checks.\n */\nexport async function verifyAiModelsCatalog(\n options: CatalogVerifyOptions,\n): Promise<CatalogVerifyReport> {\n const start = Date.now();\n const appId = options.appId ?? AI_TOOLS_APP_ID;\n const catalogId = options.catalogId ?? AI_MODELS_CATALOG_ID;\n const sampleLimit = options.sampleLimit ?? 20;\n\n if (options.ensureDescriptor !== false) {\n await ensureAiModelsCatalog(options.catalox, { appId, catalogId });\n }\n\n const orIds =\n options.expectedModelIds ??\n new Set(\n (\n options.openRouterModels ??\n (await new OpenRouterSyncProvider({\n apiKey: options.openRouterApiKey,\n query: options.openRouterQuery ?? { output_modalities: \"all\" },\n }).fetchModels())\n ).map((m) => m.modelId),\n );\n\n invalidateModelsCache(appId);\n const client = new AiModelsCatalogClient({ catalox: options.catalox, appId, catalogId });\n const catMap = await client.getAllModels();\n const catIds = new Set(catMap.keys());\n\n const missingInCatalox = [...orIds].filter((id) => !catIds.has(id));\n const extraInCatalox = [...catIds].filter((id) => !orIds.has(id));\n\n let supportsReasoningMissing = 0;\n let openRouterMirrorMissing = 0;\n for (const m of catMap.values()) {\n if (m.supportsReasoning === undefined) supportsReasoningMissing++;\n if (!m.openRouter?.id) openRouterMirrorMissing++;\n }\n\n const descriptor = await options.catalox.getCatalogDescriptor(\n { appId, superAdmin: true },\n catalogId,\n );\n const remoteKeys = descriptor?.queryableFields?.map((f) => f.key) ?? [];\n const localKeys = AI_MODELS_DESCRIPTOR.queryableFields.map((f) => f.key);\n const descriptorKeysMatch =\n remoteKeys.length === localKeys.length &&\n remoteKeys.every((k, i) => k === localKeys[i]);\n\n const ok =\n missingInCatalox.length === 0 &&\n extraInCatalox.length === 0 &&\n supportsReasoningMissing === 0 &&\n openRouterMirrorMissing === 0 &&\n descriptorKeysMatch;\n\n return {\n ok,\n openRouterCount: orIds.size,\n cataloxCount: catIds.size,\n missingInCatalox: sample(missingInCatalox, sampleLimit),\n extraInCatalox: sample(extraInCatalox, sampleLimit),\n supportsReasoningMissing,\n openRouterMirrorMissing,\n descriptorKeysMatch,\n durationMs: Date.now() - start,\n };\n}\n","import type { Firestore } from \"firebase-admin/firestore\";\nimport type { CataloxContext } from \"@x12i/catalox\";\nimport {\n flatNativeItemsCollectionRef,\n legacyNativeItemsCollectionRef,\n resolveNativeItemsLayout,\n} from \"@x12i/catalox/firebase\";\nimport { decodeModelFirestoreDocId, encodeModelFirestoreDocId } from \"../catalox/modelDocId.js\";\n\nexport type PruneStaleCatalogModelsOptions = {\n firestore: Firestore;\n catalogId: string;\n context: CataloxContext;\n /** Canonical model ids from the latest OpenRouter fetch. */\n activeModelIds: Set<string>;\n dryRun?: boolean;\n};\n\nexport type PruneStaleCatalogModelsResult = {\n scanned: number;\n pruned: number;\n prunedModelIds: string[];\n};\n\nconst DELETE_BATCH = 400;\n\n/**\n * Remove catalog documents that are no longer listed on OpenRouter.\n * Off by default — enable explicitly when running production sync jobs.\n */\nexport async function pruneStaleCatalogModels(\n options: PruneStaleCatalogModelsOptions,\n): Promise<PruneStaleCatalogModelsResult> {\n const { firestore, catalogId, activeModelIds, dryRun } = options;\n const layout = await resolveNativeItemsLayout(firestore, catalogId);\n const col =\n layout === \"legacy\"\n ? legacyNativeItemsCollectionRef(firestore, catalogId)\n : flatNativeItemsCollectionRef(firestore, catalogId);\n\n const snapshot = await col.get();\n const toDelete: string[] = [];\n\n for (const doc of snapshot.docs) {\n const modelId = decodeModelFirestoreDocId(doc.id);\n if (!activeModelIds.has(modelId)) {\n toDelete.push(modelId);\n }\n }\n\n if (!dryRun && toDelete.length > 0) {\n for (let i = 0; i < toDelete.length; i += DELETE_BATCH) {\n const chunk = toDelete.slice(i, i + DELETE_BATCH);\n const batch = firestore.batch();\n for (const modelId of chunk) {\n batch.delete(col.doc(encodeModelFirestoreDocId(modelId)));\n }\n await batch.commit();\n }\n }\n\n return {\n scanned: snapshot.size,\n pruned: toDelete.length,\n prunedModelIds: toDelete,\n };\n}\n","import { invalidateModelsCache } from \"../cache/modelCache.js\";\nimport { syncAiModelsCatalog } from \"../sync/syncAiModelsCatalog.js\";\nimport type { SyncOptions, SyncResult } from \"../sync/syncAiModelsCatalog.js\";\nimport { pruneStaleCatalogModels } from \"./pruneStaleCatalogModels.js\";\nimport type { CatalogVerifyReport } from \"./verifyAiModelsCatalog.js\";\nimport { verifyAiModelsCatalog } from \"./verifyAiModelsCatalog.js\";\nimport { AI_MODELS_CATALOG_ID, AI_TOOLS_APP_ID } from \"./aiModelsCatalogDescriptor.js\";\n\nexport type CatalogSyncJobOptions = SyncOptions & {\n /**\n * After upsert, compare Catalox to OpenRouter (default true).\n * Set false only when you will verify separately.\n */\n verifyAfter?: boolean;\n /** Fail the job when verification does not pass (default true). */\n failOnVerifyError?: boolean;\n /** Delete Firestore rows not present in the latest OpenRouter list (default false). */\n pruneStale?: boolean;\n};\n\nexport type CatalogSyncJobResult = {\n sync: SyncResult;\n verify?: CatalogVerifyReport;\n prune?: {\n scanned: number;\n pruned: number;\n prunedModelIds: string[];\n };\n ok: boolean;\n};\n\nexport class CatalogSyncJobError extends Error {\n constructor(\n message: string,\n public readonly sync: SyncResult,\n public readonly verify?: CatalogVerifyReport,\n ) {\n super(message);\n this.name = \"CatalogSyncJobError\";\n }\n}\n\n/**\n * Production entry point: sync OpenRouter → Catalox, optionally prune stale rows, then verify.\n * Suitable for cron, Cloud Run jobs, and `ai-tools sync`.\n */\nexport async function runAiModelsCatalogSync(\n options: CatalogSyncJobOptions,\n): Promise<CatalogSyncJobResult> {\n const verifyAfter = options.verifyAfter !== false;\n const failOnVerifyError = options.failOnVerifyError !== false;\n const appId = options.appId ?? AI_TOOLS_APP_ID;\n const catalogId = options.catalogId ?? AI_MODELS_CATALOG_ID;\n const ctx = { appId, superAdmin: true as const };\n\n const sync = await syncAiModelsCatalog(options);\n\n let prune: CatalogSyncJobResult[\"prune\"];\n\n if (options.pruneStale && !options.dryRun) {\n prune = await pruneStaleCatalogModels({\n firestore: options.firestore,\n catalogId,\n context: ctx,\n activeModelIds: new Set(sync.syncedModelIds),\n dryRun: options.dryRun,\n });\n invalidateModelsCache(appId);\n }\n\n let verify: CatalogVerifyReport | undefined;\n if (verifyAfter) {\n verify = await verifyAiModelsCatalog({\n catalox: options.catalox,\n appId,\n catalogId,\n expectedModelIds: new Set(sync.syncedModelIds),\n ensureDescriptor: false,\n });\n }\n\n const syncFailed = sync.errors.length > 0 || sync.upserted < sync.fetched;\n const verifyFailed = verify !== undefined && !verify.ok;\n const ok = !syncFailed && !verifyFailed;\n\n if (!ok && failOnVerifyError) {\n const parts: string[] = [];\n if (syncFailed) {\n parts.push(\n `sync incomplete: upserted ${sync.upserted}/${sync.fetched}, ${sync.errors.length} errors`,\n );\n }\n if (verifyFailed && verify) {\n parts.push(\n `verify failed: catalox ${verify.cataloxCount} vs openrouter ${verify.openRouterCount}, ` +\n `missing ${verify.missingInCatalox.length}, extra ${verify.extraInCatalox.length}`,\n );\n }\n throw new CatalogSyncJobError(parts.join(\"; \"), sync, verify);\n }\n\n return { sync, verify, prune, ok };\n}\n"],"mappings":"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","names":[]}
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  batchUpsertAiModelRecords
3
- } from "./chunk-ML2FRR4L.js";
3
+ } from "./chunk-HYGXZY25.js";
4
4
  import {
5
5
  normalizeOpenRouterModel
6
6
  } from "./chunk-6QGDZTGH.js";
@@ -126,28 +126,37 @@ async function syncAiModelsCatalog(options) {
126
126
  if (options.verbose) {
127
127
  console.log(`[sync] fetched ${models.length} models from ${provider.getModelsUrl()}`);
128
128
  }
129
+ const syncedModelIds = models.map((m) => m.modelId);
129
130
  if (options.dryRun) {
130
131
  return {
131
132
  fetched: models.length,
132
133
  upserted: 0,
133
134
  skipped: models.length,
134
135
  errors: [],
136
+ syncedModelIds,
135
137
  durationMs: Date.now() - start
136
138
  };
137
139
  }
138
- const errors = [];
139
- try {
140
- await batchUpsertAiModelRecords(options.firestore, catalogId, ctx, models);
141
- } catch (cause) {
142
- const message = cause instanceof Error ? cause.message : String(cause);
143
- throw new SyncError("SYNC_UPSERT_FAILED", `Batch upsert failed: ${message}`, cause);
144
- }
140
+ const upsert = await batchUpsertAiModelRecords(
141
+ options.firestore,
142
+ catalogId,
143
+ ctx,
144
+ models,
145
+ { onProgress: options.onProgress }
146
+ );
145
147
  invalidateModelsCache(appId);
148
+ if (upsert.failed.length > 0 && upsert.upserted === 0) {
149
+ throw new SyncError(
150
+ "SYNC_UPSERT_FAILED",
151
+ `All ${upsert.failed.length} model upserts failed. First error: ${upsert.failed[0].error}`
152
+ );
153
+ }
146
154
  return {
147
155
  fetched: models.length,
148
- upserted: models.length,
149
- skipped: 0,
150
- errors,
156
+ upserted: upsert.upserted,
157
+ skipped: models.length - upsert.upserted,
158
+ errors: upsert.failed,
159
+ syncedModelIds,
151
160
  durationMs: Date.now() - start
152
161
  };
153
162
  }
@@ -157,4 +166,4 @@ export {
157
166
  OpenRouterSyncProvider,
158
167
  syncAiModelsCatalog
159
168
  };
160
- //# sourceMappingURL=chunk-MLRHYOCD.js.map
169
+ //# sourceMappingURL=chunk-VRFVF5RH.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/catalog/ensureAiModelsCatalog.ts","../src/sync/OpenRouterSyncProvider.ts","../src/sync/syncAiModelsCatalog.ts"],"sourcesContent":["import type { Catalox, CataloxContext } from \"@x12i/catalox\";\nimport { AiToolsError } from \"../errors.js\";\nimport {\n AI_MODELS_CATALOG_ID,\n AI_MODELS_DESCRIPTOR,\n AI_TOOLS_APP_ID,\n} from \"./aiModelsCatalogDescriptor.js\";\n\nexport type EnsureAiModelsCatalogOptions = {\n appId?: string;\n catalogId?: string;\n};\n\nexport async function ensureAiModelsCatalog(\n catalox: Catalox,\n options: EnsureAiModelsCatalogOptions = {},\n): Promise<void> {\n const appId = options.appId ?? AI_TOOLS_APP_ID;\n const catalogId = options.catalogId ?? AI_MODELS_CATALOG_ID;\n\n const ctx: CataloxContext = { appId, superAdmin: true };\n\n try {\n await catalox.ensureCatalog(ctx, {\n catalogId,\n name: AI_MODELS_DESCRIPTOR.label,\n status: \"active\",\n });\n\n await catalox.bindCatalogToApp(ctx, {\n appId,\n catalogId,\n access: { canRead: true, canWrite: true, canAdmin: true },\n });\n\n await catalox.upsertCatalogDescriptor(ctx, catalogId, {\n descriptorVersion: \"1.0.0\",\n descriptor: AI_MODELS_DESCRIPTOR,\n });\n } catch (cause) {\n throw new AiToolsError(\n \"CATALOG_BOOTSTRAP_FAILED\",\n `Failed to bootstrap catalog \"${catalogId}\" for app \"${appId}\".`,\n cause,\n );\n }\n}\n","import { SyncError } from \"../errors.js\";\nimport { normalizeOpenRouterModel } from \"../models/normalizeOpenRouterModel.js\";\nimport type { AiModelRecord } from \"../models/types.js\";\nimport type {\n OpenRouterModelsQuery,\n OpenRouterModelsResponse,\n} from \"../models/openrouter.types.js\";\n\nexport type OpenRouterSyncProviderOptions = {\n /** Optional — public GET /models needs no key. */\n apiKey?: string;\n baseUrl?: string;\n /** Query params passed to OpenRouter (default: all modalities). */\n query?: OpenRouterModelsQuery;\n};\n\nfunction buildFetchHeaders(apiKey?: string): Record<string, string> {\n const headers: Record<string, string> = { Accept: \"application/json\" };\n if (apiKey?.trim()) headers.Authorization = `Bearer ${apiKey.trim()}`;\n return headers;\n}\n\nfunction buildModelsUrl(baseUrl: string, query?: OpenRouterModelsQuery): string {\n const url = new URL(`${baseUrl.replace(/\\/$/, \"\")}/models`);\n const q = { output_modalities: \"all\", ...query };\n for (const [key, value] of Object.entries(q)) {\n if (value !== undefined && value !== \"\") url.searchParams.set(key, value);\n }\n return url.toString();\n}\n\nexport class OpenRouterSyncProvider {\n private readonly apiKey?: string;\n private readonly baseUrl: string;\n private readonly query?: OpenRouterModelsQuery;\n\n constructor(options: OpenRouterSyncProviderOptions = {}) {\n this.apiKey = options.apiKey;\n this.baseUrl = options.baseUrl ?? \"https://openrouter.ai/api/v1\";\n this.query = options.query ?? { output_modalities: \"all\" };\n }\n\n /**\n * Fetches the full OpenRouter model catalog (public, no API key required).\n * @see https://openrouter.ai/api/v1/models\n */\n async fetchModels(): Promise<AiModelRecord[]> {\n const url = buildModelsUrl(this.baseUrl, this.query);\n let response: Response;\n\n try {\n response = await fetch(url, { headers: buildFetchHeaders(this.apiKey) });\n } catch (cause) {\n throw new SyncError(\"OPENROUTER_MODELS_FETCH_FAILED\", `Failed to fetch OpenRouter models from ${url}`, cause);\n }\n\n if (response.status === 401 || response.status === 403) {\n throw new SyncError(\n \"OPENROUTER_AUTH_FAILED\",\n this.apiKey\n ? \"OpenRouter API key is invalid or unauthorized.\"\n : \"OpenRouter returned unauthorized — remove OPENROUTER_API_KEY to use the public models endpoint.\",\n );\n }\n\n if (!response.ok) {\n const body = await response.text().catch(() => \"\");\n throw new SyncError(\n \"OPENROUTER_MODELS_FETCH_FAILED\",\n `OpenRouter models fetch failed (${response.status}): ${body.slice(0, 200)}`,\n );\n }\n\n const json = (await response.json()) as OpenRouterModelsResponse;\n const syncedAt = new Date().toISOString();\n return (json.data ?? []).map((row) => normalizeOpenRouterModel(row, syncedAt));\n }\n\n /** Build the URL used for the last fetch pattern (for debugging). */\n getModelsUrl(): string {\n return buildModelsUrl(this.baseUrl, this.query);\n }\n}\n","import type { Catalox, CataloxContext } from \"@x12i/catalox\";\nimport type { Firestore } from \"firebase-admin/firestore\";\nimport { ensureAiModelsCatalog } from \"../catalog/ensureAiModelsCatalog.js\";\nimport { AI_MODELS_CATALOG_ID, AI_TOOLS_APP_ID } from \"../catalog/aiModelsCatalogDescriptor.js\";\nimport { invalidateModelsCache } from \"../cache/modelCache.js\";\nimport {\n batchUpsertAiModelRecords,\n type BatchUpsertProgress,\n} from \"../catalox/upsertAiModelRecord.js\";\nimport { SyncError } from \"../errors.js\";\nimport type { AiModelRecord } from \"../models/types.js\";\nimport type { OpenRouterModelsQuery } from \"../models/openrouter.types.js\";\nimport { OpenRouterSyncProvider } from \"./OpenRouterSyncProvider.js\";\n\nexport type SyncOptions = {\n catalox: Catalox;\n firestore: Firestore;\n openRouterApiKey?: string;\n openRouterQuery?: OpenRouterModelsQuery;\n appId?: string;\n catalogId?: string;\n dryRun?: boolean;\n verbose?: boolean;\n forceCache?: boolean;\n onProgress?: (progress: BatchUpsertProgress) => void;\n};\n\nexport type SyncResult = {\n fetched: number;\n upserted: number;\n skipped: number;\n errors: Array<{ modelId: string; error: string }>;\n durationMs: number;\n /** Model ids written (or that would be written on dry-run). */\n syncedModelIds: string[];\n};\n\nexport async function syncAiModelsCatalog(options: SyncOptions): Promise<SyncResult> {\n const start = Date.now();\n const appId = options.appId ?? AI_TOOLS_APP_ID;\n const catalogId = options.catalogId ?? AI_MODELS_CATALOG_ID;\n const ctx: CataloxContext = { appId, superAdmin: true };\n\n if (options.forceCache) {\n invalidateModelsCache(appId);\n }\n\n await ensureAiModelsCatalog(options.catalox, { appId, catalogId });\n\n const provider = new OpenRouterSyncProvider({\n apiKey: options.openRouterApiKey,\n query: options.openRouterQuery ?? { output_modalities: \"all\" },\n });\n\n let models: AiModelRecord[];\n try {\n models = await provider.fetchModels();\n } catch (error) {\n if (error instanceof SyncError) throw error;\n throw new SyncError(\"SYNC_FETCH_FAILED\", \"Failed to fetch models from OpenRouter.\", error);\n }\n\n if (options.verbose) {\n console.log(`[sync] fetched ${models.length} models from ${provider.getModelsUrl()}`);\n }\n\n const syncedModelIds = models.map((m) => m.modelId);\n\n if (options.dryRun) {\n return {\n fetched: models.length,\n upserted: 0,\n skipped: models.length,\n errors: [],\n syncedModelIds,\n durationMs: Date.now() - start,\n };\n }\n\n const upsert = await batchUpsertAiModelRecords(\n options.firestore,\n catalogId,\n ctx,\n models,\n { onProgress: options.onProgress },\n );\n\n invalidateModelsCache(appId);\n\n if (upsert.failed.length > 0 && upsert.upserted === 0) {\n throw new SyncError(\n \"SYNC_UPSERT_FAILED\",\n `All ${upsert.failed.length} model upserts failed. First error: ${upsert.failed[0]!.error}`,\n );\n }\n\n return {\n fetched: models.length,\n upserted: upsert.upserted,\n skipped: models.length - upsert.upserted,\n errors: upsert.failed,\n syncedModelIds,\n durationMs: Date.now() - start,\n };\n}\n"],"mappings":"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","names":[]}
@@ -5,15 +5,19 @@ var _chunkHN6UAQAEcjs = require('../chunk-HN6UAQAE.cjs');
5
5
 
6
6
 
7
7
 
8
- var _chunkONA73BU6cjs = require('../chunk-ONA73BU6.cjs');
9
- require('../chunk-HHNHWYTP.cjs');
8
+ var _chunkQWAX7VQOcjs = require('../chunk-QWAX7VQO.cjs');
10
9
 
11
10
 
11
+ var _chunkQCRLKVB3cjs = require('../chunk-QCRLKVB3.cjs');
12
12
 
13
- var _chunkQWAX7VQOcjs = require('../chunk-QWAX7VQO.cjs');
14
13
 
15
14
 
16
- var _chunkQCRLKVB3cjs = require('../chunk-QCRLKVB3.cjs');
15
+
16
+ var _chunkHS74X2OJcjs = require('../chunk-HS74X2OJ.cjs');
17
+
18
+
19
+ var _chunkDKHGWHXPcjs = require('../chunk-DKHGWHXP.cjs');
20
+ require('../chunk-FGP3QXWL.cjs');
17
21
  require('../chunk-AV6OE2YQ.cjs');
18
22
 
19
23
 
@@ -179,15 +183,91 @@ ${report.total} total \xB7 ${report.ok} ok \xB7 ${report.unknown} unknown \
179
183
  });
180
184
  }
181
185
 
186
+ // src/cli/report.ts
187
+ function emitJson(data) {
188
+ console.log(JSON.stringify(data, null, 2));
189
+ }
190
+ function emitHuman(lines) {
191
+ for (const line of lines) {
192
+ console.log(line);
193
+ }
194
+ }
195
+ function formatSyncJobReport(result) {
196
+ const lines = [
197
+ "",
198
+ result.ok ? "\u2714 Catalog sync job succeeded" : "\u2717 Catalog sync job failed",
199
+ "",
200
+ "Sync",
201
+ ` Fetched: ${result.sync.fetched}`,
202
+ ` Upserted: ${result.sync.upserted}`,
203
+ ` Skipped: ${result.sync.skipped}`,
204
+ ` Errors: ${result.sync.errors.length}`,
205
+ ` Duration: ${result.sync.durationMs}ms`
206
+ ];
207
+ if (result.verify) {
208
+ lines.push(
209
+ "",
210
+ "Verify",
211
+ ` Status: ${result.verify.ok ? "ok" : "FAILED"}`,
212
+ ` OpenRouter: ${result.verify.openRouterCount}`,
213
+ ` Catalox: ${result.verify.cataloxCount}`,
214
+ ` Missing: ${result.verify.missingInCatalox.length}`,
215
+ ` Extra: ${result.verify.extraInCatalox.length}`,
216
+ ` Duration: ${result.verify.durationMs}ms`
217
+ );
218
+ if (result.verify.missingInCatalox.length > 0) {
219
+ lines.push(` Sample missing: ${result.verify.missingInCatalox.join(", ")}`);
220
+ }
221
+ if (result.verify.extraInCatalox.length > 0) {
222
+ lines.push(` Sample extra: ${result.verify.extraInCatalox.join(", ")}`);
223
+ }
224
+ }
225
+ if (result.prune) {
226
+ lines.push("", "Prune", ` Scanned: ${result.prune.scanned}`, ` Removed: ${result.prune.pruned}`);
227
+ }
228
+ return lines;
229
+ }
230
+
182
231
  // src/cli/commands/catalog.ts
183
232
  function registerCatalogCommand(program2) {
184
- const catalog = program2.command("catalog").description("Catalog bootstrap commands");
185
- catalog.command("ensure").description("Create catalog, descriptor, and app binding if missing").option("--app <appId>", "Override appId").option("--catalog <id>", "Override catalogId").action(async (opts) => {
186
- await _chunkONA73BU6cjs.ensureAiModelsCatalog.call(void 0, getCatalox(), {
233
+ const catalog = program2.command("catalog").description("Catalog bootstrap and health commands");
234
+ catalog.command("ensure").description("Create catalog, descriptor, and app binding if missing").option("--app <appId>", "Override appId").option("--catalog <id>", "Override catalogId").option("--json", "Machine-readable JSON on stdout").action(async (opts) => {
235
+ const appId = _nullishCoalesce(opts.app, () => ( envAppId()));
236
+ const catalogId = _nullishCoalesce(opts.catalog, () => ( envCatalogId()));
237
+ await _chunkDKHGWHXPcjs.ensureAiModelsCatalog.call(void 0, getCatalox(), { appId, catalogId });
238
+ if (opts.json) {
239
+ emitJson({ ok: true, appId, catalogId, action: "ensure" });
240
+ } else {
241
+ console.log("\u2714 ai-models catalog ensured");
242
+ }
243
+ });
244
+ catalog.command("verify").description("Health check: compare Catalox ai-models against the live OpenRouter catalog").option("--app <appId>", "Override appId").option("--catalog <id>", "Override catalogId").option("--json", "Machine-readable JSON on stdout").action(async (opts) => {
245
+ const report = await _chunkHS74X2OJcjs.verifyAiModelsCatalog.call(void 0, {
246
+ catalox: getCatalox(),
187
247
  appId: _nullishCoalesce(opts.app, () => ( envAppId())),
188
248
  catalogId: _nullishCoalesce(opts.catalog, () => ( envCatalogId()))
189
249
  });
190
- console.log("\u2714 ai-models catalog ensured");
250
+ if (opts.json) {
251
+ emitJson(report);
252
+ } else {
253
+ emitHuman([
254
+ report.ok ? "\u2714 Catalog verification passed" : "\u2717 Catalog verification FAILED",
255
+ ` OpenRouter models: ${report.openRouterCount}`,
256
+ ` Catalox models: ${report.cataloxCount}`,
257
+ ` Missing in Catalox: ${report.missingInCatalox.length}`,
258
+ ` Extra in Catalox: ${report.extraInCatalox.length}`,
259
+ ` Duration: ${report.durationMs}ms`
260
+ ]);
261
+ if (report.missingInCatalox.length > 0) {
262
+ console.log(` Sample missing: ${report.missingInCatalox.join(", ")}`);
263
+ }
264
+ if (report.extraInCatalox.length > 0) {
265
+ console.log(` Sample extra: ${report.extraInCatalox.join(", ")}`);
266
+ }
267
+ }
268
+ if (!report.ok) {
269
+ process.exit(1);
270
+ }
191
271
  });
192
272
  }
193
273
 
@@ -362,30 +442,53 @@ ${rows.length} shown \xB7 ${total} matching`);
362
442
 
363
443
  // src/cli/commands/sync.ts
364
444
  function registerSyncCommand(program2) {
365
- program2.command("sync").description("Fetch OpenRouter models (public API, no key required) and upsert into the ai-models catalog").option("--dry-run", "Print what would be written without touching Firestore").option("--verbose", "Print each model ID as it is upserted").option("--app <appId>", "Override appId").option("--catalog <id>", "Override catalogId").option("--force-cache", "Invalidate nx-cache before running").action(async (opts) => {
366
- const result = await _chunkONA73BU6cjs.syncAiModelsCatalog.call(void 0, {
367
- catalox: getCatalox(),
368
- firestore: getFirestore(),
369
- openRouterApiKey: process.env.OPENROUTER_API_KEY,
370
- appId: _nullishCoalesce(opts.app, () => ( envAppId())),
371
- catalogId: _nullishCoalesce(opts.catalog, () => ( envCatalogId())),
372
- dryRun: opts.dryRun,
373
- verbose: opts.verbose,
374
- forceCache: opts.forceCache
375
- });
376
- console.log("\nSync complete");
377
- console.log(` Fetched: ${result.fetched}`);
378
- console.log(` Upserted: ${result.upserted}`);
379
- console.log(` Skipped: ${result.skipped}`);
380
- console.log(` Errors: ${result.errors.length}`);
381
- console.log(` Duration: ${result.durationMs}ms`);
382
- if (result.errors.length > 0) {
383
- for (const e of result.errors.slice(0, 10)) {
384
- console.error(` \u2717 ${e.modelId}: ${e.error}`);
445
+ program2.command("sync").description(
446
+ "Production sync: fetch OpenRouter models, upsert into Catalox/Firestore, verify completeness"
447
+ ).option("--dry-run", "Fetch and validate without writing to Firestore").option("--no-verify", "Skip post-sync verification (not recommended in production)").option("--prune-stale", "Remove catalog rows no longer on OpenRouter").option("--json", "Machine-readable JSON on stdout").option("--verbose", "Log fetch URL and upsert progress").option("--app <appId>", "Override appId").option("--catalog <id>", "Override catalogId").option("--force-cache", "Invalidate nx-cache before running").action(
448
+ async (opts) => {
449
+ const verifyAfter = opts.verify !== false;
450
+ try {
451
+ const result = await _chunkHS74X2OJcjs.runAiModelsCatalogSync.call(void 0, {
452
+ catalox: getCatalox(),
453
+ firestore: getFirestore(),
454
+ openRouterApiKey: process.env.OPENROUTER_API_KEY,
455
+ appId: _nullishCoalesce(opts.app, () => ( envAppId())),
456
+ catalogId: _nullishCoalesce(opts.catalog, () => ( envCatalogId())),
457
+ dryRun: opts.dryRun,
458
+ verbose: opts.verbose,
459
+ forceCache: opts.forceCache,
460
+ verifyAfter,
461
+ pruneStale: opts.pruneStale,
462
+ onProgress: opts.verbose ? ({ completed, total, phase }) => {
463
+ console.log(`[sync] ${phase} ${completed}/${total}`);
464
+ } : void 0
465
+ });
466
+ if (opts.json) {
467
+ emitJson(result);
468
+ } else {
469
+ emitHuman(formatSyncJobReport(result));
470
+ }
471
+ if (!result.ok) {
472
+ process.exit(1);
473
+ }
474
+ } catch (error) {
475
+ if (error instanceof _chunkHS74X2OJcjs.CatalogSyncJobError) {
476
+ const payload = {
477
+ ok: false,
478
+ sync: error.sync,
479
+ verify: error.verify
480
+ };
481
+ if (opts.json) {
482
+ emitJson(payload);
483
+ } else {
484
+ emitHuman(formatSyncJobReport(payload));
485
+ }
486
+ process.exit(1);
487
+ }
488
+ throw error;
385
489
  }
386
- process.exit(1);
387
490
  }
388
- });
491
+ );
389
492
  }
390
493
 
391
494
  // src/cli/index.ts