@sriinnu/kosha-discovery 0.6.0 → 1.0.0

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 (123) hide show
  1. package/README.md +53 -221
  2. package/dist/cli-cmd-model.d.ts +48 -0
  3. package/dist/cli-cmd-model.d.ts.map +1 -0
  4. package/dist/cli-cmd-model.js +231 -0
  5. package/dist/cli-cmd-model.js.map +1 -0
  6. package/dist/cli-cmd-query.d.ts +40 -0
  7. package/dist/cli-cmd-query.d.ts.map +1 -0
  8. package/dist/cli-cmd-query.js +303 -0
  9. package/dist/cli-cmd-query.js.map +1 -0
  10. package/dist/cli-commands.d.ts +65 -75
  11. package/dist/cli-commands.d.ts.map +1 -1
  12. package/dist/cli-commands.js +122 -504
  13. package/dist/cli-commands.js.map +1 -1
  14. package/dist/cli-format.d.ts +40 -0
  15. package/dist/cli-format.d.ts.map +1 -1
  16. package/dist/cli-format.js +74 -0
  17. package/dist/cli-format.js.map +1 -1
  18. package/dist/cli-help.d.ts +19 -0
  19. package/dist/cli-help.d.ts.map +1 -0
  20. package/dist/cli-help.js +150 -0
  21. package/dist/cli-help.js.map +1 -0
  22. package/dist/cli.js +5 -1
  23. package/dist/cli.js.map +1 -1
  24. package/dist/discovery/anthropic.d.ts +8 -1
  25. package/dist/discovery/anthropic.d.ts.map +1 -1
  26. package/dist/discovery/anthropic.js +26 -3
  27. package/dist/discovery/anthropic.js.map +1 -1
  28. package/dist/discovery/base.d.ts +26 -0
  29. package/dist/discovery/base.d.ts.map +1 -1
  30. package/dist/discovery/base.js +55 -2
  31. package/dist/discovery/base.js.map +1 -1
  32. package/dist/discovery/free-tier.d.ts +31 -0
  33. package/dist/discovery/free-tier.d.ts.map +1 -0
  34. package/dist/discovery/free-tier.js +47 -0
  35. package/dist/discovery/free-tier.js.map +1 -0
  36. package/dist/discovery/google.d.ts +8 -1
  37. package/dist/discovery/google.d.ts.map +1 -1
  38. package/dist/discovery/google.js +25 -3
  39. package/dist/discovery/google.js.map +1 -1
  40. package/dist/discovery/litellm-seed.d.ts +32 -0
  41. package/dist/discovery/litellm-seed.d.ts.map +1 -0
  42. package/dist/discovery/litellm-seed.js +252 -0
  43. package/dist/discovery/litellm-seed.js.map +1 -0
  44. package/dist/discovery/modelsdev-catalog.d.ts +95 -0
  45. package/dist/discovery/modelsdev-catalog.d.ts.map +1 -0
  46. package/dist/discovery/modelsdev-catalog.js +131 -0
  47. package/dist/discovery/modelsdev-catalog.js.map +1 -0
  48. package/dist/discovery/modelsdev-seed.d.ts +19 -0
  49. package/dist/discovery/modelsdev-seed.d.ts.map +1 -0
  50. package/dist/discovery/modelsdev-seed.js +203 -0
  51. package/dist/discovery/modelsdev-seed.js.map +1 -0
  52. package/dist/discovery/openai-compatible.d.ts.map +1 -1
  53. package/dist/discovery/openai-compatible.js +12 -1
  54. package/dist/discovery/openai-compatible.js.map +1 -1
  55. package/dist/discovery/openai.d.ts +9 -1
  56. package/dist/discovery/openai.d.ts.map +1 -1
  57. package/dist/discovery/openai.js +31 -8
  58. package/dist/discovery/openai.js.map +1 -1
  59. package/dist/discovery/openrouter.d.ts.map +1 -1
  60. package/dist/discovery/openrouter.js +18 -1
  61. package/dist/discovery/openrouter.js.map +1 -1
  62. package/dist/discovery/promo-overrides.d.ts +54 -0
  63. package/dist/discovery/promo-overrides.d.ts.map +1 -0
  64. package/dist/discovery/promo-overrides.js +74 -0
  65. package/dist/discovery/promo-overrides.js.map +1 -0
  66. package/dist/discovery/public-seed.d.ts +30 -0
  67. package/dist/discovery/public-seed.d.ts.map +1 -0
  68. package/dist/discovery/public-seed.js +51 -0
  69. package/dist/discovery/public-seed.js.map +1 -0
  70. package/dist/discovery-contract.d.ts +14 -2
  71. package/dist/discovery-contract.d.ts.map +1 -1
  72. package/dist/discovery-contract.js +28 -0
  73. package/dist/discovery-contract.js.map +1 -1
  74. package/dist/enrichment/litellm-catalog.d.ts +81 -0
  75. package/dist/enrichment/litellm-catalog.d.ts.map +1 -0
  76. package/dist/enrichment/litellm-catalog.js +129 -0
  77. package/dist/enrichment/litellm-catalog.js.map +1 -0
  78. package/dist/enrichment/litellm.d.ts +9 -2
  79. package/dist/enrichment/litellm.d.ts.map +1 -1
  80. package/dist/enrichment/litellm.js +192 -23
  81. package/dist/enrichment/litellm.js.map +1 -1
  82. package/dist/index.d.ts +3 -1
  83. package/dist/index.d.ts.map +1 -1
  84. package/dist/index.js +2 -0
  85. package/dist/index.js.map +1 -1
  86. package/dist/model-features.d.ts +77 -0
  87. package/dist/model-features.d.ts.map +1 -0
  88. package/dist/model-features.js +245 -0
  89. package/dist/model-features.js.map +1 -0
  90. package/dist/normalize.d.ts +13 -0
  91. package/dist/normalize.d.ts.map +1 -1
  92. package/dist/normalize.js +15 -0
  93. package/dist/normalize.js.map +1 -1
  94. package/dist/provider-catalog.d.ts +7 -0
  95. package/dist/provider-catalog.d.ts.map +1 -1
  96. package/dist/provider-catalog.js +4 -0
  97. package/dist/provider-catalog.js.map +1 -1
  98. package/dist/registry-discovery.d.ts.map +1 -1
  99. package/dist/registry-discovery.js +11 -1
  100. package/dist/registry-discovery.js.map +1 -1
  101. package/dist/registry-query.d.ts.map +1 -1
  102. package/dist/registry-query.js +6 -3
  103. package/dist/registry-query.js.map +1 -1
  104. package/dist/registry-runtime.d.ts +39 -1
  105. package/dist/registry-runtime.d.ts.map +1 -1
  106. package/dist/registry-runtime.js +332 -1
  107. package/dist/registry-runtime.js.map +1 -1
  108. package/dist/registry-selection.js +2 -0
  109. package/dist/registry-selection.js.map +1 -1
  110. package/dist/registry.d.ts +9 -0
  111. package/dist/registry.d.ts.map +1 -1
  112. package/dist/registry.js +11 -1
  113. package/dist/registry.js.map +1 -1
  114. package/dist/security.d.ts.map +1 -1
  115. package/dist/security.js +32 -4
  116. package/dist/security.js.map +1 -1
  117. package/dist/tokenizer-family.d.ts +39 -0
  118. package/dist/tokenizer-family.d.ts.map +1 -0
  119. package/dist/tokenizer-family.js +92 -0
  120. package/dist/tokenizer-family.js.map +1 -0
  121. package/dist/types.d.ts +186 -0
  122. package/dist/types.d.ts.map +1 -1
  123. package/package.json +8 -2
@@ -5,17 +5,21 @@
5
5
  * Formatting utilities are imported from `./cli-format.js` to keep
6
6
  * presentation logic separate from command orchestration.
7
7
  *
8
+ * This module also serves as the barrel re-export for commands that were
9
+ * split into dedicated files (`cli-cmd-model.ts`, `cli-cmd-query.ts`,
10
+ * `cli-help.ts`) to keep each file under 450 LOC.
11
+ *
8
12
  * @module cli-commands
9
13
  */
10
- import { BOLD, CYAN, DIM, GREEN, MAGENTA, RED, YELLOW, c, formatContextWindow, formatNumber, formatPrice, formatTimestamp, line, renderTable, } from "./cli-format.js";
11
- import { createRequire } from "node:module";
12
- const _require = createRequire(import.meta.url);
13
- const VERSION = _require("../package.json").version;
14
+ import { BOLD, CYAN, DIM, GREEN, RED, YELLOW, c, formatContextWindow, formatPrice, formatPricingTier, formatRelativeTime, formatTimestamp, line, renderTable, } from "./cli-format.js";
15
+ // ---------------------------------------------------------------------------
16
+ // Shared helpers — exported so split modules can import them
17
+ // ---------------------------------------------------------------------------
14
18
  /**
15
19
  * Standard column layout reused by both `cmdList` and `cmdSearch`.
16
20
  * Extracted to avoid duplicating the same six-column definition.
17
21
  */
18
- const MODEL_TABLE_COLUMNS = [
22
+ export const MODEL_TABLE_COLUMNS = [
19
23
  { header: "Provider", width: 12 },
20
24
  { header: "Model", width: 38 },
21
25
  { header: "Mode", width: 10 },
@@ -23,6 +27,22 @@ const MODEL_TABLE_COLUMNS = [
23
27
  { header: "$/M in", width: 8, align: "right" },
24
28
  { header: "$/M out", width: 8, align: "right" },
25
29
  ];
30
+ /**
31
+ * Extended pricing column layout used when `--pricing` flag is passed.
32
+ * Replaces context-window with tier, cache, and batch columns.
33
+ */
34
+ const PRICING_TABLE_COLUMNS = [
35
+ { header: "Provider", width: 12 },
36
+ { header: "Model", width: 34 },
37
+ { header: "Mode", width: 10 },
38
+ { header: "Tier", width: 6 },
39
+ { header: "$/M in", width: 8, align: "right" },
40
+ { header: "$/M out", width: 8, align: "right" },
41
+ { header: "Cache R", width: 8, align: "right" },
42
+ { header: "Cache W", width: 8, align: "right" },
43
+ { header: "Batch in", width: 8, align: "right" },
44
+ { header: "Batch out", width: 9, align: "right" },
45
+ ];
26
46
  /**
27
47
  * Return a human-readable label for a provider's credential source.
28
48
  * Providers that need no remote auth (e.g. local Ollama) show `"none (local)"`.
@@ -30,7 +50,7 @@ const MODEL_TABLE_COLUMNS = [
30
50
  * @param provider The provider info object.
31
51
  * @returns A short label suitable for table display.
32
52
  */
33
- function formatCredentialSource(provider) {
53
+ export function formatCredentialSource(provider) {
34
54
  if (!provider.credentialSource || provider.credentialSource === "none") {
35
55
  return c(DIM, "none (local)");
36
56
  }
@@ -38,14 +58,37 @@ function formatCredentialSource(provider) {
38
58
  }
39
59
  /**
40
60
  * Ensure the registry has at least one provider loaded.
41
- * If empty (first run or stale cache), triggers a full discovery pass.
61
+ *
62
+ * The registry's `discover()` transparently hydrates from the on-disk cache
63
+ * at `~/.kosha/cache` when that cache is still within TTL, and only hits
64
+ * provider APIs on a cold start or a stale cache. I look at the resulting
65
+ * `discoveredAt` timestamp to tell the user which path actually ran — the
66
+ * old "No cached data. Running discovery..." message was misleading because
67
+ * it fired on every invocation regardless of whether the cache was used.
68
+ *
69
+ * Honesty here matters: users need to trust that `kosha list` is fast
70
+ * because the cache exists, and that `kosha update` is the way to refresh.
42
71
  *
43
72
  * @param registry The shared model registry instance.
44
73
  */
45
- async function ensureDiscovered(registry) {
46
- if (registry.providers_list().length === 0) {
47
- console.log(c(DIM, "No cached data. Running discovery..."));
48
- await registry.discover();
74
+ export async function ensureDiscovered(registry) {
75
+ if (registry.providers_list().length > 0)
76
+ return;
77
+ const beforeMs = Date.now();
78
+ await registry.discover();
79
+ const { discoveredAt } = registry.toJSON();
80
+ const providers = registry.providers_list();
81
+ const totalModels = providers.reduce((sum, p) => sum + p.models.length, 0);
82
+ // If the timestamp on the hydrated registry is older than "basically now",
83
+ // we know the disk cache was loaded rather than a live discovery running.
84
+ const usedCache = discoveredAt > 0 && beforeMs - discoveredAt > 2_000;
85
+ if (usedCache) {
86
+ const age = formatRelativeTime(discoveredAt, beforeMs);
87
+ console.log(c(DIM, `Loaded ${totalModels} models from cache (${age}). Run "kosha update" to refresh.`));
88
+ }
89
+ else {
90
+ console.log(c(DIM, `Discovered ${totalModels} models from ${providers.length} providers.`));
91
+ console.log(c(DIM, `Saved to ~/.kosha/cache and exported manifest to ~/.kosha/registry.json`));
49
92
  }
50
93
  }
51
94
  /**
@@ -53,7 +96,7 @@ async function ensureDiscovered(registry) {
53
96
  * @param m A model card object.
54
97
  * @returns An array of formatted cell strings.
55
98
  */
56
- function modelRow(m) {
99
+ export function modelRow(m) {
57
100
  return [
58
101
  c(CYAN, m.provider), m.id, m.mode,
59
102
  formatContextWindow(m.contextWindow),
@@ -61,13 +104,23 @@ function modelRow(m) {
61
104
  formatPrice(m.pricing?.outputPerMillion),
62
105
  ];
63
106
  }
64
- function parseNumberFlag(value) {
65
- if (typeof value !== "string")
66
- return undefined;
67
- const n = Number(value);
68
- if (!Number.isFinite(n))
69
- return undefined;
70
- return n;
107
+ /**
108
+ * Build an extended pricing-table row from a model card.
109
+ * Includes tier label, cache read/write, and batch pricing columns.
110
+ * @param m A model card with full pricing detail.
111
+ * @returns An array of formatted cell strings for the pricing column layout.
112
+ */
113
+ function pricingRow(m) {
114
+ return [
115
+ c(CYAN, m.provider), m.id, m.mode,
116
+ formatPricingTier(m.pricing),
117
+ formatPrice(m.pricing?.inputPerMillion),
118
+ formatPrice(m.pricing?.outputPerMillion),
119
+ formatPrice(m.pricing?.cacheReadPerMillion),
120
+ formatPrice(m.pricing?.cacheWritePerMillion),
121
+ formatPrice(m.pricing?.batchInputPerMillion),
122
+ formatPrice(m.pricing?.batchOutputPerMillion),
123
+ ];
71
124
  }
72
125
  /** Print a provider summary line (used by discover & refresh). */
73
126
  function printProviderSummary(providers) {
@@ -107,6 +160,7 @@ export async function cmdDiscover(registry, flags) {
107
160
  return;
108
161
  }
109
162
  printProviderSummary(providers);
163
+ console.log(c(DIM, `\nCached to ~/.kosha/cache · Manifest: ~/.kosha/registry.json`));
110
164
  }
111
165
  // ── latest ───────────────────────────────────────────────────────────────
112
166
  /**
@@ -133,225 +187,72 @@ export async function cmdLatest(registry, flags) {
133
187
  printProviderSummary(result.providers);
134
188
  console.log(c(DIM, `\nFetched at: ${formatTimestamp(result.discoveredAt)}`));
135
189
  }
136
- // ── list ─────────────────────────────────────────────────────────────────
190
+ // ── enrich ───────────────────────────────────────────────────────────────
137
191
  /**
138
- * List all known models in a formatted table.
139
- * Supports filtering by `--provider`, `--origin`, `--mode`, and `--capability`.
192
+ * Re-run LiteLLM enrichment on cached models without re-discovering providers.
140
193
  *
141
- * @param registry The model registry to query.
142
- * @param flags CLI flags (supports `--provider`, `--origin`, `--mode`, `--capability`, `--json`).
143
- */
144
- export async function cmdList(registry, flags) {
145
- const provider = typeof flags.provider === "string" ? flags.provider : undefined;
146
- const originProvider = typeof flags.origin === "string" ? flags.origin : undefined;
147
- const mode = typeof flags.mode === "string" ? flags.mode : undefined;
148
- const capability = typeof flags.capability === "string" ? flags.capability : undefined;
149
- await ensureDiscovered(registry);
150
- const models = registry.models({ provider, originProvider, mode, capability });
151
- if (flags.json) {
152
- console.log(JSON.stringify(models, null, 2));
153
- return;
154
- }
155
- if (models.length === 0) {
156
- console.log(c(YELLOW, "No models found matching the given filters."));
157
- return;
158
- }
159
- console.log(renderTable(MODEL_TABLE_COLUMNS, models.map(modelRow)));
160
- const providerCount = new Set(models.map((m) => m.provider)).size;
161
- console.log(c(DIM, line("\u2500", 90)));
162
- console.log(`${c(BOLD, String(models.length))} models from ${c(BOLD, String(providerCount))} providers`);
163
- }
164
- // ── roles ────────────────────────────────────────────────────────────────
165
- /**
166
- * Show a provider -> model -> roles matrix.
194
+ * This is the lightweight alternative to `kosha refresh` when you only need
195
+ * updated pricing data — no provider API calls, just a single fetch from the
196
+ * litellm community catalogue plus a cache + manifest update.
167
197
  *
168
- * Useful for assistants that need to answer: "which providers/models can do X?"
198
+ * @param registry The model registry with cached data.
199
+ * @param flags CLI flags (supports `--json`).
169
200
  */
170
- export async function cmdRoles(registry, flags) {
171
- const provider = typeof flags.provider === "string" ? flags.provider : undefined;
172
- const originProvider = typeof flags.origin === "string" ? flags.origin : undefined;
173
- const mode = typeof flags.mode === "string" ? flags.mode : undefined;
174
- const capability = typeof flags.capability === "string" ? flags.capability : undefined;
175
- const role = typeof flags.role === "string" ? flags.role : undefined;
176
- await ensureDiscovered(registry);
177
- const providers = registry.providerRoles({ provider, originProvider, mode, capability, role });
178
- if (flags.json) {
179
- console.log(JSON.stringify({
180
- providers,
181
- count: providers.length,
182
- modelCount: providers.reduce((sum, p) => sum + p.models.length, 0),
183
- missingCredentials: registry.missingCredentialPrompts(providers.map((p) => p.id)),
184
- }, null, 2));
185
- return;
186
- }
187
- if (providers.length === 0) {
188
- console.log(c(YELLOW, "No provider/model roles found for the given filters."));
189
- return;
190
- }
191
- const columns = [
192
- { header: "Provider", width: 12 },
193
- { header: "Model", width: 40 },
194
- { header: "Mode", width: 10 },
195
- { header: "Roles", width: 56 },
196
- ];
197
- const rows = providers.flatMap((providerInfo) => providerInfo.models.map((model) => [
198
- c(CYAN, providerInfo.id),
199
- model.id,
200
- model.mode,
201
- model.roles.join(", "),
202
- ]));
203
- console.log(renderTable(columns, rows));
204
- const missing = registry.missingCredentialPrompts(providers.map((p) => p.id));
205
- if (missing.length > 0) {
206
- console.log(c(DIM, "\nMissing provider credentials:"));
207
- for (const prompt of missing) {
208
- console.log(` ${c(YELLOW, prompt.providerId)}: ${prompt.message}`);
209
- }
201
+ export async function cmdEnrich(registry, flags) {
202
+ console.log(c(DIM, "Re-enriching cached models with LiteLLM pricing data..."));
203
+ const result = await registry.enrichOnly();
204
+ if (result === null) {
205
+ console.error(c(YELLOW, "No cached data found. Run `kosha discover` first."));
206
+ process.exit(1);
210
207
  }
211
- }
212
- // ── cheapest ─────────────────────────────────────────────────────────────
213
- /**
214
- * Return cheapest model candidates for a requested role/capability.
215
- */
216
- export async function cmdCheapest(registry, flags) {
217
- const provider = typeof flags.provider === "string" ? flags.provider : undefined;
218
- const originProvider = typeof flags.origin === "string" ? flags.origin : undefined;
219
- const mode = typeof flags.mode === "string" ? flags.mode : undefined;
220
- const capability = typeof flags.capability === "string" ? flags.capability : undefined;
221
- const role = typeof flags.role === "string" ? flags.role : undefined;
222
- await ensureDiscovered(registry);
223
- const result = registry.cheapestModels({
224
- provider,
225
- originProvider,
226
- mode,
227
- capability,
228
- role,
229
- limit: parseNumberFlag(flags.limit),
230
- priceMetric: typeof flags["price-metric"] === "string"
231
- ? flags["price-metric"]
232
- : undefined,
233
- inputWeight: parseNumberFlag(flags["input-weight"]),
234
- outputWeight: parseNumberFlag(flags["output-weight"]),
235
- includeUnpriced: flags["include-unpriced"] === true,
236
- });
237
208
  if (flags.json) {
238
209
  console.log(JSON.stringify(result, null, 2));
239
210
  return;
240
211
  }
241
- if (result.matches.length === 0) {
242
- console.log(c(YELLOW, "No priced models found for the requested filters."));
243
- if (result.missingCredentials.length > 0) {
244
- console.log(c(DIM, "\nMissing provider credentials:"));
245
- for (const prompt of result.missingCredentials) {
246
- console.log(` ${c(YELLOW, prompt.providerId)}: ${prompt.message}`);
247
- }
248
- }
249
- return;
250
- }
251
- const columns = [
252
- { header: "Provider", width: 12 },
253
- { header: "Model", width: 38 },
254
- { header: "Mode", width: 10 },
255
- { header: "Metric", width: 8 },
256
- { header: "Score", width: 10, align: "right" },
257
- { header: "$/M in", width: 8, align: "right" },
258
- { header: "$/M out", width: 8, align: "right" },
259
- ];
260
- const rows = result.matches.map((match) => [
261
- c(CYAN, match.model.provider),
262
- match.model.id,
263
- match.model.mode,
264
- match.priceMetric,
265
- match.score === undefined ? "\u2014" : formatPrice(match.score),
266
- formatPrice(match.model.pricing?.inputPerMillion),
267
- formatPrice(match.model.pricing?.outputPerMillion),
268
- ]);
269
- console.log(renderTable(columns, rows));
270
- console.log(c(DIM, `\n${result.pricedCandidates}/${result.candidates} candidates had usable pricing.`));
271
- if (result.missingCredentials.length > 0) {
272
- console.log(c(DIM, "\nMissing provider credentials:"));
273
- for (const prompt of result.missingCredentials) {
274
- console.log(` ${c(YELLOW, prompt.providerId)}: ${prompt.message}`);
275
- }
212
+ const totalModels = result.modelCount;
213
+ const cacheUpdated = result.cachePricingUpdated;
214
+ const batchUpdated = result.batchPricingUpdated;
215
+ console.log(c(DIM, `\nEnriched ${c(BOLD, String(totalModels))} models:`));
216
+ if (cacheUpdated > 0) {
217
+ console.log(c(DIM, ` Cache pricing updated: ${cacheUpdated} models`));
276
218
  }
277
- }
278
- // ── capabilities ─────────────────────────────────────────────────────────
279
- /**
280
- * Show an aggregated capability overview across all discovered models.
281
- * Each row shows a capability, how many models/providers support it,
282
- * and an example model ID for quick reference.
283
- *
284
- * @param registry The model registry to query.
285
- * @param flags CLI flags (supports `--provider`, `--json`).
286
- */
287
- export async function cmdCapabilities(registry, flags) {
288
- const provider = typeof flags.provider === "string" ? flags.provider : undefined;
289
- await ensureDiscovered(registry);
290
- const caps = registry.capabilities({ provider });
291
- if (flags.json) {
292
- console.log(JSON.stringify(caps, null, 2));
293
- return;
219
+ if (batchUpdated > 0) {
220
+ console.log(c(DIM, ` Batch pricing updated: ${batchUpdated} models`));
294
221
  }
295
- if (caps.length === 0) {
296
- console.log(c(YELLOW, "No capabilities found."));
297
- return;
222
+ if (cacheUpdated === 0 && batchUpdated === 0) {
223
+ console.log(c(DIM, " No new pricing data found."));
298
224
  }
299
- const columns = [
300
- { header: "Capability", width: 20 },
301
- { header: "Models", width: 8, align: "right" },
302
- { header: "Providers", width: 10, align: "right" },
303
- { header: "Example Model", width: 38 },
304
- ];
305
- const rows = caps.map((cap) => [
306
- c(CYAN, cap.capability),
307
- String(cap.modelCount),
308
- String(cap.providerCount),
309
- cap.exampleModelId ?? c(DIM, "—"),
310
- ]);
311
- const totalModels = registry.models({ provider }).length;
312
- console.log(renderTable(columns, rows));
313
- console.log(c(DIM, `\n${caps.length} capabilities across ${totalModels} models`));
225
+ console.log(c(DIM, `\nCache and manifest updated at ~/.kosha/`));
314
226
  }
315
- // ── capable ──────────────────────────────────────────────────────────────
227
+ // ── list ─────────────────────────────────────────────────────────────────
316
228
  /**
317
- * Show models that support a given capability/role.
318
- * Normalizes the query so aliases work: "embeddings", "stt", "tools", "vision".
229
+ * List all known models in a formatted table.
230
+ * Supports filtering by `--provider`, `--origin`, `--mode`, and `--capability`.
319
231
  *
320
232
  * @param registry The model registry to query.
321
- * @param query The capability query (e.g. "vision", "embeddings", "tools").
322
- * @param flags CLI flags (supports `--provider`, `--origin`, `--json`, `--limit`).
233
+ * @param flags CLI flags (supports `--provider`, `--origin`, `--mode`, `--capability`, `--pricing`, `--json`).
323
234
  */
324
- export async function cmdCapable(registry, query, flags) {
325
- if (!query) {
326
- console.error(c(RED, "Usage: kosha capable <capability>"));
327
- process.exit(1);
328
- }
235
+ export async function cmdList(registry, flags) {
329
236
  const provider = typeof flags.provider === "string" ? flags.provider : undefined;
330
237
  const originProvider = typeof flags.origin === "string" ? flags.origin : undefined;
331
238
  const mode = typeof flags.mode === "string" ? flags.mode : undefined;
332
- const limit = parseNumberFlag(flags.limit);
239
+ const capability = typeof flags.capability === "string" ? flags.capability : undefined;
240
+ const showPricing = flags.pricing === true;
333
241
  await ensureDiscovered(registry);
334
- const normalized = registry.normalizeRoleToken(query);
335
- const allModels = registry.models({ provider, originProvider, mode });
336
- let models = allModels.filter((m) => registry.modelSupportsRole(m, normalized));
337
- if (limit !== undefined && limit > 0) {
338
- models = models.slice(0, limit);
339
- }
242
+ const models = registry.models({ provider, originProvider, mode, capability });
340
243
  if (flags.json) {
341
244
  console.log(JSON.stringify(models, null, 2));
342
245
  return;
343
246
  }
344
247
  if (models.length === 0) {
345
- console.log(c(YELLOW, `No models found with capability "${query}"${normalized !== query ? ` (normalized: "${normalized}")` : ""}`));
248
+ console.log(c(YELLOW, "No models found matching the given filters."));
346
249
  return;
347
250
  }
348
- const header = normalized !== query
349
- ? `Models with capability ${c(CYAN, normalized)} ${c(DIM, `(from "${query}")`)}`
350
- : `Models with capability ${c(CYAN, normalized)}`;
351
- console.log(`\n${c(BOLD, header)}\n`);
352
- console.log(renderTable(MODEL_TABLE_COLUMNS, models.map(modelRow)));
251
+ const listColumns = showPricing ? PRICING_TABLE_COLUMNS : MODEL_TABLE_COLUMNS;
252
+ const listRowFn = showPricing ? pricingRow : modelRow;
253
+ console.log(renderTable(listColumns, models.map(listRowFn)));
353
254
  const providerCount = new Set(models.map((m) => m.provider)).size;
354
- console.log(c(DIM, line("\u2500", 90)));
255
+ console.log(c(DIM, line("\u2500", showPricing ? 110 : 90)));
355
256
  console.log(`${c(BOLD, String(models.length))} models from ${c(BOLD, String(providerCount))} providers`);
356
257
  }
357
258
  // ── search ───────────────────────────────────────────────────────────────
@@ -361,7 +262,7 @@ export async function cmdCapable(registry, query, flags) {
361
262
  *
362
263
  * @param registry The model registry to search.
363
264
  * @param query The search term (substring).
364
- * @param flags CLI flags (supports `--origin`, `--json`).
265
+ * @param flags CLI flags (supports `--origin`, `--pricing`, `--json`).
365
266
  */
366
267
  export async function cmdSearch(registry, query, flags) {
367
268
  if (!query) {
@@ -371,6 +272,7 @@ export async function cmdSearch(registry, query, flags) {
371
272
  await ensureDiscovered(registry);
372
273
  const needle = query.toLowerCase();
373
274
  const originProvider = typeof flags.origin === "string" ? flags.origin : undefined;
275
+ const showPricing = flags.pricing === true;
374
276
  // Apply optional origin filter first, then substring-match across id, name, and aliases
375
277
  const matches = registry.models({ originProvider }).filter((m) => m.id.toLowerCase().includes(needle) ||
376
278
  m.name.toLowerCase().includes(needle) ||
@@ -383,89 +285,11 @@ export async function cmdSearch(registry, query, flags) {
383
285
  console.log(c(YELLOW, `No models matching "${query}"`));
384
286
  return;
385
287
  }
386
- console.log(renderTable(MODEL_TABLE_COLUMNS, matches.map(modelRow)));
288
+ const searchColumns = showPricing ? PRICING_TABLE_COLUMNS : MODEL_TABLE_COLUMNS;
289
+ const searchRowFn = showPricing ? pricingRow : modelRow;
290
+ console.log(renderTable(searchColumns, matches.map(searchRowFn)));
387
291
  console.log(c(DIM, `\n${matches.length} result${matches.length !== 1 ? "s" : ""} for "${query}"`));
388
292
  }
389
- // ── model (detail view) ─────────────────────────────────────────────────
390
- /**
391
- * Show detailed information for a single model identified by id or alias.
392
- *
393
- * If an exact match is not found, falls back to fuzzy substring search.
394
- * Single fuzzy match is shown automatically; multiple matches produce a
395
- * disambiguation list (capped at 5 suggestions).
396
- *
397
- * @param registry The model registry to look up.
398
- * @param idOrAlias A model id, display name, or alias.
399
- * @param flags CLI flags (supports `--json`).
400
- */
401
- export async function cmdModel(registry, idOrAlias, flags) {
402
- if (!idOrAlias) {
403
- console.error(c(RED, "Usage: kosha model <id|alias>"));
404
- process.exit(1);
405
- }
406
- await ensureDiscovered(registry);
407
- const model = registry.model(idOrAlias);
408
- if (!model) {
409
- // Fuzzy fallback — substring match across id, name, and aliases so users
410
- // can type fragments like "sonnet" without knowing the full canonical ID.
411
- const needle = idOrAlias.toLowerCase();
412
- const fuzzy = registry.models().filter((m) => m.id.toLowerCase().includes(needle) ||
413
- m.name.toLowerCase().includes(needle) ||
414
- m.aliases.some((a) => a.toLowerCase().includes(needle)));
415
- if (fuzzy.length === 1)
416
- return cmdModel(registry, fuzzy[0].id, flags);
417
- if (fuzzy.length > 1) {
418
- console.log(c(YELLOW, `No exact match for "${idOrAlias}". Did you mean:`));
419
- for (const m of fuzzy.slice(0, 5))
420
- console.log(` ${c(CYAN, m.id)} (${m.provider})`);
421
- return;
422
- }
423
- console.error(c(RED, `Model not found: "${idOrAlias}"`));
424
- process.exit(1);
425
- }
426
- if (flags.json) {
427
- console.log(JSON.stringify(model, null, 2));
428
- return;
429
- }
430
- const providerName = registry.provider(model.provider)?.name ?? model.provider;
431
- const pricingStr = model.pricing
432
- ? `${formatPrice(model.pricing.inputPerMillion)} / ${formatPrice(model.pricing.outputPerMillion)} per million tokens (in/out)`
433
- : c(DIM, "unknown");
434
- // Cache pricing is optional — only displayed when the model has it
435
- const cacheStr = model.pricing?.cacheReadPerMillion !== undefined
436
- ? `\nCache Pricing: ${formatPrice(model.pricing.cacheReadPerMillion)} read / ${formatPrice(model.pricing.cacheWritePerMillion)} write per million tokens`
437
- : "";
438
- // Reasoning pricing is optional — shown when providers expose it.
439
- const reasoningStr = model.pricing?.reasoningInputPerMillion !== undefined || model.pricing?.reasoningOutputPerMillion !== undefined
440
- ? `\nReasoning Pricing: ${formatPrice(model.pricing?.reasoningInputPerMillion)} in / ${formatPrice(model.pricing?.reasoningOutputPerMillion)} out per million tokens`
441
- : "";
442
- const originPricingStr = model.originPricing
443
- ? `\nOrigin Pricing: ${formatPrice(model.originPricing.inputPerMillion)} / ${formatPrice(model.originPricing.outputPerMillion)} per million tokens (in/out)` +
444
- ((model.originPricing.reasoningInputPerMillion !== undefined || model.originPricing.reasoningOutputPerMillion !== undefined)
445
- ? `\nOrigin Reasoning Pricing: ${formatPrice(model.originPricing.reasoningInputPerMillion)} in / ${formatPrice(model.originPricing.reasoningOutputPerMillion)} out per million tokens`
446
- : "")
447
- : "";
448
- // Origin provider line: only shown when distinct from the serving-layer provider
449
- const originLine = model.originProvider && model.originProvider !== model.provider
450
- ? `\n${c(BOLD, "Origin Provider:")} ${c(CYAN, model.originProvider)}`
451
- : "";
452
- // Region and project ID are only shown when present (Bedrock / Vertex models)
453
- const regionLine = model.region ? `\n${c(BOLD, "Region:")} ${model.region}` : "";
454
- const projectLine = model.projectId ? `\n${c(BOLD, "Project ID:")} ${model.projectId}` : "";
455
- console.log(`
456
- ${c(BOLD, "Model:")} ${model.id}
457
- ${c(BOLD, "Name:")} ${model.name}
458
- ${c(BOLD, "Provider:")} ${providerName}${originLine}${regionLine}${projectLine}
459
- ${c(BOLD, "Mode:")} ${model.mode}
460
- ${c(BOLD, "Aliases:")} ${model.aliases.length > 0 ? model.aliases.join(", ") : c(DIM, "none")}
461
- ${c(BOLD, "Context Window:")} ${model.contextWindow > 0 ? formatNumber(model.contextWindow) + " tokens" : c(DIM, "unknown")}
462
- ${c(BOLD, "Max Output:")} ${model.maxOutputTokens > 0 ? formatNumber(model.maxOutputTokens) + " tokens" : c(DIM, "unknown")}${model.dimensions ? `\n${c(BOLD, "Dimensions:")} ${formatNumber(model.dimensions)}` : ""}
463
- ${c(BOLD, "Capabilities:")} ${model.capabilities.join(", ")}
464
- ${c(BOLD, "Pricing:")} ${pricingStr}${cacheStr}${reasoningStr}${originPricingStr}
465
- ${c(BOLD, "Source:")} ${model.source}
466
- ${c(BOLD, "Discovered:")} ${formatTimestamp(model.discoveredAt)}
467
- `.trim());
468
- }
469
293
  // ── providers ────────────────────────────────────────────────────────────
470
294
  /**
471
295
  * List all known providers with their authentication status and model counts.
@@ -501,92 +325,6 @@ export async function cmdProviders(registry, flags) {
501
325
  });
502
326
  console.log(renderTable(columns, rows));
503
327
  }
504
- // ── resolve ──────────────────────────────────────────────────────────────
505
- /**
506
- * Resolve a model alias to its canonical provider ID.
507
- * Unknown aliases are returned as-is with a yellow warning.
508
- *
509
- * @param registry The model registry containing alias mappings.
510
- * @param alias The alias string to resolve.
511
- * @param flags CLI flags (supports `--json`).
512
- */
513
- export async function cmdResolve(registry, alias, flags) {
514
- if (!alias) {
515
- console.error(c(RED, "Usage: kosha resolve <alias>"));
516
- process.exit(1);
517
- }
518
- await ensureDiscovered(registry);
519
- const resolved = registry.resolve(alias);
520
- if (flags.json) {
521
- console.log(JSON.stringify({ alias, resolved }, null, 2));
522
- return;
523
- }
524
- if (resolved === alias) {
525
- // resolve() returns input unchanged when no alias mapping exists
526
- console.log(c(YELLOW, `"${alias}" is not a known alias (returned as-is)`));
527
- }
528
- else {
529
- console.log(`${c(DIM, alias)} ${c(DIM, "\u2192")} ${c(CYAN, resolved)}`);
530
- }
531
- }
532
- // ── routes ───────────────────────────────────────────────────────────────
533
- /**
534
- * Show every provider route through which a model can be accessed.
535
- *
536
- * A "route" is a serving-layer entry whose normalized model ID matches the
537
- * given identifier. Output includes direct/preferred markers, origin provider,
538
- * model version hint, and serving base URL.
539
- *
540
- * Pricing columns show `—` when data is unavailable.
541
- *
542
- * @param registry The model registry to query.
543
- * @param modelId Canonical model ID or alias to look up.
544
- * @param flags CLI flags (supports `--json`).
545
- */
546
- export async function cmdRoutes(registry, modelId, flags) {
547
- if (!modelId) {
548
- console.error(c(RED, "Usage: kosha routes <model-id|alias>"));
549
- process.exit(1);
550
- }
551
- await ensureDiscovered(registry);
552
- const routes = registry.modelRouteInfo(modelId);
553
- if (flags.json) {
554
- console.log(JSON.stringify(routes, null, 2));
555
- return;
556
- }
557
- if (routes.length === 0) {
558
- console.error(c(RED, `No routes found for model: "${modelId}"`));
559
- process.exit(1);
560
- }
561
- const columns = [
562
- { header: "Pref", width: 4 },
563
- { header: "Provider", width: 14 },
564
- { header: "Model ID", width: 42 },
565
- { header: "Region", width: 14 },
566
- { header: "Origin", width: 12 },
567
- { header: "Ver", width: 12 },
568
- { header: "Base URL", width: 34 },
569
- { header: "$/M in", width: 8, align: "right" },
570
- { header: "$/M out", width: 8, align: "right" },
571
- ];
572
- const rows = routes.map((route) => [
573
- route.isPreferred ? c(GREEN, "*") : route.isDirect ? c(CYAN, "\u00B7") : " ",
574
- c(CYAN, route.provider),
575
- route.model.id,
576
- route.model.region ?? c(DIM, "—"),
577
- route.originProvider ?? c(DIM, "—"),
578
- route.version ?? c(DIM, "—"),
579
- route.baseUrl ?? c(DIM, "—"),
580
- formatPrice(route.model.pricing?.inputPerMillion),
581
- formatPrice(route.model.pricing?.outputPerMillion),
582
- ]);
583
- console.log(`\n${c(BOLD, routes[0].model.name)} ${c(DIM, `(${routes.length} route${routes.length !== 1 ? "s" : ""})`)}\n`);
584
- console.log(renderTable(columns, rows));
585
- const preferred = routes.find((route) => route.isPreferred);
586
- if (preferred) {
587
- console.log(c(DIM, `\nPreferred route: ${preferred.provider} (${preferred.baseUrl ?? "base URL unknown"})`));
588
- }
589
- }
590
328
  // ── refresh ──────────────────────────────────────────────────────────────
591
329
  /**
592
330
  * Force a full re-discovery of all providers, bypassing the cache.
@@ -607,8 +345,7 @@ export async function cmdRefresh(registry, flags) {
607
345
  }
608
346
  const providers = registry.providers_list();
609
347
  printProviderSummary(providers);
610
- // Overwrite the last summary line to include "Refreshed:" prefix
611
- // (printProviderSummary already printed the count, so we add context)
348
+ console.log(c(DIM, `\nManifest exported to ~/.kosha/registry.json`));
612
349
  }
613
350
  // ── serve ────────────────────────────────────────────────────────────────
614
351
  /**
@@ -628,129 +365,10 @@ export async function cmdServe(flags) {
628
365
  const { startServer } = await import("./server.js");
629
366
  await startServer(port);
630
367
  }
631
- // ── help & version ───────────────────────────────────────────────────────
632
- /** Print the full CLI usage / help text to stdout. */
633
- export function showHelp() {
634
- console.log(`
635
- ${c(BOLD, "kosha")} ${c(DIM, "\u2014 AI Model & Provider Discovery Registry")}
636
-
637
- ${c(BOLD, "USAGE")}
638
- kosha <command> [options]
639
-
640
- ${c(BOLD, "COMMANDS")}
641
- ${c(CYAN, "discover")} Discover all providers and models
642
- ${c(CYAN, "list")} List all known models
643
- --provider <name> Filter by serving-layer provider
644
- --origin <name> Filter by origin/creator provider (e.g. anthropic)
645
- --mode <mode> Filter by mode (chat, embedding, image, audio)
646
- --capability <cap> Filter by capability (vision, function_calling, etc.)
647
- ${c(CYAN, "search")} <query> Search models by name/ID (fuzzy match)
648
- --origin <name> Restrict search to a specific origin provider
649
- ${c(CYAN, "model")} <id|alias> Show detailed info for one model
650
- ${c(CYAN, "roles")} Show provider -> model -> roles matrix
651
- --role <role> Filter by task role (e.g. embeddings, image, tool_use)
652
- --provider <name> Filter by serving-layer provider
653
- --origin <name> Filter by model creator provider
654
- --mode <mode> Filter by mode (chat, embedding, image, audio, moderation)
655
- --capability <cap> Filter by capability tag
656
- ${c(CYAN, "capabilities")} ${c(DIM, "(caps)")} Show all capabilities across the ecosystem
657
- --provider <name> Scope to one provider
658
- ${c(CYAN, "capable")} <capability> List models with a given capability
659
- --provider <name> Filter by serving-layer provider
660
- --origin <name> Filter by origin/creator provider
661
- --mode <mode> Filter by mode (chat, embedding, image, audio)
662
- --limit <n> Maximum models to show
663
- ${c(CYAN, "cheapest")} Find cheapest eligible models
664
- --role <role> Task role, e.g. embeddings or image
665
- --capability <cap> Capability filter (vision, embedding, function_calling)
666
- --mode <mode> Mode filter
667
- --limit <n> Maximum matches to return (default 5)
668
- --price-metric <metric> input | output | blended
669
- --input-weight <n> Weight for blended metric input price
670
- --output-weight <n> Weight for blended metric output price
671
- --include-unpriced Include unpriced models after ranked matches
672
- ${c(CYAN, "routes")} <id|alias> Show all provider routes for a model
673
- ${c(CYAN, "providers")} List all providers and their status
674
- ${c(CYAN, "resolve")} <alias> Resolve an alias to canonical model ID
675
- ${c(CYAN, "latest")} Force-fetch latest model/provider details
676
- --provider <name> Scope latest fetch to one provider
677
- ${c(CYAN, "refresh")} Force re-discover all providers (bypass cache)
678
- --provider <name> Refresh only one provider
679
- ${c(CYAN, "serve")} [--port 3000] Start HTTP API server
680
-
681
- ${c(BOLD, "OPTIONS")}
682
- --json Output as JSON (works with any command)
683
- --help Show this help message
684
- --version Show version
685
-
686
- ${c(BOLD, "EXAMPLES")}
687
- ${c(DIM, "$")} kosha discover
688
- ${c(DIM, "$")} kosha list --provider anthropic
689
- ${c(DIM, "$")} kosha list --origin anthropic
690
- ${c(DIM, "$")} kosha list --mode embedding --json
691
- ${c(DIM, "$")} kosha search gemini
692
- ${c(DIM, "$")} kosha search claude --origin anthropic
693
- ${c(DIM, "$")} kosha model sonnet
694
- ${c(DIM, "$")} kosha roles --role embeddings
695
- ${c(DIM, "$")} kosha capabilities
696
- ${c(DIM, "$")} kosha capable vision
697
- ${c(DIM, "$")} kosha capable embeddings --limit 5
698
- ${c(DIM, "$")} kosha cheapest --role image --limit 3
699
- ${c(DIM, "$")} kosha routes claude-opus-4-6
700
- ${c(DIM, "$")} kosha routes gpt-4o --json
701
- ${c(DIM, "$")} kosha providers
702
- ${c(DIM, "$")} kosha latest
703
- ${c(DIM, "$")} kosha latest --provider openai --json
704
- ${c(DIM, "$")} kosha resolve haiku
705
- ${c(DIM, "$")} kosha refresh --provider anthropic
706
- ${c(DIM, "$")} kosha serve --port 8080
707
- `.trim());
708
- }
709
- /** Print the CLI version string to stdout. */
710
- export function showVersion() {
711
- console.log(`kosha-discovery v${VERSION}`);
712
- }
713
- /**
714
- * Display a branded splash screen when `kosha` is invoked with no arguments.
715
- *
716
- * Shows the Kosha logo, tagline, version, and quick-start commands.
717
- * Uses MAGENTA branding with a clean, minimal layout.
718
- */
719
- export function showSplash() {
720
- const brandWord = `${c(CYAN, "k")}${c(GREEN, "o")}${c(YELLOW, "s")}${c(MAGENTA, "h")}${c(RED, "a")}`;
721
- const mascot1 = `${c(CYAN, " /\\_/\\ ")} ${c(DIM, "assistant mascot")}`;
722
- const mascot2 = `${c(CYAN, "( o.o )")} ${c(DIM, "ready to route")}`;
723
- const mascot3 = `${c(CYAN, " > ^ < ")} ${c(DIM, "providers + models")}`;
724
- console.log(`
725
- ${c(MAGENTA, " ╔═══════════════════════════════════════════════════╗")}
726
- ${c(MAGENTA, " ║")} ${c(MAGENTA, "║")}
727
- ${c(MAGENTA, " ║")} ${c(BOLD, " █▄▀ █▀█ █▀ █ █ ▄▀█")} ${c(MAGENTA, "║")}
728
- ${c(MAGENTA, " ║")} ${c(BOLD, " █ █ █▄█ ▄█ █▀█ █▀█")} ${c(DIM, "कोश — treasury")} ${c(MAGENTA, "║")}
729
- ${c(MAGENTA, " ║")} ${c(MAGENTA, "║")}
730
- ${c(MAGENTA, " ║")} ${brandWord} ${c(DIM, "AI Model & Provider Discovery Registry")} ${c(MAGENTA, "║")}
731
- ${c(MAGENTA, " ║")} ${c(DIM, `v${VERSION}`)} ${c(MAGENTA, "║")}
732
- ${c(MAGENTA, " ║")} ${mascot1} ${c(MAGENTA, "║")}
733
- ${c(MAGENTA, " ║")} ${mascot2} ${c(MAGENTA, "║")}
734
- ${c(MAGENTA, " ║")} ${mascot3} ${c(MAGENTA, "║")}
735
- ${c(MAGENTA, " ║")} ${c(MAGENTA, "║")}
736
- ${c(MAGENTA, " ╚═══════════════════════════════════════════════════╝")}
737
-
738
- ${c(BOLD, "Quick start:")}
739
-
740
- ${c(CYAN, "kosha discover")} Scan all providers for models
741
- ${c(CYAN, "kosha list")} List all discovered models
742
- ${c(CYAN, "kosha search")} ${c(DIM, "<q>")} Search by name or ID
743
- ${c(CYAN, "kosha model")} ${c(DIM, "<id>")} Detailed info for one model
744
- ${c(CYAN, "kosha capabilities")} What capabilities exist?
745
- ${c(CYAN, "kosha capable")} ${c(DIM, "<cap>")} Models with a given capability
746
- ${c(CYAN, "kosha roles")} Provider -> model -> roles matrix
747
- ${c(CYAN, "kosha cheapest")} Cheapest models for a role
748
- ${c(CYAN, "kosha routes")} ${c(DIM, "<id>")} All provider routes for a model
749
- ${c(CYAN, "kosha providers")} Show provider status
750
- ${c(CYAN, "kosha latest")} Force-fetch latest provider/model details
751
- ${c(CYAN, "kosha serve")} Start the HTTP API server
752
-
753
- ${c(DIM, "Run")} ${c(CYAN, "kosha --help")} ${c(DIM, "for full usage.")}
754
- `);
755
- }
368
+ // ---------------------------------------------------------------------------
369
+ // Re-exports from split modules
370
+ // ---------------------------------------------------------------------------
371
+ export { cmdModel, cmdResolve, cmdRoutes } from "./cli-cmd-model.js";
372
+ export { cmdRoles, cmdCheapest, cmdCapabilities, cmdCapable } from "./cli-cmd-query.js";
373
+ export { showHelp, showVersion, showSplash } from "./cli-help.js";
756
374
  //# sourceMappingURL=cli-commands.js.map