@agentproto/catalog-sync 0.5.6 → 0.5.9

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.
@@ -1,4 +1,7 @@
1
1
  import { z } from 'zod';
2
+ import { existsSync, readFileSync } from 'fs';
3
+ import { dirname, join, resolve } from 'path';
4
+ import { fileURLToPath } from 'url';
2
5
 
3
6
  /**
4
7
  * @agentproto/catalog-sync v0.1.0
@@ -11,6 +14,47 @@ import { z } from 'zod';
11
14
  function defineGenerator(g) {
12
15
  return g;
13
16
  }
17
+ var __dirname$1 = dirname(fileURLToPath(import.meta.url));
18
+ function catalogSyncDir() {
19
+ return resolve(__dirname$1, "..");
20
+ }
21
+ function ledgerAbsPath(ledgerId) {
22
+ return join(catalogSyncDir(), "ledger", `${ledgerId}.json`);
23
+ }
24
+ function ledgerRelPath(ledgerId) {
25
+ return `packages/catalog-sync/ledger/${ledgerId}.json`;
26
+ }
27
+ function readLedger(ledgerId) {
28
+ const path = ledgerAbsPath(ledgerId);
29
+ if (!existsSync(path)) return {};
30
+ try {
31
+ const parsed = JSON.parse(readFileSync(path, "utf8"));
32
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return {};
33
+ return parsed;
34
+ } catch {
35
+ return {};
36
+ }
37
+ }
38
+ function serializeLedger(ledger) {
39
+ const sorted = {};
40
+ for (const id of Object.keys(ledger).sort()) sorted[id] = ledger[id];
41
+ return `${JSON.stringify(sorted, null, 2)}
42
+ `;
43
+ }
44
+ function computeAddedAtLedger(currentIds, previous, createdAt, today) {
45
+ const next = { ...previous };
46
+ for (const id of currentIds) {
47
+ if (next[id] !== void 0) continue;
48
+ next[id] = createdAt[id] ?? today;
49
+ }
50
+ return next;
51
+ }
52
+ function todayIso() {
53
+ return (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
54
+ }
55
+ function isoDateFromUnixSeconds(seconds) {
56
+ return new Date(seconds * 1e3).toISOString().slice(0, 10);
57
+ }
14
58
 
15
59
  // src/generators/llm-openrouter.ts
16
60
  var OPENROUTER_MODELS_URL = "https://openrouter.ai/api/v1/models";
@@ -23,7 +67,8 @@ var PricingSchema = z.object({
23
67
  }).passthrough();
24
68
  var ModelSchema = z.object({
25
69
  id: z.string(),
26
- pricing: PricingSchema.optional()
70
+ pricing: PricingSchema.optional(),
71
+ created: z.number().optional()
27
72
  }).passthrough();
28
73
  var SnapshotSchema = z.object({
29
74
  data: z.array(ModelSchema)
@@ -56,6 +101,9 @@ function serializeEntry(e) {
56
101
  if (e.cacheWriteMultiplier !== void 0) {
57
102
  fields.push(`cacheWriteMultiplier: ${fmt(e.cacheWriteMultiplier)}`);
58
103
  }
104
+ if (e.addedAt !== void 0) {
105
+ fields.push(`addedAt: ${JSON.stringify(e.addedAt)}`);
106
+ }
59
107
  fields.push(`vendor: ${JSON.stringify(e.vendor)}`);
60
108
  fields.push(`provider: ${JSON.stringify(e.provider)}`);
61
109
  return `{
@@ -72,6 +120,9 @@ function serializeFile(entries) {
72
120
  "// Pricing carries provider USD (inputPer1M / outputPer1M) plus cache",
73
121
  "// multipliers (cacheReadMultiplier / cacheWriteMultiplier) derived from the",
74
122
  "// source's input_cache_read / input_cache_write per-token fields when present.",
123
+ "// addedAt is the ISO date this id was first seen by a sync run \u2014 backfilled",
124
+ "// from the source's own `created` timestamp, then NEVER mutated; see",
125
+ "// packages/catalog-sync/src/added-at.ts and the package README.",
75
126
  "",
76
127
  'import type { LLMPricing } from "./catalog.js"',
77
128
  "",
@@ -91,11 +142,13 @@ function serializeFile(entries) {
91
142
  lines.push("");
92
143
  return lines.join("\n");
93
144
  }
145
+ var LEDGER_ID = "llm-openrouter";
94
146
  async function generate(ctx) {
95
147
  const src = sources[0];
96
148
  if (!src) throw new Error("llm:openrouter: no source configured");
97
149
  const parsed = SnapshotSchema.parse(await ctx.fetchSource(src));
98
150
  const entries = {};
151
+ const createdAt = {};
99
152
  for (const model of parsed.data) {
100
153
  const pricing = model.pricing;
101
154
  if (!pricing) continue;
@@ -118,8 +171,19 @@ async function generate(ctx) {
118
171
  entry.cacheWriteMultiplier = round6(cacheWritePer1M / inputPer1M);
119
172
  }
120
173
  entries[model.id] = entry;
174
+ if (model.created !== void 0) createdAt[model.id] = isoDateFromUnixSeconds(model.created);
121
175
  }
122
- return { [OUTPUT_PATH]: serializeFile(entries) };
176
+ const ledger = computeAddedAtLedger(
177
+ Object.keys(entries),
178
+ readLedger(LEDGER_ID),
179
+ createdAt,
180
+ todayIso()
181
+ );
182
+ for (const [id, entry] of Object.entries(entries)) entry.addedAt = ledger[id];
183
+ return {
184
+ [OUTPUT_PATH]: serializeFile(entries),
185
+ [ledgerRelPath(LEDGER_ID)]: serializeLedger(ledger)
186
+ };
123
187
  }
124
188
  var sources = [{ id: "llm-openrouter", url: OPENROUTER_MODELS_URL }];
125
189
  var llmOpenRouterGenerator = defineGenerator({
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/types.ts","../../src/generators/llm-openrouter.ts"],"names":[],"mappings":"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","file":"llm-openrouter.mjs","sourcesContent":["/**\n * catalog-sync generator contract.\n *\n * FROZEN — a sibling agent builds provider generators against this surface.\n * Do not change field names, shapes, or the `defineGenerator` signature\n * without coordinating. Adding new generators is the expected extension\n * path; editing this file is not.\n */\n\nexport interface CatalogSource {\n id: string\n url: string\n /** HTTP method for the live (`--refresh`) fetch. Default `\"GET\"`. */\n method?: \"GET\" | \"POST\"\n /**\n * Request headers for the live fetch. A value may embed `env:VAR_NAME`\n * tokens, resolved from `process.env` at fetch time (e.g.\n * `{ \"xi-api-key\": \"env:ELEVENLABS_API_KEY\" }` or\n * `{ Authorization: \"Bearer env:MINIMAX_API_KEY\" }`). If any referenced env\n * var is unset, the source is treated as un-refreshable — the committed\n * snapshot is reused rather than fetched without auth. Offline reads\n * (default, no `--refresh`) ignore headers entirely.\n */\n headers?: Record<string, string>\n /** JSON request body for POST sources (e.g. MiniMax `get_voice`). */\n body?: unknown\n}\n\nexport interface GeneratorContext {\n /**\n * Reads the pinned snapshot at `snapshots/<src.id>.json`; if missing and\n * `refresh=true`, fetches `src.url` and writes the snapshot. Returns\n * parsed JSON.\n *\n * Offline by default: when no snapshot exists and `refresh` is false this\n * throws — generators MUST be runnable without network access so CI and\n * tests are deterministic. The only network path is behind `--refresh`.\n */\n fetchSource(src: CatalogSource): Promise<unknown>\n refresh: boolean\n}\n\n/** Repo-relative path → TS source text. */\nexport type GeneratedFiles = Record<string, string>\n\nexport interface CatalogGenerator {\n /** e.g. \"llm:openrouter\" */\n name: string\n modality: \"llm\" | \"image\" | \"video\" | \"audio\" | \"voice\"\n sources: CatalogSource[]\n generate(ctx: GeneratorContext): Promise<GeneratedFiles>\n}\n\n/** Identity helper — lets a generator module declare itself with type inference. */\nexport function defineGenerator(g: CatalogGenerator): CatalogGenerator {\n return g\n}\n","import { z } from \"zod\"\n\nimport { defineGenerator, type GeneratedFiles, type GeneratorContext } from \"../types.js\"\n\n/**\n * PINNED source. The OpenRouter `/api/v1/models` payload lists every route\n * the router exposes with per-token USD pricing. We never fetch it at build\n * or test time — only behind `--refresh`, which writes\n * `snapshots/llm-openrouter.json` so the next offline run is deterministic.\n */\nconst OPENROUTER_MODELS_URL = \"https://openrouter.ai/api/v1/models\"\n\n/** Repo-relative drop-in target for {@link @agentproto/model-catalog}. */\nconst OUTPUT_PATH = \"packages/model-catalog/src/llm/openrouter-routes.generated.ts\"\n\n// ── Source schema ────────────────────────────────────────────────────────\n// OpenRouter prices are STRINGS of USD-per-token (e.g. \"0.000003\" = $3/1M).\n// `input_cache_read` / `input_cache_write` are optional and only present for\n// models that support prompt caching (Anthropic, etc.). These are the REAL\n// OpenRouter field names (verified against the live /v1/models payload) — an\n// earlier draft used `cache_read`/`cache_write`, which never matched, so cache\n// multipliers were silently always absent. passthrough() keeps the other live\n// keys (web_search, image, audio, internal_reasoning, input_cache_write_1h)\n// forward-compatible without a regenerate — we only read what we model.\n\nconst PricingSchema = z\n .object({\n prompt: z.string().optional(),\n completion: z.string().optional(),\n input_cache_read: z.string().optional(),\n input_cache_write: z.string().optional(),\n })\n .passthrough()\n\nconst ModelSchema = z\n .object({\n id: z.string(),\n pricing: PricingSchema.optional(),\n })\n .passthrough()\n\nconst SnapshotSchema = z.object({\n data: z.array(ModelSchema),\n})\n\n// ── Output shape (mirrors model-catalog's LLMPricing) ────────────────────\n// Drop-in for `packages/model-catalog/src/llm/openrouter-routes.generated.ts`.\n// Structurally identical to LLMPricing — inputPer1M / outputPer1M in USD per\n// 1M tokens, plus cacheReadMultiplier / cacheWriteMultiplier WHEN the source\n// carries cache_read / cache_write. The existing committed file omits cache\n// fields; this generator emits them when present (per the P3 spec: \"plus\n// cache fields if the source has them\"). Fields are optional on LLMPricing,\n// so the output remains drop-in compatible.\n\ninterface LLMPricingEntry {\n inputPer1M: number\n outputPer1M: number\n cacheReadMultiplier?: number\n cacheWriteMultiplier?: number\n vendor: string\n provider: \"openrouter\"\n}\n\n// ── Number helpers ──────────────────────────────────────────────────────\n\n/** Round to 6 decimal places, killing binary-float noise from the per-token × 1e6 product. */\nfunction round6(n: number): number {\n return Math.round(n * 1_000_000) / 1_000_000\n}\n\n/** Convert a per-token USD string to USD-per-1M-tokens, or undefined if unpriced. */\nfunction per1m(tokenPrice: string | undefined): number | undefined {\n if (tokenPrice === undefined) return undefined\n const n = Number(tokenPrice)\n if (!Number.isFinite(n)) return undefined\n return round6(n * 1_000_000)\n}\n\n/** Shortest faithful decimal string (matches existing file: 1, 0.5, 0.7448, 1.25). */\nfunction fmt(n: number): string {\n if (!Number.isFinite(n)) return \"0\"\n return `${round6(n)}`\n}\n\n/** Vendor slug = first segment of the OpenRouter route id (`vendor/model`). */\nfunction vendorOf(id: string): string {\n if (!id.includes(\"/\")) return id\n return id.split(\"/\")[0] ?? id\n}\n\n// ── Serialization ────────────────────────────────────────────────────────\n\nfunction serializeEntry(e: LLMPricingEntry): string {\n const fields: string[] = [\n `inputPer1M: ${fmt(e.inputPer1M)}`,\n `outputPer1M: ${fmt(e.outputPer1M)}`,\n ]\n if (e.cacheReadMultiplier !== undefined) {\n fields.push(`cacheReadMultiplier: ${fmt(e.cacheReadMultiplier)}`)\n }\n if (e.cacheWriteMultiplier !== undefined) {\n fields.push(`cacheWriteMultiplier: ${fmt(e.cacheWriteMultiplier)}`)\n }\n fields.push(`vendor: ${JSON.stringify(e.vendor)}`)\n fields.push(`provider: ${JSON.stringify(e.provider)}`)\n return `{\\n ${fields.join(\",\\n \")},\\n }`\n}\n\nfunction serializeFile(entries: Record<string, LLMPricingEntry>): string {\n const ids = Object.keys(entries).sort()\n const providers = Array.from(new Set(ids.map(vendorOf))).sort()\n\n const lines: string[] = [\n \"// AUTO-GENERATED by @agentproto/catalog-sync (llm:openrouter).\",\n \"// Do not edit by hand — re-run `pnpm --filter @agentproto/catalog-sync generate`.\",\n `// Source: ${OPENROUTER_MODELS_URL}`,\n \"// Pricing carries provider USD (inputPer1M / outputPer1M) plus cache\",\n \"// multipliers (cacheReadMultiplier / cacheWriteMultiplier) derived from the\",\n \"// source's input_cache_read / input_cache_write per-token fields when present.\",\n \"\",\n 'import type { LLMPricing } from \"./catalog.js\"',\n \"\",\n \"export const OPENROUTER_ROUTES: Record<string, LLMPricing> = {\",\n ]\n for (const id of ids) {\n lines.push(` ${JSON.stringify(id)}: ${serializeEntry(entries[id]!)},`)\n }\n lines.push(\"}\")\n lines.push(\"\")\n lines.push(\"/** Provider slugs treated as OpenRouter routes by the picker. */\")\n lines.push(\"export const OPENROUTER_PROVIDERS: readonly string[] = [\")\n for (const p of providers) {\n lines.push(` ${JSON.stringify(p)},`)\n }\n lines.push(\"]\")\n lines.push(\"\")\n return lines.join(\"\\n\")\n}\n\n// ── Generator ───────────────────────────────────────────────────────────\n\nasync function generate(ctx: GeneratorContext): Promise<GeneratedFiles> {\n const src = sources[0]\n if (!src) throw new Error(\"llm:openrouter: no source configured\")\n const parsed = SnapshotSchema.parse(await ctx.fetchSource(src))\n\n const entries: Record<string, LLMPricingEntry> = {}\n for (const model of parsed.data) {\n const pricing = model.pricing\n if (!pricing) continue\n const inputPer1M = per1m(pricing.prompt)\n const outputPer1M = per1m(pricing.completion)\n if (inputPer1M === undefined || outputPer1M === undefined) continue\n // Skip unpriced routes (e.g. openrouter/auto, which carries \"0\" pricing).\n if (inputPer1M === 0 && outputPer1M === 0) continue\n\n const entry: LLMPricingEntry = {\n inputPer1M,\n outputPer1M,\n vendor: vendorOf(model.id),\n provider: \"openrouter\",\n }\n\n const cacheReadPer1M = per1m(pricing.input_cache_read)\n const cacheWritePer1M = per1m(pricing.input_cache_write)\n if (cacheReadPer1M !== undefined && cacheReadPer1M > 0 && inputPer1M > 0) {\n entry.cacheReadMultiplier = round6(cacheReadPer1M / inputPer1M)\n }\n if (cacheWritePer1M !== undefined && cacheWritePer1M > 0 && inputPer1M > 0) {\n entry.cacheWriteMultiplier = round6(cacheWritePer1M / inputPer1M)\n }\n\n entries[model.id] = entry\n }\n\n return { [OUTPUT_PATH]: serializeFile(entries) }\n}\n\nconst sources = [{ id: \"llm-openrouter\", url: OPENROUTER_MODELS_URL }]\n\nexport const llmOpenRouterGenerator = defineGenerator({\n name: \"llm:openrouter\",\n modality: \"llm\",\n sources,\n generate,\n})\n"]}
1
+ {"version":3,"sources":["../../src/types.ts","../../src/added-at.ts","../../src/generators/llm-openrouter.ts"],"names":["__dirname"],"mappings":"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","file":"llm-openrouter.mjs","sourcesContent":["/**\n * catalog-sync generator contract.\n *\n * FROZEN — a sibling agent builds provider generators against this surface.\n * Do not change field names, shapes, or the `defineGenerator` signature\n * without coordinating. Adding new generators is the expected extension\n * path; editing this file is not.\n */\n\nexport interface CatalogSource {\n id: string\n url: string\n /** HTTP method for the live (`--refresh`) fetch. Default `\"GET\"`. */\n method?: \"GET\" | \"POST\"\n /**\n * Request headers for the live fetch. A value may embed `env:VAR_NAME`\n * tokens, resolved from `process.env` at fetch time (e.g.\n * `{ \"xi-api-key\": \"env:ELEVENLABS_API_KEY\" }` or\n * `{ Authorization: \"Bearer env:MINIMAX_API_KEY\" }`). If any referenced env\n * var is unset, the source is treated as un-refreshable — the committed\n * snapshot is reused rather than fetched without auth. Offline reads\n * (default, no `--refresh`) ignore headers entirely.\n */\n headers?: Record<string, string>\n /** JSON request body for POST sources (e.g. MiniMax `get_voice`). */\n body?: unknown\n}\n\nexport interface GeneratorContext {\n /**\n * Reads the pinned snapshot at `snapshots/<src.id>.json`; if missing and\n * `refresh=true`, fetches `src.url` and writes the snapshot. Returns\n * parsed JSON.\n *\n * Offline by default: when no snapshot exists and `refresh` is false this\n * throws — generators MUST be runnable without network access so CI and\n * tests are deterministic. The only network path is behind `--refresh`.\n */\n fetchSource(src: CatalogSource): Promise<unknown>\n refresh: boolean\n}\n\n/** Repo-relative path → TS source text. */\nexport type GeneratedFiles = Record<string, string>\n\nexport interface CatalogGenerator {\n /** e.g. \"llm:openrouter\" */\n name: string\n modality: \"llm\" | \"image\" | \"video\" | \"audio\" | \"voice\"\n sources: CatalogSource[]\n generate(ctx: GeneratorContext): Promise<GeneratedFiles>\n}\n\n/** Identity helper — lets a generator module declare itself with type inference. */\nexport function defineGenerator(g: CatalogGenerator): CatalogGenerator {\n return g\n}\n","/**\n * `addedAt` ledger — tracks, per generator, the ISO date each model id was\n * FIRST seen by a sync run. See `README.md` (\"addedAt convention\") for the\n * full contract; this module is the mechanism.\n *\n * Invariant: once an id has a ledger entry, that date is NEVER overwritten —\n * not even if the source's own `created` timestamp for that id later\n * resolves to something different (a provider backdating/correcting its own\n * metadata must not rewrite catalog history). New ids are backfilled from\n * the source's own creation timestamp when it has one, else stamped with\n * the current run's date.\n *\n * Ledger entries are never dropped when an id disappears from a run — the\n * ledger records \"first ever seen\", not \"currently present\". A model that\n * vanishes from a provider's list and later reappears keeps its original\n * `addedAt` rather than looking newly added.\n *\n * Each generator owns one ledger file, committed at\n * `packages/catalog-sync/ledger/<generator-id>.json` (id → ISO date, sorted\n * keys, deterministic serialization) — included as a normal entry in the\n * generator's `GeneratedFiles` return value, so `runner.ts`'s existing\n * diff/write logic covers it for free (including `--check` drift detection).\n */\n\nimport { existsSync, readFileSync } from \"node:fs\"\nimport { dirname, join, resolve } from \"node:path\"\nimport { fileURLToPath } from \"node:url\"\n\nconst __dirname = dirname(fileURLToPath(import.meta.url))\n\n/** The catalog-sync package directory — see `runner.ts`'s `catalogSyncDir`. */\nfunction catalogSyncDir(): string {\n return resolve(__dirname, \"..\")\n}\n\n/** Absolute path to a generator's committed addedAt ledger. */\nexport function ledgerAbsPath(ledgerId: string): string {\n return join(catalogSyncDir(), \"ledger\", `${ledgerId}.json`)\n}\n\n/** Repo-relative path for a ledger — for inclusion in a generator's `GeneratedFiles`. */\nexport function ledgerRelPath(ledgerId: string): string {\n return `packages/catalog-sync/ledger/${ledgerId}.json`\n}\n\nexport type AddedAtLedger = Record<string, string>\n\n/** Reads a committed ledger from disk. Missing/unparseable → empty ledger. */\nexport function readLedger(ledgerId: string): AddedAtLedger {\n const path = ledgerAbsPath(ledgerId)\n if (!existsSync(path)) return {}\n try {\n const parsed: unknown = JSON.parse(readFileSync(path, \"utf8\"))\n if (!parsed || typeof parsed !== \"object\" || Array.isArray(parsed)) return {}\n return parsed as AddedAtLedger\n } catch {\n return {}\n }\n}\n\n/** Deterministic (sorted-key) JSON serialization, matching the `*.generated.ts` style of a trailing newline. */\nexport function serializeLedger(ledger: AddedAtLedger): string {\n const sorted: AddedAtLedger = {}\n for (const id of Object.keys(ledger).sort()) sorted[id] = ledger[id]!\n return `${JSON.stringify(sorted, null, 2)}\\n`\n}\n\n/**\n * Merge the current run's ids into a ledger.\n *\n * - id already in `previous` → keep the existing stamp verbatim (never\n * mutated), regardless of what `createdAt` says this run.\n * - id new to the ledger, source has a native creation timestamp for it →\n * backfill with `createdAt[id]`.\n * - id new to the ledger, no native timestamp → stamp with `today`.\n * - ids in `previous` but absent from `currentIds` this run are kept as-is\n * (see module doc comment — the ledger is \"first ever seen\").\n */\nexport function computeAddedAtLedger(\n currentIds: readonly string[],\n previous: AddedAtLedger,\n createdAt: Readonly<Record<string, string>>,\n today: string\n): AddedAtLedger {\n const next: AddedAtLedger = { ...previous }\n for (const id of currentIds) {\n if (next[id] !== undefined) continue\n next[id] = createdAt[id] ?? today\n }\n return next\n}\n\n/** Today's date as `YYYY-MM-DD`, in UTC. */\nexport function todayIso(): string {\n return new Date().toISOString().slice(0, 10)\n}\n\n/** Converts a provider's Unix-seconds creation timestamp to `YYYY-MM-DD`. */\nexport function isoDateFromUnixSeconds(seconds: number): string {\n return new Date(seconds * 1000).toISOString().slice(0, 10)\n}\n","import { z } from \"zod\"\n\nimport { defineGenerator, type GeneratedFiles, type GeneratorContext } from \"../types.js\"\nimport {\n computeAddedAtLedger,\n isoDateFromUnixSeconds,\n ledgerRelPath,\n readLedger,\n serializeLedger,\n todayIso,\n} from \"../added-at.js\"\n\n/**\n * PINNED source. The OpenRouter `/api/v1/models` payload lists every route\n * the router exposes with per-token USD pricing. We never fetch it at build\n * or test time — only behind `--refresh`, which writes\n * `snapshots/llm-openrouter.json` so the next offline run is deterministic.\n */\nconst OPENROUTER_MODELS_URL = \"https://openrouter.ai/api/v1/models\"\n\n/** Repo-relative drop-in target for {@link @agentproto/model-catalog}. */\nconst OUTPUT_PATH = \"packages/model-catalog/src/llm/openrouter-routes.generated.ts\"\n\n// ── Source schema ────────────────────────────────────────────────────────\n// OpenRouter prices are STRINGS of USD-per-token (e.g. \"0.000003\" = $3/1M).\n// `input_cache_read` / `input_cache_write` are optional and only present for\n// models that support prompt caching (Anthropic, etc.). These are the REAL\n// OpenRouter field names (verified against the live /v1/models payload) — an\n// earlier draft used `cache_read`/`cache_write`, which never matched, so cache\n// multipliers were silently always absent. passthrough() keeps the other live\n// keys (web_search, image, audio, internal_reasoning, input_cache_write_1h)\n// forward-compatible without a regenerate — we only read what we model.\n\nconst PricingSchema = z\n .object({\n prompt: z.string().optional(),\n completion: z.string().optional(),\n input_cache_read: z.string().optional(),\n input_cache_write: z.string().optional(),\n })\n .passthrough()\n\n// `created` is Unix seconds — OpenRouter's own route-creation timestamp,\n// used to backfill `addedAt` for ids not already in the ledger (see\n// `../added-at.ts`). Verified present on the live payload (2026-08-31).\nconst ModelSchema = z\n .object({\n id: z.string(),\n pricing: PricingSchema.optional(),\n created: z.number().optional(),\n })\n .passthrough()\n\nconst SnapshotSchema = z.object({\n data: z.array(ModelSchema),\n})\n\n// ── Output shape (mirrors model-catalog's LLMPricing) ────────────────────\n// Drop-in for `packages/model-catalog/src/llm/openrouter-routes.generated.ts`.\n// Structurally identical to LLMPricing — inputPer1M / outputPer1M in USD per\n// 1M tokens, plus cacheReadMultiplier / cacheWriteMultiplier WHEN the source\n// carries cache_read / cache_write. The existing committed file omits cache\n// fields; this generator emits them when present (per the P3 spec: \"plus\n// cache fields if the source has them\"). Fields are optional on LLMPricing,\n// so the output remains drop-in compatible.\n\ninterface LLMPricingEntry {\n inputPer1M: number\n outputPer1M: number\n cacheReadMultiplier?: number\n cacheWriteMultiplier?: number\n /** ISO date this id was first seen by a sync run. See `../added-at.ts`. */\n addedAt?: string\n vendor: string\n provider: \"openrouter\"\n}\n\n// ── Number helpers ──────────────────────────────────────────────────────\n\n/** Round to 6 decimal places, killing binary-float noise from the per-token × 1e6 product. */\nfunction round6(n: number): number {\n return Math.round(n * 1_000_000) / 1_000_000\n}\n\n/** Convert a per-token USD string to USD-per-1M-tokens, or undefined if unpriced. */\nfunction per1m(tokenPrice: string | undefined): number | undefined {\n if (tokenPrice === undefined) return undefined\n const n = Number(tokenPrice)\n if (!Number.isFinite(n)) return undefined\n return round6(n * 1_000_000)\n}\n\n/** Shortest faithful decimal string (matches existing file: 1, 0.5, 0.7448, 1.25). */\nfunction fmt(n: number): string {\n if (!Number.isFinite(n)) return \"0\"\n return `${round6(n)}`\n}\n\n/** Vendor slug = first segment of the OpenRouter route id (`vendor/model`). */\nfunction vendorOf(id: string): string {\n if (!id.includes(\"/\")) return id\n return id.split(\"/\")[0] ?? id\n}\n\n// ── Serialization ────────────────────────────────────────────────────────\n\nfunction serializeEntry(e: LLMPricingEntry): string {\n const fields: string[] = [\n `inputPer1M: ${fmt(e.inputPer1M)}`,\n `outputPer1M: ${fmt(e.outputPer1M)}`,\n ]\n if (e.cacheReadMultiplier !== undefined) {\n fields.push(`cacheReadMultiplier: ${fmt(e.cacheReadMultiplier)}`)\n }\n if (e.cacheWriteMultiplier !== undefined) {\n fields.push(`cacheWriteMultiplier: ${fmt(e.cacheWriteMultiplier)}`)\n }\n if (e.addedAt !== undefined) {\n fields.push(`addedAt: ${JSON.stringify(e.addedAt)}`)\n }\n fields.push(`vendor: ${JSON.stringify(e.vendor)}`)\n fields.push(`provider: ${JSON.stringify(e.provider)}`)\n return `{\\n ${fields.join(\",\\n \")},\\n }`\n}\n\nfunction serializeFile(entries: Record<string, LLMPricingEntry>): string {\n const ids = Object.keys(entries).sort()\n const providers = Array.from(new Set(ids.map(vendorOf))).sort()\n\n const lines: string[] = [\n \"// AUTO-GENERATED by @agentproto/catalog-sync (llm:openrouter).\",\n \"// Do not edit by hand — re-run `pnpm --filter @agentproto/catalog-sync generate`.\",\n `// Source: ${OPENROUTER_MODELS_URL}`,\n \"// Pricing carries provider USD (inputPer1M / outputPer1M) plus cache\",\n \"// multipliers (cacheReadMultiplier / cacheWriteMultiplier) derived from the\",\n \"// source's input_cache_read / input_cache_write per-token fields when present.\",\n \"// addedAt is the ISO date this id was first seen by a sync run — backfilled\",\n \"// from the source's own `created` timestamp, then NEVER mutated; see\",\n \"// packages/catalog-sync/src/added-at.ts and the package README.\",\n \"\",\n 'import type { LLMPricing } from \"./catalog.js\"',\n \"\",\n \"export const OPENROUTER_ROUTES: Record<string, LLMPricing> = {\",\n ]\n for (const id of ids) {\n lines.push(` ${JSON.stringify(id)}: ${serializeEntry(entries[id]!)},`)\n }\n lines.push(\"}\")\n lines.push(\"\")\n lines.push(\"/** Provider slugs treated as OpenRouter routes by the picker. */\")\n lines.push(\"export const OPENROUTER_PROVIDERS: readonly string[] = [\")\n for (const p of providers) {\n lines.push(` ${JSON.stringify(p)},`)\n }\n lines.push(\"]\")\n lines.push(\"\")\n return lines.join(\"\\n\")\n}\n\n// ── Generator ───────────────────────────────────────────────────────────\n\nconst LEDGER_ID = \"llm-openrouter\"\n\nasync function generate(ctx: GeneratorContext): Promise<GeneratedFiles> {\n const src = sources[0]\n if (!src) throw new Error(\"llm:openrouter: no source configured\")\n const parsed = SnapshotSchema.parse(await ctx.fetchSource(src))\n\n const entries: Record<string, LLMPricingEntry> = {}\n const createdAt: Record<string, string> = {}\n for (const model of parsed.data) {\n const pricing = model.pricing\n if (!pricing) continue\n const inputPer1M = per1m(pricing.prompt)\n const outputPer1M = per1m(pricing.completion)\n if (inputPer1M === undefined || outputPer1M === undefined) continue\n // Skip unpriced routes (e.g. openrouter/auto, which carries \"0\" pricing).\n if (inputPer1M === 0 && outputPer1M === 0) continue\n\n const entry: LLMPricingEntry = {\n inputPer1M,\n outputPer1M,\n vendor: vendorOf(model.id),\n provider: \"openrouter\",\n }\n\n const cacheReadPer1M = per1m(pricing.input_cache_read)\n const cacheWritePer1M = per1m(pricing.input_cache_write)\n if (cacheReadPer1M !== undefined && cacheReadPer1M > 0 && inputPer1M > 0) {\n entry.cacheReadMultiplier = round6(cacheReadPer1M / inputPer1M)\n }\n if (cacheWritePer1M !== undefined && cacheWritePer1M > 0 && inputPer1M > 0) {\n entry.cacheWriteMultiplier = round6(cacheWritePer1M / inputPer1M)\n }\n\n entries[model.id] = entry\n if (model.created !== undefined) createdAt[model.id] = isoDateFromUnixSeconds(model.created)\n }\n\n const ledger = computeAddedAtLedger(\n Object.keys(entries),\n readLedger(LEDGER_ID),\n createdAt,\n todayIso()\n )\n for (const [id, entry] of Object.entries(entries)) entry.addedAt = ledger[id]\n\n return {\n [OUTPUT_PATH]: serializeFile(entries),\n [ledgerRelPath(LEDGER_ID)]: serializeLedger(ledger),\n }\n}\n\nconst sources = [{ id: \"llm-openrouter\", url: OPENROUTER_MODELS_URL }]\n\nexport const llmOpenRouterGenerator = defineGenerator({\n name: \"llm:openrouter\",\n modality: \"llm\",\n sources,\n generate,\n})\n"]}
@@ -1,4 +1,7 @@
1
1
  import { z } from 'zod';
2
+ import { existsSync, readFileSync } from 'fs';
3
+ import { dirname, join, resolve } from 'path';
4
+ import { fileURLToPath } from 'url';
2
5
 
3
6
  /**
4
7
  * @agentproto/catalog-sync v0.1.0
@@ -11,6 +14,47 @@ import { z } from 'zod';
11
14
  function defineGenerator(g) {
12
15
  return g;
13
16
  }
17
+ var __dirname$1 = dirname(fileURLToPath(import.meta.url));
18
+ function catalogSyncDir() {
19
+ return resolve(__dirname$1, "..");
20
+ }
21
+ function ledgerAbsPath(ledgerId) {
22
+ return join(catalogSyncDir(), "ledger", `${ledgerId}.json`);
23
+ }
24
+ function ledgerRelPath(ledgerId) {
25
+ return `packages/catalog-sync/ledger/${ledgerId}.json`;
26
+ }
27
+ function readLedger(ledgerId) {
28
+ const path = ledgerAbsPath(ledgerId);
29
+ if (!existsSync(path)) return {};
30
+ try {
31
+ const parsed = JSON.parse(readFileSync(path, "utf8"));
32
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return {};
33
+ return parsed;
34
+ } catch {
35
+ return {};
36
+ }
37
+ }
38
+ function serializeLedger(ledger) {
39
+ const sorted = {};
40
+ for (const id of Object.keys(ledger).sort()) sorted[id] = ledger[id];
41
+ return `${JSON.stringify(sorted, null, 2)}
42
+ `;
43
+ }
44
+ function computeAddedAtLedger(currentIds, previous, createdAt, today) {
45
+ const next = { ...previous };
46
+ for (const id of currentIds) {
47
+ if (next[id] !== void 0) continue;
48
+ next[id] = createdAt[id] ?? today;
49
+ }
50
+ return next;
51
+ }
52
+ function todayIso() {
53
+ return (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
54
+ }
55
+ function isoDateFromUnixSeconds(seconds) {
56
+ return new Date(seconds * 1e3).toISOString().slice(0, 10);
57
+ }
14
58
 
15
59
  // src/generators/llm-requesty.ts
16
60
  var REQUESTY_MODELS_URL = "https://router.requesty.ai/v1/models";
@@ -20,7 +64,8 @@ var ModelSchema = z.object({
20
64
  input_price: z.number().optional(),
21
65
  output_price: z.number().optional(),
22
66
  cached_price: z.number().optional(),
23
- supports_caching: z.boolean().optional()
67
+ supports_caching: z.boolean().optional(),
68
+ created: z.number().optional()
24
69
  }).passthrough();
25
70
  var SnapshotSchema = z.object({
26
71
  data: z.array(ModelSchema)
@@ -49,6 +94,9 @@ function serializeEntry(e) {
49
94
  if (e.cacheReadMultiplier !== void 0) {
50
95
  fields.push(`cacheReadMultiplier: ${fmt(e.cacheReadMultiplier)}`);
51
96
  }
97
+ if (e.addedAt !== void 0) {
98
+ fields.push(`addedAt: ${JSON.stringify(e.addedAt)}`);
99
+ }
52
100
  fields.push(`vendor: ${JSON.stringify(e.vendor)}`);
53
101
  fields.push(`provider: ${JSON.stringify(e.provider)}`);
54
102
  return `{
@@ -66,6 +114,9 @@ function serializeFile(entries) {
66
114
  "// multiplier (cacheReadMultiplier) derived from the source's cached_price",
67
115
  "// per-token field when `supports_caching` is true. Requesty has no",
68
116
  "// cache-write price, so cacheWriteMultiplier is never emitted.",
117
+ "// addedAt is the ISO date this id was first seen by a sync run \u2014 backfilled",
118
+ "// from the source's own `created` timestamp, then NEVER mutated; see",
119
+ "// packages/catalog-sync/src/added-at.ts and the package README.",
69
120
  "",
70
121
  'import type { LLMPricing } from "./catalog.js"',
71
122
  "",
@@ -85,11 +136,13 @@ function serializeFile(entries) {
85
136
  lines.push("");
86
137
  return lines.join("\n");
87
138
  }
139
+ var LEDGER_ID = "llm-requesty";
88
140
  async function generate(ctx) {
89
141
  const src = sources[0];
90
142
  if (!src) throw new Error("llm:requesty: no source configured");
91
143
  const parsed = SnapshotSchema.parse(await ctx.fetchSource(src));
92
144
  const entries = {};
145
+ const createdAt = {};
93
146
  for (const model of parsed.data) {
94
147
  const inputPer1M = per1m(model.input_price);
95
148
  const outputPer1M = per1m(model.output_price);
@@ -108,8 +161,19 @@ async function generate(ctx) {
108
161
  }
109
162
  }
110
163
  entries[model.id] = entry;
164
+ if (model.created !== void 0) createdAt[model.id] = isoDateFromUnixSeconds(model.created);
111
165
  }
112
- return { [OUTPUT_PATH]: serializeFile(entries) };
166
+ const ledger = computeAddedAtLedger(
167
+ Object.keys(entries),
168
+ readLedger(LEDGER_ID),
169
+ createdAt,
170
+ todayIso()
171
+ );
172
+ for (const [id, entry] of Object.entries(entries)) entry.addedAt = ledger[id];
173
+ return {
174
+ [OUTPUT_PATH]: serializeFile(entries),
175
+ [ledgerRelPath(LEDGER_ID)]: serializeLedger(ledger)
176
+ };
113
177
  }
114
178
  var sources = [
115
179
  {
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/types.ts","../../src/generators/llm-requesty.ts"],"names":[],"mappings":"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","file":"llm-requesty.mjs","sourcesContent":["/**\n * catalog-sync generator contract.\n *\n * FROZEN — a sibling agent builds provider generators against this surface.\n * Do not change field names, shapes, or the `defineGenerator` signature\n * without coordinating. Adding new generators is the expected extension\n * path; editing this file is not.\n */\n\nexport interface CatalogSource {\n id: string\n url: string\n /** HTTP method for the live (`--refresh`) fetch. Default `\"GET\"`. */\n method?: \"GET\" | \"POST\"\n /**\n * Request headers for the live fetch. A value may embed `env:VAR_NAME`\n * tokens, resolved from `process.env` at fetch time (e.g.\n * `{ \"xi-api-key\": \"env:ELEVENLABS_API_KEY\" }` or\n * `{ Authorization: \"Bearer env:MINIMAX_API_KEY\" }`). If any referenced env\n * var is unset, the source is treated as un-refreshable — the committed\n * snapshot is reused rather than fetched without auth. Offline reads\n * (default, no `--refresh`) ignore headers entirely.\n */\n headers?: Record<string, string>\n /** JSON request body for POST sources (e.g. MiniMax `get_voice`). */\n body?: unknown\n}\n\nexport interface GeneratorContext {\n /**\n * Reads the pinned snapshot at `snapshots/<src.id>.json`; if missing and\n * `refresh=true`, fetches `src.url` and writes the snapshot. Returns\n * parsed JSON.\n *\n * Offline by default: when no snapshot exists and `refresh` is false this\n * throws — generators MUST be runnable without network access so CI and\n * tests are deterministic. The only network path is behind `--refresh`.\n */\n fetchSource(src: CatalogSource): Promise<unknown>\n refresh: boolean\n}\n\n/** Repo-relative path → TS source text. */\nexport type GeneratedFiles = Record<string, string>\n\nexport interface CatalogGenerator {\n /** e.g. \"llm:openrouter\" */\n name: string\n modality: \"llm\" | \"image\" | \"video\" | \"audio\" | \"voice\"\n sources: CatalogSource[]\n generate(ctx: GeneratorContext): Promise<GeneratedFiles>\n}\n\n/** Identity helper — lets a generator module declare itself with type inference. */\nexport function defineGenerator(g: CatalogGenerator): CatalogGenerator {\n return g\n}\n","import { z } from \"zod\"\n\nimport { defineGenerator, type GeneratedFiles, type GeneratorContext } from \"../types.js\"\n\n/**\n * PINNED source. The Requesty `/v1/models` payload lists every route the\n * router exposes with per-token USD pricing. We never fetch it at build or\n * test time — only behind `--refresh`, which writes\n * `snapshots/llm-requesty.json` so the next offline run is deterministic.\n * Live fetch requires `Authorization: Bearer $REQUESTY_API_KEY` (see\n * `sources` below) — without the key set, the framework reuses the\n * committed snapshot rather than fetching unauthenticated.\n */\nconst REQUESTY_MODELS_URL = \"https://router.requesty.ai/v1/models\"\n\n/** Repo-relative drop-in target for {@link @agentproto/model-catalog}. */\nconst OUTPUT_PATH = \"packages/model-catalog/src/llm/requesty-routes.generated.ts\"\n\n// ── Source schema ────────────────────────────────────────────────────────\n// UNLIKE OpenRouter, Requesty prices are JSON **numbers** in USD-PER-TOKEN\n// (e.g. `4e-7` = $0.40/1M), not strings, and live flat on the model entry\n// rather than nested under a `pricing` object. `cached_price` is a cache\n// READ price (no cache-write price exists in this source — we never emit\n// `cacheWriteMultiplier`). `supports_caching` gates whether `cached_price`\n// is actually meaningful: some entries carry a non-zero `cached_price` with\n// `supports_caching: false` (cache pricing published but not offered), so\n// the multiplier must only be derived when the flag is true. passthrough()\n// keeps the other live keys (context_window, supports_vision, description,\n// …) forward-compatible without a regenerate — we only read what we model.\n\nconst ModelSchema = z\n .object({\n id: z.string(),\n input_price: z.number().optional(),\n output_price: z.number().optional(),\n cached_price: z.number().optional(),\n supports_caching: z.boolean().optional(),\n })\n .passthrough()\n\nconst SnapshotSchema = z.object({\n data: z.array(ModelSchema),\n})\n\n// ── Output shape (mirrors model-catalog's LLMPricing) ────────────────────\n// Drop-in for `packages/model-catalog/src/llm/requesty-routes.generated.ts`.\n// Structurally identical to LLMPricing — inputPer1M / outputPer1M in USD per\n// 1M tokens, plus cacheReadMultiplier when the source carries a meaningful\n// `cached_price`. No `cacheWriteMultiplier` — Requesty has no cache-write\n// price to derive one from.\n\ninterface LLMPricingEntry {\n inputPer1M: number\n outputPer1M: number\n cacheReadMultiplier?: number\n vendor: string\n provider: \"requesty\"\n}\n\n// ── Number helpers ──────────────────────────────────────────────────────\n\n/** Round to 6 decimal places, killing binary-float noise from the per-token × 1e6 product. */\nfunction round6(n: number): number {\n return Math.round(n * 1_000_000) / 1_000_000\n}\n\n/**\n * Convert a per-token USD number to USD-per-1M-tokens, or undefined if\n * unpriced. UNLIKE OpenRouter (per-token USD strings), Requesty already\n * gives per-token USD as a JSON number, so there's no `Number(...)` parse\n * step — just the same ×1e6 + round6 normalization.\n */\nfunction per1m(tokenPrice: number | undefined): number | undefined {\n if (tokenPrice === undefined) return undefined\n if (!Number.isFinite(tokenPrice)) return undefined\n return round6(tokenPrice * 1_000_000)\n}\n\n/** Shortest faithful decimal string (matches existing file: 1, 0.5, 0.7448, 1.25). */\nfunction fmt(n: number): string {\n if (!Number.isFinite(n)) return \"0\"\n return `${round6(n)}`\n}\n\n/** Vendor slug = first segment of the Requesty route id (`vendor/model`). */\nfunction vendorOf(id: string): string {\n if (!id.includes(\"/\")) return id\n return id.split(\"/\")[0] ?? id\n}\n\n// ── Serialization ────────────────────────────────────────────────────────\n\nfunction serializeEntry(e: LLMPricingEntry): string {\n const fields: string[] = [\n `inputPer1M: ${fmt(e.inputPer1M)}`,\n `outputPer1M: ${fmt(e.outputPer1M)}`,\n ]\n if (e.cacheReadMultiplier !== undefined) {\n fields.push(`cacheReadMultiplier: ${fmt(e.cacheReadMultiplier)}`)\n }\n fields.push(`vendor: ${JSON.stringify(e.vendor)}`)\n fields.push(`provider: ${JSON.stringify(e.provider)}`)\n return `{\\n ${fields.join(\",\\n \")},\\n }`\n}\n\nfunction serializeFile(entries: Record<string, LLMPricingEntry>): string {\n const ids = Object.keys(entries).sort()\n const providers = Array.from(new Set(ids.map(vendorOf))).sort()\n\n const lines: string[] = [\n \"// AUTO-GENERATED by @agentproto/catalog-sync (llm:requesty).\",\n \"// Do not edit by hand — re-run `pnpm --filter @agentproto/catalog-sync generate`.\",\n `// Source: ${REQUESTY_MODELS_URL}`,\n \"// Pricing carries provider USD (inputPer1M / outputPer1M) plus a cache\",\n \"// multiplier (cacheReadMultiplier) derived from the source's cached_price\",\n \"// per-token field when `supports_caching` is true. Requesty has no\",\n \"// cache-write price, so cacheWriteMultiplier is never emitted.\",\n \"\",\n 'import type { LLMPricing } from \"./catalog.js\"',\n \"\",\n \"export const REQUESTY_ROUTES: Record<string, LLMPricing> = {\",\n ]\n for (const id of ids) {\n lines.push(` ${JSON.stringify(id)}: ${serializeEntry(entries[id]!)},`)\n }\n lines.push(\"}\")\n lines.push(\"\")\n lines.push(\"/** Provider slugs treated as Requesty routes by the picker. */\")\n lines.push(\"export const REQUESTY_PROVIDERS: readonly string[] = [\")\n for (const p of providers) {\n lines.push(` ${JSON.stringify(p)},`)\n }\n lines.push(\"]\")\n lines.push(\"\")\n return lines.join(\"\\n\")\n}\n\n// ── Generator ───────────────────────────────────────────────────────────\n\nasync function generate(ctx: GeneratorContext): Promise<GeneratedFiles> {\n const src = sources[0]\n if (!src) throw new Error(\"llm:requesty: no source configured\")\n const parsed = SnapshotSchema.parse(await ctx.fetchSource(src))\n\n const entries: Record<string, LLMPricingEntry> = {}\n for (const model of parsed.data) {\n const inputPer1M = per1m(model.input_price)\n const outputPer1M = per1m(model.output_price)\n if (inputPer1M === undefined || outputPer1M === undefined) continue\n // Skip unpriced routes.\n if (inputPer1M === 0 && outputPer1M === 0) continue\n\n const entry: LLMPricingEntry = {\n inputPer1M,\n outputPer1M,\n vendor: vendorOf(model.id),\n provider: \"requesty\",\n }\n\n if (model.supports_caching === true && inputPer1M > 0) {\n const cachePer1M = per1m(model.cached_price)\n if (cachePer1M !== undefined) {\n entry.cacheReadMultiplier = round6(cachePer1M / inputPer1M)\n }\n }\n\n entries[model.id] = entry\n }\n\n return { [OUTPUT_PATH]: serializeFile(entries) }\n}\n\nconst sources = [\n {\n id: \"llm-requesty\",\n url: REQUESTY_MODELS_URL,\n // Live refresh needs the account key (Bearer). Without REQUESTY_API_KEY\n // set, the framework reuses the committed snapshot.\n headers: { Authorization: \"Bearer env:REQUESTY_API_KEY\" },\n },\n]\n\nexport const llmRequestyGenerator = defineGenerator({\n name: \"llm:requesty\",\n modality: \"llm\",\n sources,\n generate,\n})\n"]}
1
+ {"version":3,"sources":["../../src/types.ts","../../src/added-at.ts","../../src/generators/llm-requesty.ts"],"names":["__dirname"],"mappings":"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","file":"llm-requesty.mjs","sourcesContent":["/**\n * catalog-sync generator contract.\n *\n * FROZEN — a sibling agent builds provider generators against this surface.\n * Do not change field names, shapes, or the `defineGenerator` signature\n * without coordinating. Adding new generators is the expected extension\n * path; editing this file is not.\n */\n\nexport interface CatalogSource {\n id: string\n url: string\n /** HTTP method for the live (`--refresh`) fetch. Default `\"GET\"`. */\n method?: \"GET\" | \"POST\"\n /**\n * Request headers for the live fetch. A value may embed `env:VAR_NAME`\n * tokens, resolved from `process.env` at fetch time (e.g.\n * `{ \"xi-api-key\": \"env:ELEVENLABS_API_KEY\" }` or\n * `{ Authorization: \"Bearer env:MINIMAX_API_KEY\" }`). If any referenced env\n * var is unset, the source is treated as un-refreshable — the committed\n * snapshot is reused rather than fetched without auth. Offline reads\n * (default, no `--refresh`) ignore headers entirely.\n */\n headers?: Record<string, string>\n /** JSON request body for POST sources (e.g. MiniMax `get_voice`). */\n body?: unknown\n}\n\nexport interface GeneratorContext {\n /**\n * Reads the pinned snapshot at `snapshots/<src.id>.json`; if missing and\n * `refresh=true`, fetches `src.url` and writes the snapshot. Returns\n * parsed JSON.\n *\n * Offline by default: when no snapshot exists and `refresh` is false this\n * throws — generators MUST be runnable without network access so CI and\n * tests are deterministic. The only network path is behind `--refresh`.\n */\n fetchSource(src: CatalogSource): Promise<unknown>\n refresh: boolean\n}\n\n/** Repo-relative path → TS source text. */\nexport type GeneratedFiles = Record<string, string>\n\nexport interface CatalogGenerator {\n /** e.g. \"llm:openrouter\" */\n name: string\n modality: \"llm\" | \"image\" | \"video\" | \"audio\" | \"voice\"\n sources: CatalogSource[]\n generate(ctx: GeneratorContext): Promise<GeneratedFiles>\n}\n\n/** Identity helper — lets a generator module declare itself with type inference. */\nexport function defineGenerator(g: CatalogGenerator): CatalogGenerator {\n return g\n}\n","/**\n * `addedAt` ledger — tracks, per generator, the ISO date each model id was\n * FIRST seen by a sync run. See `README.md` (\"addedAt convention\") for the\n * full contract; this module is the mechanism.\n *\n * Invariant: once an id has a ledger entry, that date is NEVER overwritten —\n * not even if the source's own `created` timestamp for that id later\n * resolves to something different (a provider backdating/correcting its own\n * metadata must not rewrite catalog history). New ids are backfilled from\n * the source's own creation timestamp when it has one, else stamped with\n * the current run's date.\n *\n * Ledger entries are never dropped when an id disappears from a run — the\n * ledger records \"first ever seen\", not \"currently present\". A model that\n * vanishes from a provider's list and later reappears keeps its original\n * `addedAt` rather than looking newly added.\n *\n * Each generator owns one ledger file, committed at\n * `packages/catalog-sync/ledger/<generator-id>.json` (id → ISO date, sorted\n * keys, deterministic serialization) — included as a normal entry in the\n * generator's `GeneratedFiles` return value, so `runner.ts`'s existing\n * diff/write logic covers it for free (including `--check` drift detection).\n */\n\nimport { existsSync, readFileSync } from \"node:fs\"\nimport { dirname, join, resolve } from \"node:path\"\nimport { fileURLToPath } from \"node:url\"\n\nconst __dirname = dirname(fileURLToPath(import.meta.url))\n\n/** The catalog-sync package directory — see `runner.ts`'s `catalogSyncDir`. */\nfunction catalogSyncDir(): string {\n return resolve(__dirname, \"..\")\n}\n\n/** Absolute path to a generator's committed addedAt ledger. */\nexport function ledgerAbsPath(ledgerId: string): string {\n return join(catalogSyncDir(), \"ledger\", `${ledgerId}.json`)\n}\n\n/** Repo-relative path for a ledger — for inclusion in a generator's `GeneratedFiles`. */\nexport function ledgerRelPath(ledgerId: string): string {\n return `packages/catalog-sync/ledger/${ledgerId}.json`\n}\n\nexport type AddedAtLedger = Record<string, string>\n\n/** Reads a committed ledger from disk. Missing/unparseable → empty ledger. */\nexport function readLedger(ledgerId: string): AddedAtLedger {\n const path = ledgerAbsPath(ledgerId)\n if (!existsSync(path)) return {}\n try {\n const parsed: unknown = JSON.parse(readFileSync(path, \"utf8\"))\n if (!parsed || typeof parsed !== \"object\" || Array.isArray(parsed)) return {}\n return parsed as AddedAtLedger\n } catch {\n return {}\n }\n}\n\n/** Deterministic (sorted-key) JSON serialization, matching the `*.generated.ts` style of a trailing newline. */\nexport function serializeLedger(ledger: AddedAtLedger): string {\n const sorted: AddedAtLedger = {}\n for (const id of Object.keys(ledger).sort()) sorted[id] = ledger[id]!\n return `${JSON.stringify(sorted, null, 2)}\\n`\n}\n\n/**\n * Merge the current run's ids into a ledger.\n *\n * - id already in `previous` → keep the existing stamp verbatim (never\n * mutated), regardless of what `createdAt` says this run.\n * - id new to the ledger, source has a native creation timestamp for it →\n * backfill with `createdAt[id]`.\n * - id new to the ledger, no native timestamp → stamp with `today`.\n * - ids in `previous` but absent from `currentIds` this run are kept as-is\n * (see module doc comment — the ledger is \"first ever seen\").\n */\nexport function computeAddedAtLedger(\n currentIds: readonly string[],\n previous: AddedAtLedger,\n createdAt: Readonly<Record<string, string>>,\n today: string\n): AddedAtLedger {\n const next: AddedAtLedger = { ...previous }\n for (const id of currentIds) {\n if (next[id] !== undefined) continue\n next[id] = createdAt[id] ?? today\n }\n return next\n}\n\n/** Today's date as `YYYY-MM-DD`, in UTC. */\nexport function todayIso(): string {\n return new Date().toISOString().slice(0, 10)\n}\n\n/** Converts a provider's Unix-seconds creation timestamp to `YYYY-MM-DD`. */\nexport function isoDateFromUnixSeconds(seconds: number): string {\n return new Date(seconds * 1000).toISOString().slice(0, 10)\n}\n","import { z } from \"zod\"\n\nimport { defineGenerator, type GeneratedFiles, type GeneratorContext } from \"../types.js\"\nimport {\n computeAddedAtLedger,\n isoDateFromUnixSeconds,\n ledgerRelPath,\n readLedger,\n serializeLedger,\n todayIso,\n} from \"../added-at.js\"\n\n/**\n * PINNED source. The Requesty `/v1/models` payload lists every route the\n * router exposes with per-token USD pricing. We never fetch it at build or\n * test time — only behind `--refresh`, which writes\n * `snapshots/llm-requesty.json` so the next offline run is deterministic.\n * Live fetch requires `Authorization: Bearer $REQUESTY_API_KEY` (see\n * `sources` below) — without the key set, the framework reuses the\n * committed snapshot rather than fetching unauthenticated.\n */\nconst REQUESTY_MODELS_URL = \"https://router.requesty.ai/v1/models\"\n\n/** Repo-relative drop-in target for {@link @agentproto/model-catalog}. */\nconst OUTPUT_PATH = \"packages/model-catalog/src/llm/requesty-routes.generated.ts\"\n\n// ── Source schema ────────────────────────────────────────────────────────\n// UNLIKE OpenRouter, Requesty prices are JSON **numbers** in USD-PER-TOKEN\n// (e.g. `4e-7` = $0.40/1M), not strings, and live flat on the model entry\n// rather than nested under a `pricing` object. `cached_price` is a cache\n// READ price (no cache-write price exists in this source — we never emit\n// `cacheWriteMultiplier`). `supports_caching` gates whether `cached_price`\n// is actually meaningful: some entries carry a non-zero `cached_price` with\n// `supports_caching: false` (cache pricing published but not offered), so\n// the multiplier must only be derived when the flag is true. passthrough()\n// keeps the other live keys (context_window, supports_vision, description,\n// …) forward-compatible without a regenerate — we only read what we model.\n\n// `created` is Unix seconds — Requesty's own route-creation timestamp, used\n// to backfill `addedAt` for ids not already in the ledger (see\n// `../added-at.ts`). Verified present on the live payload (2026-08-31).\nconst ModelSchema = z\n .object({\n id: z.string(),\n input_price: z.number().optional(),\n output_price: z.number().optional(),\n cached_price: z.number().optional(),\n supports_caching: z.boolean().optional(),\n created: z.number().optional(),\n })\n .passthrough()\n\nconst SnapshotSchema = z.object({\n data: z.array(ModelSchema),\n})\n\n// ── Output shape (mirrors model-catalog's LLMPricing) ────────────────────\n// Drop-in for `packages/model-catalog/src/llm/requesty-routes.generated.ts`.\n// Structurally identical to LLMPricing — inputPer1M / outputPer1M in USD per\n// 1M tokens, plus cacheReadMultiplier when the source carries a meaningful\n// `cached_price`. No `cacheWriteMultiplier` — Requesty has no cache-write\n// price to derive one from.\n\ninterface LLMPricingEntry {\n inputPer1M: number\n outputPer1M: number\n cacheReadMultiplier?: number\n /** ISO date this id was first seen by a sync run. See `../added-at.ts`. */\n addedAt?: string\n vendor: string\n provider: \"requesty\"\n}\n\n// ── Number helpers ──────────────────────────────────────────────────────\n\n/** Round to 6 decimal places, killing binary-float noise from the per-token × 1e6 product. */\nfunction round6(n: number): number {\n return Math.round(n * 1_000_000) / 1_000_000\n}\n\n/**\n * Convert a per-token USD number to USD-per-1M-tokens, or undefined if\n * unpriced. UNLIKE OpenRouter (per-token USD strings), Requesty already\n * gives per-token USD as a JSON number, so there's no `Number(...)` parse\n * step — just the same ×1e6 + round6 normalization.\n */\nfunction per1m(tokenPrice: number | undefined): number | undefined {\n if (tokenPrice === undefined) return undefined\n if (!Number.isFinite(tokenPrice)) return undefined\n return round6(tokenPrice * 1_000_000)\n}\n\n/** Shortest faithful decimal string (matches existing file: 1, 0.5, 0.7448, 1.25). */\nfunction fmt(n: number): string {\n if (!Number.isFinite(n)) return \"0\"\n return `${round6(n)}`\n}\n\n/** Vendor slug = first segment of the Requesty route id (`vendor/model`). */\nfunction vendorOf(id: string): string {\n if (!id.includes(\"/\")) return id\n return id.split(\"/\")[0] ?? id\n}\n\n// ── Serialization ────────────────────────────────────────────────────────\n\nfunction serializeEntry(e: LLMPricingEntry): string {\n const fields: string[] = [\n `inputPer1M: ${fmt(e.inputPer1M)}`,\n `outputPer1M: ${fmt(e.outputPer1M)}`,\n ]\n if (e.cacheReadMultiplier !== undefined) {\n fields.push(`cacheReadMultiplier: ${fmt(e.cacheReadMultiplier)}`)\n }\n if (e.addedAt !== undefined) {\n fields.push(`addedAt: ${JSON.stringify(e.addedAt)}`)\n }\n fields.push(`vendor: ${JSON.stringify(e.vendor)}`)\n fields.push(`provider: ${JSON.stringify(e.provider)}`)\n return `{\\n ${fields.join(\",\\n \")},\\n }`\n}\n\nfunction serializeFile(entries: Record<string, LLMPricingEntry>): string {\n const ids = Object.keys(entries).sort()\n const providers = Array.from(new Set(ids.map(vendorOf))).sort()\n\n const lines: string[] = [\n \"// AUTO-GENERATED by @agentproto/catalog-sync (llm:requesty).\",\n \"// Do not edit by hand — re-run `pnpm --filter @agentproto/catalog-sync generate`.\",\n `// Source: ${REQUESTY_MODELS_URL}`,\n \"// Pricing carries provider USD (inputPer1M / outputPer1M) plus a cache\",\n \"// multiplier (cacheReadMultiplier) derived from the source's cached_price\",\n \"// per-token field when `supports_caching` is true. Requesty has no\",\n \"// cache-write price, so cacheWriteMultiplier is never emitted.\",\n \"// addedAt is the ISO date this id was first seen by a sync run — backfilled\",\n \"// from the source's own `created` timestamp, then NEVER mutated; see\",\n \"// packages/catalog-sync/src/added-at.ts and the package README.\",\n \"\",\n 'import type { LLMPricing } from \"./catalog.js\"',\n \"\",\n \"export const REQUESTY_ROUTES: Record<string, LLMPricing> = {\",\n ]\n for (const id of ids) {\n lines.push(` ${JSON.stringify(id)}: ${serializeEntry(entries[id]!)},`)\n }\n lines.push(\"}\")\n lines.push(\"\")\n lines.push(\"/** Provider slugs treated as Requesty routes by the picker. */\")\n lines.push(\"export const REQUESTY_PROVIDERS: readonly string[] = [\")\n for (const p of providers) {\n lines.push(` ${JSON.stringify(p)},`)\n }\n lines.push(\"]\")\n lines.push(\"\")\n return lines.join(\"\\n\")\n}\n\n// ── Generator ───────────────────────────────────────────────────────────\n\nconst LEDGER_ID = \"llm-requesty\"\n\nasync function generate(ctx: GeneratorContext): Promise<GeneratedFiles> {\n const src = sources[0]\n if (!src) throw new Error(\"llm:requesty: no source configured\")\n const parsed = SnapshotSchema.parse(await ctx.fetchSource(src))\n\n const entries: Record<string, LLMPricingEntry> = {}\n const createdAt: Record<string, string> = {}\n for (const model of parsed.data) {\n const inputPer1M = per1m(model.input_price)\n const outputPer1M = per1m(model.output_price)\n if (inputPer1M === undefined || outputPer1M === undefined) continue\n // Skip unpriced routes.\n if (inputPer1M === 0 && outputPer1M === 0) continue\n\n const entry: LLMPricingEntry = {\n inputPer1M,\n outputPer1M,\n vendor: vendorOf(model.id),\n provider: \"requesty\",\n }\n\n if (model.supports_caching === true && inputPer1M > 0) {\n const cachePer1M = per1m(model.cached_price)\n if (cachePer1M !== undefined) {\n entry.cacheReadMultiplier = round6(cachePer1M / inputPer1M)\n }\n }\n\n entries[model.id] = entry\n if (model.created !== undefined) createdAt[model.id] = isoDateFromUnixSeconds(model.created)\n }\n\n const ledger = computeAddedAtLedger(\n Object.keys(entries),\n readLedger(LEDGER_ID),\n createdAt,\n todayIso()\n )\n for (const [id, entry] of Object.entries(entries)) entry.addedAt = ledger[id]\n\n return {\n [OUTPUT_PATH]: serializeFile(entries),\n [ledgerRelPath(LEDGER_ID)]: serializeLedger(ledger),\n }\n}\n\nconst sources = [\n {\n id: \"llm-requesty\",\n url: REQUESTY_MODELS_URL,\n // Live refresh needs the account key (Bearer). Without REQUESTY_API_KEY\n // set, the framework reuses the committed snapshot.\n headers: { Authorization: \"Bearer env:REQUESTY_API_KEY\" },\n },\n]\n\nexport const llmRequestyGenerator = defineGenerator({\n name: \"llm:requesty\",\n modality: \"llm\",\n sources,\n generate,\n})\n"]}
package/dist/index.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { CatalogGenerator } from './types.js';
2
2
  export { CatalogSource, GeneratedFiles, GeneratorContext, defineGenerator } from './types.js';
3
- export { RunGeneratorsOptions, RunGeneratorsResult, runGenerators } from './runner.js';
3
+ export { CATALOG_CHANGELOG_PATH, RunGeneratorsOptions, RunGeneratorsResult, runGenerators } from './runner.js';
4
4
  export { llmOpenRouterGenerator } from './generators/llm-openrouter.js';
5
5
  export { llmRequestyGenerator } from './generators/llm-requesty.js';
6
6
  export { CatalogFileDiff, RefreshableSource, ReviewedRefreshOptions, ReviewedRefreshResult, SourceRefreshResult, refreshSources, reviewedRefresh } from './refresh-workflow.js';
@@ -42,4 +42,100 @@ declare const voiceMinimax: CatalogGenerator;
42
42
 
43
43
  declare const imageReplicate: CatalogGenerator;
44
44
 
45
- export { CatalogGenerator, imageReplicate, llmHuggingfaceGenerator, voiceElevenlabs, voiceMinimax };
45
+ /**
46
+ * `addedAt` ledger — tracks, per generator, the ISO date each model id was
47
+ * FIRST seen by a sync run. See `README.md` ("addedAt convention") for the
48
+ * full contract; this module is the mechanism.
49
+ *
50
+ * Invariant: once an id has a ledger entry, that date is NEVER overwritten —
51
+ * not even if the source's own `created` timestamp for that id later
52
+ * resolves to something different (a provider backdating/correcting its own
53
+ * metadata must not rewrite catalog history). New ids are backfilled from
54
+ * the source's own creation timestamp when it has one, else stamped with
55
+ * the current run's date.
56
+ *
57
+ * Ledger entries are never dropped when an id disappears from a run — the
58
+ * ledger records "first ever seen", not "currently present". A model that
59
+ * vanishes from a provider's list and later reappears keeps its original
60
+ * `addedAt` rather than looking newly added.
61
+ *
62
+ * Each generator owns one ledger file, committed at
63
+ * `packages/catalog-sync/ledger/<generator-id>.json` (id → ISO date, sorted
64
+ * keys, deterministic serialization) — included as a normal entry in the
65
+ * generator's `GeneratedFiles` return value, so `runner.ts`'s existing
66
+ * diff/write logic covers it for free (including `--check` drift detection).
67
+ */
68
+ /** Absolute path to a generator's committed addedAt ledger. */
69
+ declare function ledgerAbsPath(ledgerId: string): string;
70
+ /** Repo-relative path for a ledger — for inclusion in a generator's `GeneratedFiles`. */
71
+ declare function ledgerRelPath(ledgerId: string): string;
72
+ type AddedAtLedger = Record<string, string>;
73
+ /** Reads a committed ledger from disk. Missing/unparseable → empty ledger. */
74
+ declare function readLedger(ledgerId: string): AddedAtLedger;
75
+ /** Deterministic (sorted-key) JSON serialization, matching the `*.generated.ts` style of a trailing newline. */
76
+ declare function serializeLedger(ledger: AddedAtLedger): string;
77
+ /**
78
+ * Merge the current run's ids into a ledger.
79
+ *
80
+ * - id already in `previous` → keep the existing stamp verbatim (never
81
+ * mutated), regardless of what `createdAt` says this run.
82
+ * - id new to the ledger, source has a native creation timestamp for it →
83
+ * backfill with `createdAt[id]`.
84
+ * - id new to the ledger, no native timestamp → stamp with `today`.
85
+ * - ids in `previous` but absent from `currentIds` this run are kept as-is
86
+ * (see module doc comment — the ledger is "first ever seen").
87
+ */
88
+ declare function computeAddedAtLedger(currentIds: readonly string[], previous: AddedAtLedger, createdAt: Readonly<Record<string, string>>, today: string): AddedAtLedger;
89
+ /** Today's date as `YYYY-MM-DD`, in UTC. */
90
+ declare function todayIso(): string;
91
+ /** Converts a provider's Unix-seconds creation timestamp to `YYYY-MM-DD`. */
92
+ declare function isoDateFromUnixSeconds(seconds: number): string;
93
+
94
+ /**
95
+ * `CATALOG-CHANGELOG.md` — a human-readable "what models changed" log,
96
+ * distinct from the changesets-driven `CHANGELOG.md` next to it (that's
97
+ * package-version history, one entry per publish). This file answers "what
98
+ * models did catalog-sync add or remove, and when" by reading top-to-bottom.
99
+ *
100
+ * Wired into `runner.ts`: every `runGenerators` call diffs each changed
101
+ * `*.generated.ts` file's top-level id keys (before vs after) and, when a
102
+ * run actually adds or removes model ids for at least one generator,
103
+ * APPENDS one dated section (`## YYYY-MM-DD`) with a `### <generator name>`
104
+ * subsection per affected generator. Sections are appended at the END of
105
+ * the file (newest last) — a plain, non-destructive `+=`, so the writer
106
+ * never has to parse and re-emit prior history.
107
+ *
108
+ * A run with no id-level drift (pricing-only changes, or a `--check` with
109
+ * nothing to report) appends NOTHING — no empty section, no touched file,
110
+ * no diff. addedAt ledger files (`ledger/*.json`) are intentionally NOT
111
+ * diffed here — they never lose an id (see `added-at.ts`), so they carry no
112
+ * "removed" signal, and every id they'd report as "added" is already
113
+ * covered by the generator's own `*.generated.ts` diff.
114
+ */
115
+ /** Extracts the top-level quoted ids of a generated `Record<string, X>` map. */
116
+ declare function extractRecordIds(source: string): Set<string>;
117
+ interface ModelIdDiff {
118
+ added: string[];
119
+ removed: string[];
120
+ }
121
+ /** Diffs the id sets of a generated file's before/after content. `before` undefined = new file (no removals possible). */
122
+ declare function diffModelIds(before: string | undefined, after: string): ModelIdDiff;
123
+ interface GeneratorChangelogEntry {
124
+ /** The owning generator's `name` (e.g. `"llm:openrouter"`). */
125
+ generator: string;
126
+ added: string[];
127
+ removed: string[];
128
+ }
129
+ /** Renders a dated section, or `""` when every entry is empty (nothing to report). */
130
+ declare function renderChangelogSection(date: string, entries: readonly GeneratorChangelogEntry[]): string;
131
+ declare const CATALOG_CHANGELOG_HEADER = "# Catalog Changelog\n\nHuman-readable log of model additions/removals detected by\n`@agentproto/catalog-sync` runs \u2014 NOT `CHANGELOG.md` next to it (that one is\nchangesets-driven package-version history). A dated section is appended\nautomatically here whenever a sync run adds or removes model ids; sections\nare appended at the END of the file (newest last). Pricing-only drift (no id\nadded/removed) is not logged here \u2014 see the generated files' own git history\nfor that.\n";
132
+ /**
133
+ * Appends a new dated section to `existing` changelog content.
134
+ *
135
+ * Returns `undefined` when there is nothing to report (no generator has an
136
+ * added/removed id this run) — the caller should leave the file untouched
137
+ * in that case, not write an unchanged copy.
138
+ */
139
+ declare function appendChangelog(existing: string | undefined, date: string, entries: readonly GeneratorChangelogEntry[]): string | undefined;
140
+
141
+ export { type AddedAtLedger, CATALOG_CHANGELOG_HEADER, CatalogGenerator, type GeneratorChangelogEntry, type ModelIdDiff, appendChangelog, computeAddedAtLedger, diffModelIds, extractRecordIds, imageReplicate, isoDateFromUnixSeconds, ledgerAbsPath, ledgerRelPath, llmHuggingfaceGenerator, readLedger, renderChangelogSection, serializeLedger, todayIso, voiceElevenlabs, voiceMinimax };