@x12i/ai-tools 1.0.2 → 1.0.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.
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
package/README.md CHANGED
@@ -61,7 +61,117 @@ npx ai-tools models list --reasoning
61
61
  npx ai-tools models count --reasoning
62
62
  ```
63
63
 
64
- Re-run `npm run seed` or `npx ai-tools sync` after upgrading so existing Firestore rows get `supportsReasoning` indexed.
64
+ Re-run [Updating the model catalog](#updating-the-model-catalog) after upgrading `@x12i/ai-tools` so existing Firestore rows pick up new fields (e.g. `supportsReasoning`).
65
+
66
+ ## Updating the model catalog
67
+
68
+ The **ai-models** catalog in Catalox/Firestore is a mirror of the [OpenRouter Models API](https://openrouter.ai/docs/api-reference/models). OpenRouter’s public `/models` endpoint needs **no API key**; optional `OPENROUTER_API_KEY` only helps with rate limits.
69
+
70
+ ### Prerequisites
71
+
72
+ Add to `.env` (see [Environment variables](#environment-variables)):
73
+
74
+ | Variable | Required for sync |
75
+ |----------|-------------------|
76
+ | `GOOGLE_SERVICE_ACCOUNT_BASE64` | Yes |
77
+ | `FIREBASE_PROJECT_ID` | Yes |
78
+ | `FIRESTORE_DATABASE_ID` | No (defaults to `(default)`) |
79
+ | `AI_TOOLS_APP_ID` | No (default: `ai-tools`) |
80
+ | `AI_TOOLS_CATALOG_ID` | No (default: `ai-models`) |
81
+
82
+ First time only, register the catalog descriptor and app binding:
83
+
84
+ ```bash
85
+ npx ai-tools catalog ensure
86
+ ```
87
+
88
+ ### When to run an update
89
+
90
+ | Situation | What to run |
91
+ |-----------|-------------|
92
+ | **First deploy** | `npx ai-tools catalog ensure` then `npx ai-tools sync` |
93
+ | **Routine refresh** (new models/pricing on OpenRouter) | `npx ai-tools sync` |
94
+ | **After upgrading `@x12i/ai-tools`** (new indexed fields) | `npx ai-tools sync` |
95
+ | **Health check only** (no writes) | `npx ai-tools catalog verify` |
96
+ | **Remove models OpenRouter dropped** | `npx ai-tools sync --prune-stale` |
97
+
98
+ ### Recommended commands
99
+
100
+ **Full update (production default)** — fetch OpenRouter, upsert every model, then verify counts match:
101
+
102
+ ```bash
103
+ npx ai-tools sync
104
+ ```
105
+
106
+ Same job via npm script:
107
+
108
+ ```bash
109
+ npm run seed
110
+ npm run seed:verbose # progress lines
111
+ ```
112
+
113
+ **CI / cron** — JSON on stdout, non-zero exit if sync or verify fails:
114
+
115
+ ```bash
116
+ npx ai-tools sync --json
117
+ # or
118
+ npm run verify:seed
119
+ ```
120
+
121
+ **Verify only** (read-only; good between scheduled syncs):
122
+
123
+ ```bash
124
+ npx ai-tools catalog verify
125
+ npx ai-tools catalog verify --json
126
+ # or
127
+ npm run verify:sync
128
+ ```
129
+
130
+ **Dry run** (fetch OpenRouter, no Firestore writes):
131
+
132
+ ```bash
133
+ npx ai-tools sync --dry-run
134
+ ```
135
+
136
+ | npm script | Equivalent |
137
+ |------------|------------|
138
+ | `npm run seed` | `npx ai-tools sync` |
139
+ | `npm run seed:verbose` | `npx ai-tools sync --verbose` |
140
+ | `npm run verify:seed` | `npx ai-tools sync` (via seed script, sync + verify) |
141
+ | `npm run verify:sync` | `npx ai-tools catalog verify` |
142
+
143
+ ### What `sync` does
144
+
145
+ 1. Ensures catalog + descriptor exist (`catalog ensure` logic).
146
+ 2. Fetches all models from OpenRouter (`output_modalities=all`).
147
+ 3. Upserts each row into Firestore/Catalox (`data` + `indexed` fields).
148
+ 4. Verifies Catalox count equals OpenRouter count (unless `--no-verify`).
149
+
150
+ On re-sync, existing rows are **updated** (`syncedAt`, pricing, metadata); `createdAt` is preserved and `version` increments.
151
+
152
+ ### Cron example
153
+
154
+ ```bash
155
+ # Daily at 04:00 — fail the job if sync or verify fails
156
+ 0 4 * * * cd /path/to/app && npx ai-tools sync --json >> /var/log/ai-tools-sync.log 2>&1
157
+ ```
158
+
159
+ ### Programmatic update
160
+
161
+ ```ts
162
+ import { createCataloxFromEnv } from "@x12i/catalox/firebase";
163
+ import { runAiModelsCatalogSync, verifyAiModelsCatalog } from "@x12i/ai-tools/catalog";
164
+
165
+ const { catalox, firestore } = createCataloxFromEnv();
166
+
167
+ // Sync + verify (throws CatalogSyncJobError on failure)
168
+ const job = await runAiModelsCatalogSync({ catalox, firestore, verifyAfter: true });
169
+ console.log(job.sync.upserted, job.verify?.ok);
170
+
171
+ // Verify only
172
+ const report = await verifyAiModelsCatalog({ catalox });
173
+ if (!report.ok) throw new Error(`catalog drift: missing=${report.missingInCatalox.length}`);
174
+ ```
65
175
 
66
176
  ## Smart model name resolution
67
177
 
@@ -139,15 +249,6 @@ npx ai-tools models resolve --model claude-sonnet --verbose
139
249
  npx ai-tools models resolve --model turbomax-9000 --json
140
250
  ```
141
251
 
142
- ## Seed catalog (Firestore + OpenRouter)
143
-
144
- ```bash
145
- # Requires .env: GOOGLE_SERVICE_ACCOUNT_BASE64, FIREBASE_PROJECT_ID
146
- # OpenRouter /models is public — no API key required
147
- npm run seed
148
- npm run seed:verbose
149
- ```
150
-
151
252
  ## Tests
152
253
 
153
254
  ```bash
@@ -158,9 +259,9 @@ npm run test:all # both
158
259
 
159
260
  ## CLI
160
261
 
262
+ Catalog update commands are documented in [Updating the model catalog](#updating-the-model-catalog). Other commands:
263
+
161
264
  ```bash
162
- npx ai-tools catalog ensure
163
- npx ai-tools sync --verbose
164
265
  npx ai-tools models list --provider openai
165
266
  npx ai-tools models list --reasoning
166
267
  npx ai-tools models resolve --model gpt4o --provider openrouter --verbose
@@ -194,6 +295,7 @@ npx ai-tools alias check
194
295
  - `@x12i/ai-tools/catalox`
195
296
  - `@x12i/ai-tools/aliases`
196
297
  - `@x12i/ai-tools/models`
298
+ - `@x12i/ai-tools/catalog` — `runAiModelsCatalogSync`, `verifyAiModelsCatalog`, bootstrap
197
299
 
198
300
  ## Project aliases
199
301
 
@@ -1,5 +1,6 @@
1
1
  import { Catalox } from '@x12i/catalox';
2
- import { l as ModelResolverOptions, a as AiModelRecord, h as ModelResolutionInput, j as ModelResolutionResult } from './types-DdGB3YaA.cjs';
2
+ import { a as AiModelRecord } from './types-BYXnCvKx.cjs';
3
+ import { h as ModelResolverOptions, M as ModelResolutionInput, f as ModelResolutionResult } from './types-CX6QFNNy.cjs';
3
4
 
4
5
  type AiModelsCatalogClientOptions = {
5
6
  catalox: Catalox;
@@ -1,5 +1,6 @@
1
1
  import { Catalox } from '@x12i/catalox';
2
- import { l as ModelResolverOptions, a as AiModelRecord, h as ModelResolutionInput, j as ModelResolutionResult } from './types-DdGB3YaA.js';
2
+ import { a as AiModelRecord } from './types-BYXnCvKx.js';
3
+ import { h as ModelResolverOptions, M as ModelResolutionInput, f as ModelResolutionResult } from './types-CuiPDcVs.js';
3
4
 
4
5
  type AiModelsCatalogClientOptions = {
5
6
  catalox: Catalox;
@@ -1,6 +1,7 @@
1
- import { d as AliasRegistry, u as ResolvedModelRef, f as AliasValidationReport } from '../types-DdGB3YaA.cjs';
2
- export { b as AliasEntry, c as AliasFileSchema, e as AliasRegistryOptions } from '../types-DdGB3YaA.cjs';
3
- import { A as AiModelsCatalogClient } from '../AiModelsCatalogClient-CSVlKql5.cjs';
1
+ import { b as AliasRegistry, j as ResolvedModelRef, d as AliasValidationReport } from '../types-CX6QFNNy.cjs';
2
+ export { A as AliasEntry, a as AliasFileSchema, c as AliasRegistryOptions } from '../types-CX6QFNNy.cjs';
3
+ import { A as AiModelsCatalogClient } from '../AiModelsCatalogClient-CNeqFiFs.cjs';
4
+ import '../types-BYXnCvKx.cjs';
4
5
  import '@x12i/catalox';
5
6
 
6
7
  type AliasResolverOptions = {
@@ -1,6 +1,7 @@
1
- import { d as AliasRegistry, u as ResolvedModelRef, f as AliasValidationReport } from '../types-DdGB3YaA.js';
2
- export { b as AliasEntry, c as AliasFileSchema, e as AliasRegistryOptions } from '../types-DdGB3YaA.js';
3
- import { A as AiModelsCatalogClient } from '../AiModelsCatalogClient-B-dNLXX0.js';
1
+ import { b as AliasRegistry, j as ResolvedModelRef, d as AliasValidationReport } from '../types-CuiPDcVs.js';
2
+ export { A as AliasEntry, a as AliasFileSchema, c as AliasRegistryOptions } from '../types-CuiPDcVs.js';
3
+ import { A as AiModelsCatalogClient } from '../AiModelsCatalogClient-nwFoEaqL.js';
4
+ import '../types-BYXnCvKx.js';
4
5
  import '@x12i/catalox';
5
6
 
6
7
  type AliasResolverOptions = {
@@ -0,0 +1,30 @@
1
+ "use strict";Object.defineProperty(exports, "__esModule", {value: true});require('../chunk-C3H7RTFR.cjs');
2
+
3
+
4
+
5
+
6
+
7
+ var _chunkHS74X2OJcjs = require('../chunk-HS74X2OJ.cjs');
8
+
9
+
10
+ var _chunkDKHGWHXPcjs = require('../chunk-DKHGWHXP.cjs');
11
+ require('../chunk-FGP3QXWL.cjs');
12
+ require('../chunk-AV6OE2YQ.cjs');
13
+ require('../chunk-F2F4UEFD.cjs');
14
+
15
+
16
+
17
+
18
+ var _chunkTF4L2NECcjs = require('../chunk-TF4L2NEC.cjs');
19
+ require('../chunk-7Q742NI3.cjs');
20
+
21
+
22
+
23
+
24
+
25
+
26
+
27
+
28
+
29
+ exports.AI_MODELS_CATALOG_ID = _chunkTF4L2NECcjs.AI_MODELS_CATALOG_ID; exports.AI_MODELS_DESCRIPTOR = _chunkTF4L2NECcjs.AI_MODELS_DESCRIPTOR; exports.AI_TOOLS_APP_ID = _chunkTF4L2NECcjs.AI_TOOLS_APP_ID; exports.CatalogSyncJobError = _chunkHS74X2OJcjs.CatalogSyncJobError; exports.ensureAiModelsCatalog = _chunkDKHGWHXPcjs.ensureAiModelsCatalog; exports.pruneStaleCatalogModels = _chunkHS74X2OJcjs.pruneStaleCatalogModels; exports.runAiModelsCatalogSync = _chunkHS74X2OJcjs.runAiModelsCatalogSync; exports.verifyAiModelsCatalog = _chunkHS74X2OJcjs.verifyAiModelsCatalog;
30
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["/Users/ami/Documents/prometheus/x12i/ai-tools/dist/catalog/index.cjs"],"names":[],"mappings":"AAAA,0GAA8B;AAC9B;AACE;AACA;AACA;AACA;AACF,yDAA8B;AAC9B;AACE;AACF,yDAA8B;AAC9B,iCAA8B;AAC9B,iCAA8B;AAC9B,iCAA8B;AAC9B;AACE;AACA;AACA;AACF,yDAA8B;AAC9B,iCAA8B;AAC9B;AACE;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACF,yjBAAC","file":"/Users/ami/Documents/prometheus/x12i/ai-tools/dist/catalog/index.cjs"}
@@ -0,0 +1,100 @@
1
+ import { Catalox, CatalogDescriptor, CataloxContext } from '@x12i/catalox';
2
+ import { a as AiModelRecord, d as OpenRouterModelsQuery } from '../types-BYXnCvKx.cjs';
3
+ import { a as SyncResult, S as SyncOptions } from '../syncAiModelsCatalog-CnXRLm2c.cjs';
4
+ import { Firestore } from 'firebase-admin/firestore';
5
+ import '../upsertAiModelRecord-CjY-sny0.cjs';
6
+
7
+ type EnsureAiModelsCatalogOptions = {
8
+ appId?: string;
9
+ catalogId?: string;
10
+ };
11
+ declare function ensureAiModelsCatalog(catalox: Catalox, options?: EnsureAiModelsCatalogOptions): Promise<void>;
12
+
13
+ declare const AI_MODELS_CATALOG_ID = "ai-models";
14
+ declare const AI_TOOLS_APP_ID = "ai-tools";
15
+ declare const AI_MODELS_DESCRIPTOR: CatalogDescriptor;
16
+
17
+ type CatalogVerifyOptions = {
18
+ catalox: Catalox;
19
+ appId?: string;
20
+ catalogId?: string;
21
+ /** When set, compare against this id set (e.g. from a sync that just ran). */
22
+ expectedModelIds?: Set<string>;
23
+ /** When set, compare against this list instead of fetching OpenRouter. */
24
+ openRouterModels?: AiModelRecord[];
25
+ openRouterApiKey?: string;
26
+ openRouterQuery?: OpenRouterModelsQuery;
27
+ /** Ensure descriptor is registered before reading (default true). */
28
+ ensureDescriptor?: boolean;
29
+ /** Max model ids to include in report samples (default 20). */
30
+ sampleLimit?: number;
31
+ };
32
+ type CatalogVerifyReport = {
33
+ ok: boolean;
34
+ openRouterCount: number;
35
+ cataloxCount: number;
36
+ missingInCatalox: string[];
37
+ extraInCatalox: string[];
38
+ supportsReasoningMissing: number;
39
+ openRouterMirrorMissing: number;
40
+ descriptorKeysMatch: boolean;
41
+ durationMs: number;
42
+ };
43
+ /**
44
+ * Compare the live OpenRouter catalog with Catalox/Firestore ai-models items.
45
+ * Use after sync in CI/cron, or standalone health checks.
46
+ */
47
+ declare function verifyAiModelsCatalog(options: CatalogVerifyOptions): Promise<CatalogVerifyReport>;
48
+
49
+ type CatalogSyncJobOptions = SyncOptions & {
50
+ /**
51
+ * After upsert, compare Catalox to OpenRouter (default true).
52
+ * Set false only when you will verify separately.
53
+ */
54
+ verifyAfter?: boolean;
55
+ /** Fail the job when verification does not pass (default true). */
56
+ failOnVerifyError?: boolean;
57
+ /** Delete Firestore rows not present in the latest OpenRouter list (default false). */
58
+ pruneStale?: boolean;
59
+ };
60
+ type CatalogSyncJobResult = {
61
+ sync: SyncResult;
62
+ verify?: CatalogVerifyReport;
63
+ prune?: {
64
+ scanned: number;
65
+ pruned: number;
66
+ prunedModelIds: string[];
67
+ };
68
+ ok: boolean;
69
+ };
70
+ declare class CatalogSyncJobError extends Error {
71
+ readonly sync: SyncResult;
72
+ readonly verify?: CatalogVerifyReport | undefined;
73
+ constructor(message: string, sync: SyncResult, verify?: CatalogVerifyReport | undefined);
74
+ }
75
+ /**
76
+ * Production entry point: sync OpenRouter → Catalox, optionally prune stale rows, then verify.
77
+ * Suitable for cron, Cloud Run jobs, and `ai-tools sync`.
78
+ */
79
+ declare function runAiModelsCatalogSync(options: CatalogSyncJobOptions): Promise<CatalogSyncJobResult>;
80
+
81
+ type PruneStaleCatalogModelsOptions = {
82
+ firestore: Firestore;
83
+ catalogId: string;
84
+ context: CataloxContext;
85
+ /** Canonical model ids from the latest OpenRouter fetch. */
86
+ activeModelIds: Set<string>;
87
+ dryRun?: boolean;
88
+ };
89
+ type PruneStaleCatalogModelsResult = {
90
+ scanned: number;
91
+ pruned: number;
92
+ prunedModelIds: string[];
93
+ };
94
+ /**
95
+ * Remove catalog documents that are no longer listed on OpenRouter.
96
+ * Off by default — enable explicitly when running production sync jobs.
97
+ */
98
+ declare function pruneStaleCatalogModels(options: PruneStaleCatalogModelsOptions): Promise<PruneStaleCatalogModelsResult>;
99
+
100
+ export { AI_MODELS_CATALOG_ID, AI_MODELS_DESCRIPTOR, AI_TOOLS_APP_ID, CatalogSyncJobError, type CatalogSyncJobOptions, type CatalogSyncJobResult, type CatalogVerifyOptions, type CatalogVerifyReport, type EnsureAiModelsCatalogOptions, type PruneStaleCatalogModelsOptions, type PruneStaleCatalogModelsResult, ensureAiModelsCatalog, pruneStaleCatalogModels, runAiModelsCatalogSync, verifyAiModelsCatalog };
@@ -0,0 +1,100 @@
1
+ import { Catalox, CatalogDescriptor, CataloxContext } from '@x12i/catalox';
2
+ import { a as AiModelRecord, d as OpenRouterModelsQuery } from '../types-BYXnCvKx.js';
3
+ import { a as SyncResult, S as SyncOptions } from '../syncAiModelsCatalog-DpkN_w7S.js';
4
+ import { Firestore } from 'firebase-admin/firestore';
5
+ import '../upsertAiModelRecord-C831wOIF.js';
6
+
7
+ type EnsureAiModelsCatalogOptions = {
8
+ appId?: string;
9
+ catalogId?: string;
10
+ };
11
+ declare function ensureAiModelsCatalog(catalox: Catalox, options?: EnsureAiModelsCatalogOptions): Promise<void>;
12
+
13
+ declare const AI_MODELS_CATALOG_ID = "ai-models";
14
+ declare const AI_TOOLS_APP_ID = "ai-tools";
15
+ declare const AI_MODELS_DESCRIPTOR: CatalogDescriptor;
16
+
17
+ type CatalogVerifyOptions = {
18
+ catalox: Catalox;
19
+ appId?: string;
20
+ catalogId?: string;
21
+ /** When set, compare against this id set (e.g. from a sync that just ran). */
22
+ expectedModelIds?: Set<string>;
23
+ /** When set, compare against this list instead of fetching OpenRouter. */
24
+ openRouterModels?: AiModelRecord[];
25
+ openRouterApiKey?: string;
26
+ openRouterQuery?: OpenRouterModelsQuery;
27
+ /** Ensure descriptor is registered before reading (default true). */
28
+ ensureDescriptor?: boolean;
29
+ /** Max model ids to include in report samples (default 20). */
30
+ sampleLimit?: number;
31
+ };
32
+ type CatalogVerifyReport = {
33
+ ok: boolean;
34
+ openRouterCount: number;
35
+ cataloxCount: number;
36
+ missingInCatalox: string[];
37
+ extraInCatalox: string[];
38
+ supportsReasoningMissing: number;
39
+ openRouterMirrorMissing: number;
40
+ descriptorKeysMatch: boolean;
41
+ durationMs: number;
42
+ };
43
+ /**
44
+ * Compare the live OpenRouter catalog with Catalox/Firestore ai-models items.
45
+ * Use after sync in CI/cron, or standalone health checks.
46
+ */
47
+ declare function verifyAiModelsCatalog(options: CatalogVerifyOptions): Promise<CatalogVerifyReport>;
48
+
49
+ type CatalogSyncJobOptions = SyncOptions & {
50
+ /**
51
+ * After upsert, compare Catalox to OpenRouter (default true).
52
+ * Set false only when you will verify separately.
53
+ */
54
+ verifyAfter?: boolean;
55
+ /** Fail the job when verification does not pass (default true). */
56
+ failOnVerifyError?: boolean;
57
+ /** Delete Firestore rows not present in the latest OpenRouter list (default false). */
58
+ pruneStale?: boolean;
59
+ };
60
+ type CatalogSyncJobResult = {
61
+ sync: SyncResult;
62
+ verify?: CatalogVerifyReport;
63
+ prune?: {
64
+ scanned: number;
65
+ pruned: number;
66
+ prunedModelIds: string[];
67
+ };
68
+ ok: boolean;
69
+ };
70
+ declare class CatalogSyncJobError extends Error {
71
+ readonly sync: SyncResult;
72
+ readonly verify?: CatalogVerifyReport | undefined;
73
+ constructor(message: string, sync: SyncResult, verify?: CatalogVerifyReport | undefined);
74
+ }
75
+ /**
76
+ * Production entry point: sync OpenRouter → Catalox, optionally prune stale rows, then verify.
77
+ * Suitable for cron, Cloud Run jobs, and `ai-tools sync`.
78
+ */
79
+ declare function runAiModelsCatalogSync(options: CatalogSyncJobOptions): Promise<CatalogSyncJobResult>;
80
+
81
+ type PruneStaleCatalogModelsOptions = {
82
+ firestore: Firestore;
83
+ catalogId: string;
84
+ context: CataloxContext;
85
+ /** Canonical model ids from the latest OpenRouter fetch. */
86
+ activeModelIds: Set<string>;
87
+ dryRun?: boolean;
88
+ };
89
+ type PruneStaleCatalogModelsResult = {
90
+ scanned: number;
91
+ pruned: number;
92
+ prunedModelIds: string[];
93
+ };
94
+ /**
95
+ * Remove catalog documents that are no longer listed on OpenRouter.
96
+ * Off by default — enable explicitly when running production sync jobs.
97
+ */
98
+ declare function pruneStaleCatalogModels(options: PruneStaleCatalogModelsOptions): Promise<PruneStaleCatalogModelsResult>;
99
+
100
+ export { AI_MODELS_CATALOG_ID, AI_MODELS_DESCRIPTOR, AI_TOOLS_APP_ID, CatalogSyncJobError, type CatalogSyncJobOptions, type CatalogSyncJobResult, type CatalogVerifyOptions, type CatalogVerifyReport, type EnsureAiModelsCatalogOptions, type PruneStaleCatalogModelsOptions, type PruneStaleCatalogModelsResult, ensureAiModelsCatalog, pruneStaleCatalogModels, runAiModelsCatalogSync, verifyAiModelsCatalog };
@@ -0,0 +1,30 @@
1
+ import "../chunk-M5TMA73F.js";
2
+ import {
3
+ CatalogSyncJobError,
4
+ pruneStaleCatalogModels,
5
+ runAiModelsCatalogSync,
6
+ verifyAiModelsCatalog
7
+ } from "../chunk-MX3AMQFC.js";
8
+ import {
9
+ ensureAiModelsCatalog
10
+ } from "../chunk-VRFVF5RH.js";
11
+ import "../chunk-HYGXZY25.js";
12
+ import "../chunk-6QGDZTGH.js";
13
+ import "../chunk-KQOALKKX.js";
14
+ import {
15
+ AI_MODELS_CATALOG_ID,
16
+ AI_MODELS_DESCRIPTOR,
17
+ AI_TOOLS_APP_ID
18
+ } from "../chunk-DJ5SWJDY.js";
19
+ import "../chunk-AJEKEWWB.js";
20
+ export {
21
+ AI_MODELS_CATALOG_ID,
22
+ AI_MODELS_DESCRIPTOR,
23
+ AI_TOOLS_APP_ID,
24
+ CatalogSyncJobError,
25
+ ensureAiModelsCatalog,
26
+ pruneStaleCatalogModels,
27
+ runAiModelsCatalogSync,
28
+ verifyAiModelsCatalog
29
+ };
30
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
@@ -4,7 +4,7 @@
4
4
 
5
5
 
6
6
 
7
- var _chunkHHNHWYTPcjs = require('../chunk-HHNHWYTP.cjs');
7
+ var _chunkFGP3QXWLcjs = require('../chunk-FGP3QXWL.cjs');
8
8
 
9
9
 
10
10
  var _chunkF2F4UEFDcjs = require('../chunk-F2F4UEFD.cjs');
@@ -17,5 +17,5 @@ require('../chunk-7Q742NI3.cjs');
17
17
 
18
18
 
19
19
 
20
- exports.AiModelsCatalogClient = _chunkF2F4UEFDcjs.AiModelsCatalogClient; exports.batchUpsertAiModelRecords = _chunkHHNHWYTPcjs.batchUpsertAiModelRecords; exports.decodeModelFirestoreDocId = _chunkHHNHWYTPcjs.decodeModelFirestoreDocId; exports.encodeModelFirestoreDocId = _chunkHHNHWYTPcjs.encodeModelFirestoreDocId; exports.sanitizeForFirestore = _chunkHHNHWYTPcjs.sanitizeForFirestore; exports.upsertAiModelRecord = _chunkHHNHWYTPcjs.upsertAiModelRecord;
20
+ exports.AiModelsCatalogClient = _chunkF2F4UEFDcjs.AiModelsCatalogClient; exports.batchUpsertAiModelRecords = _chunkFGP3QXWLcjs.batchUpsertAiModelRecords; exports.decodeModelFirestoreDocId = _chunkFGP3QXWLcjs.decodeModelFirestoreDocId; exports.encodeModelFirestoreDocId = _chunkFGP3QXWLcjs.encodeModelFirestoreDocId; exports.sanitizeForFirestore = _chunkFGP3QXWLcjs.sanitizeForFirestore; exports.upsertAiModelRecord = _chunkFGP3QXWLcjs.upsertAiModelRecord;
21
21
  //# sourceMappingURL=index.cjs.map
@@ -1,23 +1,11 @@
1
- export { A as AiModelsCatalogClient, a as AiModelsCatalogClientOptions } from '../AiModelsCatalogClient-CSVlKql5.cjs';
2
- import { Firestore } from 'firebase-admin/firestore';
3
- import { CataloxContext } from '@x12i/catalox';
4
- import { a as AiModelRecord } from '../types-DdGB3YaA.cjs';
1
+ export { A as AiModelsCatalogClient, a as AiModelsCatalogClientOptions } from '../AiModelsCatalogClient-CNeqFiFs.cjs';
2
+ export { b as batchUpsertAiModelRecords, s as sanitizeForFirestore, u as upsertAiModelRecord } from '../upsertAiModelRecord-CjY-sny0.cjs';
3
+ import '@x12i/catalox';
4
+ import '../types-BYXnCvKx.cjs';
5
+ import '../types-CX6QFNNy.cjs';
6
+ import 'firebase-admin/firestore';
5
7
 
6
8
  declare function encodeModelFirestoreDocId(modelId: string): string;
7
9
  declare function decodeModelFirestoreDocId(docId: string): string;
8
10
 
9
- /** Firestore rejects `undefined` — strip before write. */
10
- declare function sanitizeForFirestore<T>(value: T): T;
11
- type UpsertAiModelRecordOptions = {
12
- firestore: Firestore;
13
- context: CataloxContext;
14
- catalogId: string;
15
- record: AiModelRecord;
16
- };
17
- /**
18
- * Upserts one ai-models row via Firestore (safe doc ids for `provider/model` ids).
19
- */
20
- declare function upsertAiModelRecord(options: UpsertAiModelRecordOptions): Promise<void>;
21
- declare function batchUpsertAiModelRecords(firestore: Firestore, catalogId: string, context: CataloxContext, records: AiModelRecord[]): Promise<void>;
22
-
23
- export { batchUpsertAiModelRecords, decodeModelFirestoreDocId, encodeModelFirestoreDocId, sanitizeForFirestore, upsertAiModelRecord };
11
+ export { decodeModelFirestoreDocId, encodeModelFirestoreDocId };
@@ -1,23 +1,11 @@
1
- export { A as AiModelsCatalogClient, a as AiModelsCatalogClientOptions } from '../AiModelsCatalogClient-B-dNLXX0.js';
2
- import { Firestore } from 'firebase-admin/firestore';
3
- import { CataloxContext } from '@x12i/catalox';
4
- import { a as AiModelRecord } from '../types-DdGB3YaA.js';
1
+ export { A as AiModelsCatalogClient, a as AiModelsCatalogClientOptions } from '../AiModelsCatalogClient-nwFoEaqL.js';
2
+ export { b as batchUpsertAiModelRecords, s as sanitizeForFirestore, u as upsertAiModelRecord } from '../upsertAiModelRecord-C831wOIF.js';
3
+ import '@x12i/catalox';
4
+ import '../types-BYXnCvKx.js';
5
+ import '../types-CuiPDcVs.js';
6
+ import 'firebase-admin/firestore';
5
7
 
6
8
  declare function encodeModelFirestoreDocId(modelId: string): string;
7
9
  declare function decodeModelFirestoreDocId(docId: string): string;
8
10
 
9
- /** Firestore rejects `undefined` — strip before write. */
10
- declare function sanitizeForFirestore<T>(value: T): T;
11
- type UpsertAiModelRecordOptions = {
12
- firestore: Firestore;
13
- context: CataloxContext;
14
- catalogId: string;
15
- record: AiModelRecord;
16
- };
17
- /**
18
- * Upserts one ai-models row via Firestore (safe doc ids for `provider/model` ids).
19
- */
20
- declare function upsertAiModelRecord(options: UpsertAiModelRecordOptions): Promise<void>;
21
- declare function batchUpsertAiModelRecords(firestore: Firestore, catalogId: string, context: CataloxContext, records: AiModelRecord[]): Promise<void>;
22
-
23
- export { batchUpsertAiModelRecords, decodeModelFirestoreDocId, encodeModelFirestoreDocId, sanitizeForFirestore, upsertAiModelRecord };
11
+ export { decodeModelFirestoreDocId, encodeModelFirestoreDocId };
@@ -4,7 +4,7 @@ import {
4
4
  encodeModelFirestoreDocId,
5
5
  sanitizeForFirestore,
6
6
  upsertAiModelRecord
7
- } from "../chunk-ML2FRR4L.js";
7
+ } from "../chunk-HYGXZY25.js";
8
8
  import {
9
9
  AiModelsCatalogClient
10
10
  } from "../chunk-KQOALKKX.js";
@@ -0,0 +1 @@
1
+ "use strict";//# sourceMappingURL=chunk-C3H7RTFR.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["/Users/ami/Documents/prometheus/x12i/ai-tools/dist/chunk-C3H7RTFR.cjs"],"names":[],"mappings":"AAAA","file":"/Users/ami/Documents/prometheus/x12i/ai-tools/dist/chunk-C3H7RTFR.cjs"}
@@ -1,6 +1,6 @@
1
1
  "use strict";Object.defineProperty(exports, "__esModule", {value: true}); function _nullishCoalesce(lhs, rhsFn) { if (lhs != null) { return lhs; } else { return rhsFn(); } } function _optionalChain(ops) { let lastAccessLHS = undefined; let value = ops[0]; let i = 1; while (i < ops.length) { const op = ops[i]; const fn = ops[i + 1]; i += 2; if ((op === 'optionalAccess' || op === 'optionalCall') && value == null) { return undefined; } if (op === 'access' || op === 'optionalAccess') { lastAccessLHS = value; value = fn(value); } else if (op === 'call' || op === 'optionalCall') { value = fn((...args) => value.call(lastAccessLHS, ...args)); lastAccessLHS = undefined; } } return value; }
2
2
 
3
- var _chunkHHNHWYTPcjs = require('./chunk-HHNHWYTP.cjs');
3
+ var _chunkFGP3QXWLcjs = require('./chunk-FGP3QXWL.cjs');
4
4
 
5
5
 
6
6
  var _chunkAV6OE2YQcjs = require('./chunk-AV6OE2YQ.cjs');
@@ -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 _chunkHHNHWYTPcjs.batchUpsertAiModelRecords.call(void 0, options.firestore, catalogId, ctx, models);
141
- } catch (cause) {
142
- const message = cause instanceof Error ? cause.message : String(cause);
143
- throw new (0, _chunk7Q742NI3cjs.SyncError)("SYNC_UPSERT_FAILED", `Batch upsert failed: ${message}`, cause);
144
- }
140
+ const upsert = await _chunkFGP3QXWLcjs.batchUpsertAiModelRecords.call(void 0,
141
+ options.firestore,
142
+ catalogId,
143
+ ctx,
144
+ models,
145
+ { onProgress: options.onProgress }
146
+ );
145
147
  _chunkTF4L2NECcjs.invalidateModelsCache.call(void 0, appId);
148
+ if (upsert.failed.length > 0 && upsert.upserted === 0) {
149
+ throw new (0, _chunk7Q742NI3cjs.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 @@ async function syncAiModelsCatalog(options) {
157
166
 
158
167
 
159
168
  exports.ensureAiModelsCatalog = ensureAiModelsCatalog; exports.OpenRouterSyncProvider = OpenRouterSyncProvider; exports.syncAiModelsCatalog = syncAiModelsCatalog;
160
- //# sourceMappingURL=chunk-ONA73BU6.cjs.map
169
+ //# sourceMappingURL=chunk-DKHGWHXP.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["/Users/ami/Documents/prometheus/x12i/ai-tools/dist/chunk-DKHGWHXP.cjs","../src/catalog/ensureAiModelsCatalog.ts","../src/sync/OpenRouterSyncProvider.ts","../src/sync/syncAiModelsCatalog.ts"],"names":[],"mappings":"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","file":"/Users/ami/Documents/prometheus/x12i/ai-tools/dist/chunk-DKHGWHXP.cjs","sourcesContent":[null,"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"]}