File size: 5,165 Bytes
ed57015
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
/**
 * export-catalog — build the signed catalog product from the live freeapi DB.
 *
 * This is the single source of truth pipeline: every model and its resolved
 * quirks come straight out of the same SQLite DB the router uses, so the
 * published catalog can never drift from what the gateway actually serves.
 * The output JSON is what the standalone catalog server (../../catalog) hands
 * to clients — fresh ("live") to Premium, a monthly snapshot to free users.
 *
 * Usage:
 *   tsx src/scripts/export-catalog.ts [--db <path>] [--out <path>]
 *                                     [--version <YYYY.MM.DD>] [--tier live|monthly]
 *                                     [--stdout]
 *
 * Defaults: reads the server's data/freeapi.db, writes the suite's
 * catalog/data/catalog.live.json. The catalog server signs it separately
 * (catalog/src/scripts/sign-catalog.ts) so the signature covers the exact
 * bytes served.
 */
import fs from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';
import { initDb, getDb } from '../db/index.js';
import { getAllProviders } from '../providers/index.js';
import { resolveQuirksByModel, quirkKey, listQuirkDefinitions } from '../services/quirks.js';

const __dirname = path.dirname(fileURLToPath(import.meta.url));
const SUITE_ROOT = path.resolve(__dirname, '../../../..'); // freeapi/server/src/scripts -> suite root

function arg(name: string): string | undefined {
  const i = process.argv.indexOf(`--${name}`);
  return i !== -1 ? process.argv[i + 1] : undefined;
}
function flag(name: string): boolean {
  return process.argv.includes(`--${name}`);
}

function dateVersion(d = new Date()): string {
  const yyyy = d.getUTCFullYear();
  const mm = String(d.getUTCMonth() + 1).padStart(2, '0');
  const dd = String(d.getUTCDate()).padStart(2, '0');
  return `${yyyy}.${mm}.${dd}`;
}

interface ModelRow {
  platform: string;
  model_id: string;
  display_name: string;
  intelligence_rank: number;
  speed_rank: number;
  size_label: string;
  rpm_limit: number | null;
  rpd_limit: number | null;
  tpm_limit: number | null;
  tpd_limit: number | null;
  monthly_token_budget: string;
  context_window: number | null;
  enabled: number;
  supports_vision: number;
  supports_tools: number;
}

function main() {
  const dbPath = arg('db');
  const tier = (arg('tier') as 'live' | 'monthly' | undefined) ?? 'live';
  const version = arg('version') ?? dateVersion();

  // In --stdout mode stdout must carry ONLY the JSON document; incidental
  // logs (e.g. initDb's "Database initialized at ...") go to stderr instead.
  if (flag('stdout')) console.log = console.error;

  initDb(dbPath);
  const db = getDb();

  const models = db
    .prepare(
      `SELECT platform, model_id, display_name, intelligence_rank, speed_rank, size_label,
              rpm_limit, rpd_limit, tpm_limit, tpd_limit, monthly_token_budget, context_window,
              enabled, supports_vision, supports_tools
         FROM models
        ORDER BY intelligence_rank ASC, platform ASC, model_id ASC`,
    )
    .all() as ModelRow[];

  const quirksByModel = resolveQuirksByModel(db);
  const quirkDefs = listQuirkDefinitions(db);

  // Only platforms that actually have a registered provider AND a model in the
  // catalog — skip 'custom' (user-supplied endpoint, nothing to publish).
  const livePlatforms = new Set(models.map((m) => m.platform));
  const platforms = getAllProviders()
    .filter((p) => p.platform !== 'custom' && livePlatforms.has(p.platform))
    .map((p) => ({ id: p.platform, name: p.name }))
    .sort((a, b) => a.id.localeCompare(b.id));

  const catalogModels = models.map((m) => ({
    platform: m.platform,
    modelId: m.model_id,
    displayName: m.display_name,
    intelligenceRank: m.intelligence_rank,
    speedRank: m.speed_rank,
    sizeLabel: m.size_label,
    limits: {
      rpm: m.rpm_limit,
      rpd: m.rpd_limit,
      tpm: m.tpm_limit,
      tpd: m.tpd_limit,
    },
    monthlyTokenBudget: m.monthly_token_budget,
    contextWindow: m.context_window,
    enabled: m.enabled === 1,
    supportsVision: m.supports_vision === 1,
    supportsTools: m.supports_tools === 1,
    quirks: quirksByModel.get(quirkKey(m.platform, m.model_id)) ?? [],
  }));

  const catalog = {
    version,
    generatedAt: new Date().toISOString(),
    tier,
    counts: {
      platforms: platforms.length,
      models: catalogModels.length,
      enabledModels: catalogModels.filter((m) => m.enabled).length,
      quirks: quirkDefs.length,
    },
    platforms,
    models: catalogModels,
    quirks: quirkDefs,
  };

  const json = JSON.stringify(catalog, null, 2) + '\n';

  if (flag('stdout')) {
    process.stdout.write(json);
    return;
  }

  const outPath = arg('out') ?? path.join(SUITE_ROOT, 'catalog', 'data', `catalog.${tier}.json`);
  fs.mkdirSync(path.dirname(outPath), { recursive: true });
  fs.writeFileSync(outPath, json);
  console.log(
    `Wrote ${tier} catalog ${version}: ${catalog.counts.models} models ` +
      `(${catalog.counts.enabledModels} enabled), ${catalog.counts.platforms} platforms, ` +
      `${catalog.counts.quirks} quirks -> ${outPath}`,
  );
}

main();