@tyvm/knowhow 0.0.104 → 0.0.106

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 (233) hide show
  1. package/CONFIG.md +8 -5
  2. package/package.json +3 -2
  3. package/scripts/check-model-pricing.ts +509 -0
  4. package/scripts/compare-openrouter-coverage.ts +576 -0
  5. package/src/agents/base/base.ts +127 -2
  6. package/src/agents/tools/execCommand.ts +4 -0
  7. package/src/agents/tools/executeScript/definition.ts +1 -1
  8. package/src/agents/tools/index.ts +0 -1
  9. package/src/agents/tools/list.ts +3 -43
  10. package/src/agents/tools/writeFile.ts +1 -1
  11. package/src/auth/browserLogin.ts +9 -4
  12. package/src/chat/modules/RemoteSyncModule.ts +3 -0
  13. package/src/cli.ts +31 -1
  14. package/src/clients/cerebras.ts +10 -0
  15. package/src/clients/contextLimits.ts +7 -2
  16. package/src/clients/copilot.ts +23 -0
  17. package/src/clients/deepseek.ts +16 -0
  18. package/src/clients/fireworks.ts +15 -0
  19. package/src/clients/gemini.ts +45 -2
  20. package/src/clients/github.ts +16 -0
  21. package/src/clients/groq.ts +15 -0
  22. package/src/clients/http.ts +190 -6
  23. package/src/clients/index.ts +215 -9
  24. package/src/clients/llama.ts +16 -0
  25. package/src/clients/mistral.ts +16 -0
  26. package/src/clients/nvidia.ts +16 -0
  27. package/src/clients/openai.ts +41 -11
  28. package/src/clients/openrouter.ts +17 -0
  29. package/src/clients/pricing/anthropic.ts +105 -78
  30. package/src/clients/pricing/cerebras.ts +11 -0
  31. package/src/clients/pricing/copilot.ts +60 -0
  32. package/src/clients/pricing/deepseek.ts +15 -0
  33. package/src/clients/pricing/fireworks.ts +32 -0
  34. package/src/clients/pricing/github.ts +69 -0
  35. package/src/clients/pricing/google.ts +245 -206
  36. package/src/clients/pricing/groq.ts +56 -0
  37. package/src/clients/pricing/index.ts +43 -6
  38. package/src/clients/pricing/llama.ts +18 -0
  39. package/src/clients/pricing/mistral.ts +34 -0
  40. package/src/clients/pricing/models.ts +23 -0
  41. package/src/clients/pricing/nvidia.ts +102 -0
  42. package/src/clients/pricing/openai.ts +347 -171
  43. package/src/clients/pricing/openrouter.ts +36 -0
  44. package/src/clients/pricing/types.ts +110 -0
  45. package/src/clients/pricing/xai.ts +123 -66
  46. package/src/clients/types.ts +4 -0
  47. package/src/clients/xai.ts +152 -2
  48. package/src/fileSync.ts +8 -2
  49. package/src/login.ts +11 -3
  50. package/src/services/AgentSyncFs.ts +36 -12
  51. package/src/services/KnowhowClient.ts +11 -0
  52. package/src/services/LazyToolsService.ts +6 -0
  53. package/src/services/S3.ts +0 -7
  54. package/src/services/SyncedAgentWatcher.ts +13 -298
  55. package/src/services/index.ts +1 -0
  56. package/src/services/modules/index.ts +11 -2
  57. package/src/services/watchers/FsSyncer.ts +155 -0
  58. package/src/services/watchers/RemoteSyncer.ts +153 -0
  59. package/src/services/watchers/index.ts +2 -0
  60. package/src/types.ts +56 -279
  61. package/src/worker.ts +174 -0
  62. package/tests/clients/pricing.test.ts +37 -0
  63. package/tests/manual/clients/completions.json +838 -226
  64. package/tests/manual/clients/completions.test.ts +46 -31
  65. package/ts_build/package.json +3 -2
  66. package/ts_build/src/agents/base/base.d.ts +17 -1
  67. package/ts_build/src/agents/base/base.js +82 -1
  68. package/ts_build/src/agents/base/base.js.map +1 -1
  69. package/ts_build/src/agents/tools/execCommand.js +3 -0
  70. package/ts_build/src/agents/tools/execCommand.js.map +1 -1
  71. package/ts_build/src/agents/tools/executeScript/definition.js +1 -1
  72. package/ts_build/src/agents/tools/executeScript/definition.js.map +1 -1
  73. package/ts_build/src/agents/tools/index.d.ts +0 -1
  74. package/ts_build/src/agents/tools/index.js +0 -1
  75. package/ts_build/src/agents/tools/index.js.map +1 -1
  76. package/ts_build/src/agents/tools/list.js +3 -38
  77. package/ts_build/src/agents/tools/list.js.map +1 -1
  78. package/ts_build/src/agents/tools/visionTool.d.ts +1 -1
  79. package/ts_build/src/agents/tools/writeFile.js +1 -1
  80. package/ts_build/src/agents/tools/writeFile.js.map +1 -1
  81. package/ts_build/src/ai.d.ts +1 -1
  82. package/ts_build/src/auth/browserLogin.d.ts +2 -1
  83. package/ts_build/src/auth/browserLogin.js +10 -3
  84. package/ts_build/src/auth/browserLogin.js.map +1 -1
  85. package/ts_build/src/chat/modules/RemoteSyncModule.js +1 -0
  86. package/ts_build/src/chat/modules/RemoteSyncModule.js.map +1 -1
  87. package/ts_build/src/cli.js +19 -0
  88. package/ts_build/src/cli.js.map +1 -1
  89. package/ts_build/src/clients/anthropic.d.ts +1 -82
  90. package/ts_build/src/clients/cerebras.d.ts +4 -0
  91. package/ts_build/src/clients/cerebras.js +14 -0
  92. package/ts_build/src/clients/cerebras.js.map +1 -0
  93. package/ts_build/src/clients/contextLimits.js +7 -2
  94. package/ts_build/src/clients/contextLimits.js.map +1 -1
  95. package/ts_build/src/clients/copilot.d.ts +4 -0
  96. package/ts_build/src/clients/copilot.js +15 -0
  97. package/ts_build/src/clients/copilot.js.map +1 -0
  98. package/ts_build/src/clients/deepseek.d.ts +4 -0
  99. package/ts_build/src/clients/deepseek.js +15 -0
  100. package/ts_build/src/clients/deepseek.js.map +1 -0
  101. package/ts_build/src/clients/fireworks.d.ts +4 -0
  102. package/ts_build/src/clients/fireworks.js +15 -0
  103. package/ts_build/src/clients/fireworks.js.map +1 -0
  104. package/ts_build/src/clients/gemini.d.ts +1 -0
  105. package/ts_build/src/clients/gemini.js +28 -1
  106. package/ts_build/src/clients/gemini.js.map +1 -1
  107. package/ts_build/src/clients/github.d.ts +4 -0
  108. package/ts_build/src/clients/github.js +15 -0
  109. package/ts_build/src/clients/github.js.map +1 -0
  110. package/ts_build/src/clients/groq.d.ts +4 -0
  111. package/ts_build/src/clients/groq.js +15 -0
  112. package/ts_build/src/clients/groq.js.map +1 -0
  113. package/ts_build/src/clients/http.d.ts +22 -1
  114. package/ts_build/src/clients/http.js +132 -7
  115. package/ts_build/src/clients/http.js.map +1 -1
  116. package/ts_build/src/clients/index.d.ts +22 -0
  117. package/ts_build/src/clients/index.js +150 -5
  118. package/ts_build/src/clients/index.js.map +1 -1
  119. package/ts_build/src/clients/llama.d.ts +4 -0
  120. package/ts_build/src/clients/llama.js +15 -0
  121. package/ts_build/src/clients/llama.js.map +1 -0
  122. package/ts_build/src/clients/mistral.d.ts +4 -0
  123. package/ts_build/src/clients/mistral.js +15 -0
  124. package/ts_build/src/clients/mistral.js.map +1 -0
  125. package/ts_build/src/clients/nvidia.d.ts +4 -0
  126. package/ts_build/src/clients/nvidia.js +15 -0
  127. package/ts_build/src/clients/nvidia.js.map +1 -0
  128. package/ts_build/src/clients/openai.d.ts +4 -206
  129. package/ts_build/src/clients/openai.js +27 -9
  130. package/ts_build/src/clients/openai.js.map +1 -1
  131. package/ts_build/src/clients/openrouter.d.ts +4 -0
  132. package/ts_build/src/clients/openrouter.js +15 -0
  133. package/ts_build/src/clients/openrouter.js.map +1 -0
  134. package/ts_build/src/clients/pricing/anthropic.d.ts +26 -78
  135. package/ts_build/src/clients/pricing/anthropic.js +75 -78
  136. package/ts_build/src/clients/pricing/anthropic.js.map +1 -1
  137. package/ts_build/src/clients/pricing/cerebras.d.ts +4 -0
  138. package/ts_build/src/clients/pricing/cerebras.js +11 -0
  139. package/ts_build/src/clients/pricing/cerebras.js.map +1 -0
  140. package/ts_build/src/clients/pricing/copilot.d.ts +5 -0
  141. package/ts_build/src/clients/pricing/copilot.js +35 -0
  142. package/ts_build/src/clients/pricing/copilot.js.map +1 -0
  143. package/ts_build/src/clients/pricing/deepseek.d.ts +5 -0
  144. package/ts_build/src/clients/pricing/deepseek.js +10 -0
  145. package/ts_build/src/clients/pricing/deepseek.js.map +1 -0
  146. package/ts_build/src/clients/pricing/fireworks.d.ts +5 -0
  147. package/ts_build/src/clients/pricing/fireworks.js +21 -0
  148. package/ts_build/src/clients/pricing/fireworks.js.map +1 -0
  149. package/ts_build/src/clients/pricing/github.d.ts +4 -0
  150. package/ts_build/src/clients/pricing/github.js +58 -0
  151. package/ts_build/src/clients/pricing/github.js.map +1 -0
  152. package/ts_build/src/clients/pricing/google.d.ts +59 -6
  153. package/ts_build/src/clients/pricing/google.js +214 -167
  154. package/ts_build/src/clients/pricing/google.js.map +1 -1
  155. package/ts_build/src/clients/pricing/groq.d.ts +5 -0
  156. package/ts_build/src/clients/pricing/groq.js +41 -0
  157. package/ts_build/src/clients/pricing/groq.js.map +1 -0
  158. package/ts_build/src/clients/pricing/index.d.ts +17 -6
  159. package/ts_build/src/clients/pricing/index.js +65 -10
  160. package/ts_build/src/clients/pricing/index.js.map +1 -1
  161. package/ts_build/src/clients/pricing/llama.d.ts +4 -0
  162. package/ts_build/src/clients/pricing/llama.js +14 -0
  163. package/ts_build/src/clients/pricing/llama.js.map +1 -0
  164. package/ts_build/src/clients/pricing/mistral.d.ts +5 -0
  165. package/ts_build/src/clients/pricing/mistral.js +23 -0
  166. package/ts_build/src/clients/pricing/mistral.js.map +1 -0
  167. package/ts_build/src/clients/pricing/models.d.ts +9 -0
  168. package/ts_build/src/clients/pricing/models.js +19 -0
  169. package/ts_build/src/clients/pricing/models.js.map +1 -0
  170. package/ts_build/src/clients/pricing/nvidia.d.ts +8 -0
  171. package/ts_build/src/clients/pricing/nvidia.js +96 -0
  172. package/ts_build/src/clients/pricing/nvidia.js.map +1 -0
  173. package/ts_build/src/clients/pricing/openai.d.ts +86 -197
  174. package/ts_build/src/clients/pricing/openai.js +294 -168
  175. package/ts_build/src/clients/pricing/openai.js.map +1 -1
  176. package/ts_build/src/clients/pricing/openrouter.d.ts +4 -0
  177. package/ts_build/src/clients/pricing/openrouter.js +29 -0
  178. package/ts_build/src/clients/pricing/openrouter.js.map +1 -0
  179. package/ts_build/src/clients/pricing/types.d.ts +46 -0
  180. package/ts_build/src/clients/pricing/types.js +49 -0
  181. package/ts_build/src/clients/pricing/types.js.map +1 -0
  182. package/ts_build/src/clients/pricing/xai.d.ts +39 -64
  183. package/ts_build/src/clients/pricing/xai.js +93 -60
  184. package/ts_build/src/clients/pricing/xai.js.map +1 -1
  185. package/ts_build/src/clients/types.d.ts +1 -0
  186. package/ts_build/src/clients/xai.d.ts +2 -58
  187. package/ts_build/src/clients/xai.js +123 -2
  188. package/ts_build/src/clients/xai.js.map +1 -1
  189. package/ts_build/src/fileSync.js +7 -2
  190. package/ts_build/src/fileSync.js.map +1 -1
  191. package/ts_build/src/login.js +8 -2
  192. package/ts_build/src/login.js.map +1 -1
  193. package/ts_build/src/services/AgentSyncFs.js +1 -0
  194. package/ts_build/src/services/AgentSyncFs.js.map +1 -1
  195. package/ts_build/src/services/KnowhowClient.d.ts +1 -0
  196. package/ts_build/src/services/KnowhowClient.js +7 -0
  197. package/ts_build/src/services/KnowhowClient.js.map +1 -1
  198. package/ts_build/src/services/LazyToolsService.d.ts +1 -0
  199. package/ts_build/src/services/LazyToolsService.js +3 -0
  200. package/ts_build/src/services/LazyToolsService.js.map +1 -1
  201. package/ts_build/src/services/S3.js +0 -7
  202. package/ts_build/src/services/S3.js.map +1 -1
  203. package/ts_build/src/services/SyncedAgentWatcher.d.ts +0 -51
  204. package/ts_build/src/services/SyncedAgentWatcher.js +1 -282
  205. package/ts_build/src/services/SyncedAgentWatcher.js.map +1 -1
  206. package/ts_build/src/services/index.d.ts +1 -0
  207. package/ts_build/src/services/index.js +1 -0
  208. package/ts_build/src/services/index.js.map +1 -1
  209. package/ts_build/src/services/modules/index.js +41 -1
  210. package/ts_build/src/services/modules/index.js.map +1 -1
  211. package/ts_build/src/services/watchers/FsSyncer.d.ts +27 -0
  212. package/ts_build/src/services/watchers/FsSyncer.js +135 -0
  213. package/ts_build/src/services/watchers/FsSyncer.js.map +1 -0
  214. package/ts_build/src/services/watchers/RemoteSyncer.d.ts +28 -0
  215. package/ts_build/src/services/watchers/RemoteSyncer.js +126 -0
  216. package/ts_build/src/services/watchers/RemoteSyncer.js.map +1 -0
  217. package/ts_build/src/services/watchers/index.d.ts +2 -0
  218. package/ts_build/src/services/watchers/index.js +19 -0
  219. package/ts_build/src/services/watchers/index.js.map +1 -0
  220. package/ts_build/src/types.d.ts +163 -124
  221. package/ts_build/src/types.js +33 -213
  222. package/ts_build/src/types.js.map +1 -1
  223. package/ts_build/src/worker.d.ts +4 -0
  224. package/ts_build/src/worker.js +140 -0
  225. package/ts_build/src/worker.js.map +1 -1
  226. package/ts_build/tests/clients/pricing.test.js +21 -0
  227. package/ts_build/tests/clients/pricing.test.js.map +1 -1
  228. package/ts_build/tests/manual/clients/completions.test.js +27 -24
  229. package/ts_build/tests/manual/clients/completions.test.js.map +1 -1
  230. package/src/clients/pricing/catalog.ts +0 -287
  231. package/ts_build/src/clients/pricing/catalog.d.ts +0 -28
  232. package/ts_build/src/clients/pricing/catalog.js +0 -179
  233. package/ts_build/src/clients/pricing/catalog.js.map +0 -1
@@ -0,0 +1,576 @@
1
+ #!/usr/bin/env npx ts-node
2
+ /**
3
+ * compare-openrouter-coverage.ts
4
+ *
5
+ * Fetches the live OpenRouter model list and compares it against knowhow's
6
+ * registered providers and models.
7
+ *
8
+ * Usage:
9
+ * npx ts-node scripts/compare-openrouter-coverage.ts
10
+ * npx ts-node scripts/compare-openrouter-coverage.ts --provider anthropic
11
+ * npx ts-node scripts/compare-openrouter-coverage.ts --output coverage.md
12
+ * npx ts-node scripts/compare-openrouter-coverage.ts --missing-providers
13
+ *
14
+ * Options:
15
+ * --provider <name> Filter to a specific OpenRouter provider slug
16
+ * --output <file> Write results to a markdown file
17
+ * --missing-providers Only show providers we don't support at all
18
+ * --show-ours-only Show models we have that OpenRouter doesn't
19
+ * --include-deprecated Include deprecated/retired models in coverage comparison (default: excluded)
20
+ */
21
+
22
+ import https from "https";
23
+ import fs from "fs";
24
+ import path from "path";
25
+
26
+ // ─── Our model registries ────────────────────────────────────────────────────
27
+ import { OpenAiTextPricing } from "../src/clients/pricing/openai";
28
+ import { AnthropicTextPricing } from "../src/clients/pricing/anthropic";
29
+ import { GeminiTextPricing } from "../src/clients/pricing/google";
30
+ import { XaiTextPricing } from "../src/clients/pricing/xai";
31
+ import { GroqTextPricing } from "../src/clients/pricing/groq";
32
+ import { DeepSeekTextPricing } from "../src/clients/pricing/deepseek";
33
+ import { MistralTextPricing } from "../src/clients/pricing/mistral";
34
+ import { NvidiaTextPricing } from "../src/clients/pricing/nvidia";
35
+ import { LlamaTextPricing } from "../src/clients/pricing/llama";
36
+ import { CerebrasTextPricing } from "../src/clients/pricing/cerebras";
37
+ import { ALL_MODEL_CATALOG } from "../src/clients/pricing/models";
38
+
39
+ // ─── CLI args ────────────────────────────────────────────────────────────────
40
+ const args = process.argv.slice(2);
41
+ const getArg = (flag: string) => {
42
+ const idx = args.indexOf(flag);
43
+ return idx !== -1 ? args[idx + 1] : undefined;
44
+ };
45
+ const hasFlag = (flag: string) => args.includes(flag);
46
+
47
+ const filterProvider = getArg("--provider");
48
+ const outputFile = getArg("--output");
49
+ const missingProvidersOnly = hasFlag("--missing-providers");
50
+ const showOursOnly = hasFlag("--show-ours-only");
51
+ const includeDeprecated = hasFlag("--include-deprecated");
52
+
53
+ // ─── Our providers: map OpenRouter provider slug → set of model IDs ──────────
54
+ // OpenRouter model IDs are in the format "provider/model-name"
55
+ // We map our internal provider names to their OpenRouter slug equivalents.
56
+ //
57
+ // Note: We intentionally EXCLUDE "openrouter" itself from this comparison
58
+ // since we're comparing *against* OpenRouter, not our openrouter passthrough.
59
+ //
60
+ // By default, deprecated/retired models are excluded from coverage comparison.
61
+ // Pass --include-deprecated to include them.
62
+
63
+ // Build a set of deprecated/limitedAvailability model IDs from the catalog
64
+ const excludedModelIds = new Set(
65
+ ALL_MODEL_CATALOG
66
+ .filter((e) => e.deprecated === true || e.limitedAvailability === true || e.type === "live")
67
+ .map((e) => e.id.toLowerCase())
68
+ );
69
+
70
+ // All models (including deprecated) — used for coverage matching so deprecated
71
+ // models we DO have are not counted as gaps
72
+ function allModels(pricing: Record<string, any>): Set<string> {
73
+ return new Set(Object.keys(pricing));
74
+ }
75
+
76
+ // Active (non-deprecated) models — used for "we have X models" count
77
+ function activeModels(pricing: Record<string, any>): Set<string> {
78
+ return new Set(
79
+ Object.keys(pricing).filter(
80
+ (id) => includeDeprecated || !excludedModelIds.has(id.toLowerCase())
81
+ )
82
+ );
83
+ }
84
+
85
+ const OUR_PROVIDERS: Record<string, Set<string>> = {
86
+ openai: activeModels(OpenAiTextPricing),
87
+ anthropic: activeModels(AnthropicTextPricing),
88
+ google: activeModels(GeminiTextPricing),
89
+ "x-ai": activeModels(XaiTextPricing), // OpenRouter uses "x-ai" for xai
90
+ groq: activeModels(GroqTextPricing),
91
+ deepseek: activeModels(DeepSeekTextPricing),
92
+ mistralai: activeModels(MistralTextPricing), // OpenRouter uses "mistralai"
93
+ nvidia: activeModels(NvidiaTextPricing),
94
+ meta: new Set<string>(), // populated below from nvidia & llama
95
+ llama: activeModels(LlamaTextPricing),
96
+ cerebras: activeModels(CerebrasTextPricing),
97
+ };
98
+
99
+ // All models including deprecated — for coverage matching only
100
+ const ALL_OUR_PROVIDERS: Record<string, Set<string>> = {
101
+ openai: allModels(OpenAiTextPricing),
102
+ anthropic: allModels(AnthropicTextPricing),
103
+ google: allModels(GeminiTextPricing),
104
+ "x-ai": allModels(XaiTextPricing),
105
+ groq: allModels(GroqTextPricing),
106
+ deepseek: allModels(DeepSeekTextPricing),
107
+ mistralai: allModels(MistralTextPricing),
108
+ nvidia: allModels(NvidiaTextPricing),
109
+ meta: new Set<string>(),
110
+ llama: allModels(LlamaTextPricing),
111
+ cerebras: allModels(CerebrasTextPricing),
112
+ };
113
+
114
+ // Normalize our model IDs to bare model names (strip "provider/" prefix if present)
115
+ // so we can compare against OpenRouter's model names within a provider
116
+ function stripProvider(modelId: string): string {
117
+ const parts = modelId.split("/");
118
+ if (parts.length > 1) return parts.slice(1).join("/");
119
+ return modelId;
120
+ }
121
+
122
+ // Normalize a model ID for fuzzy matching:
123
+ // - lowercase
124
+ // - replace dots with dashes (e.g. "claude-opus-4.7" → "claude-opus-4-7")
125
+ // - strip variant suffixes like ":thinking", ":free", ":extended"
126
+ // - strip known trailing date suffixes like "-20250514", "-20251001", etc.
127
+ // - strip trailing "-beta", "-preview", "-latest", "-exp"
128
+ const DATE_SUFFIX_RE = /-\d{8}$/;
129
+ const KNOWN_VERSION_SUFFIXES = /-(beta|preview|latest|exp|rc\d*)$/i;
130
+
131
+ function normalizeModelId(id: string): string {
132
+ return id
133
+ .toLowerCase()
134
+ .replace(/\./g, "-") // dots to dashes
135
+ .replace(/:[^:]+$/, "") // strip :thinking, :free, :extended, etc.
136
+ .replace(DATE_SUFFIX_RE, "") // strip -20250514 style date suffix
137
+ .replace(KNOWN_VERSION_SUFFIXES, ""); // strip -beta, -preview, etc.
138
+ }
139
+
140
+ // Check if an OR model ID matches one of our model IDs.
141
+ // Returns true if:
142
+ // 1. Exact normalized match, OR
143
+ // 2. Our model starts with OR model (we have a dated variant, OR has generic alias)
144
+ // e.g. OR "claude-opus-4-5" matches our "claude-opus-4-5-20251101"
145
+ // 3. OR model starts with our model (OR has more specific, we have base name)
146
+ function modelMatches(orBareId: string, ourBareModels: Set<string>): boolean {
147
+ const orNorm = normalizeModelId(orBareId);
148
+ for (const ourId of ourBareModels) {
149
+ const ourNorm = normalizeModelId(ourId);
150
+ if (orNorm === ourNorm) return true;
151
+ // Our model is a dated variant of the OR model (e.g. claude-opus-4-5 vs claude-opus-4-5-20251101)
152
+ if (ourNorm.startsWith(orNorm + "-") && /^\d+$/.test(ourNorm.slice(orNorm.length + 1))) return true;
153
+ // OR model is more specific than ours (e.g. or has grok-4-0709, we have grok-4)
154
+ if (orNorm.startsWith(ourNorm + "-") && /^\d+$/.test(orNorm.slice(ourNorm.length + 1))) return true;
155
+ // OR model has a non-numeric variant suffix (e.g. claude-opus-4-6-fast vs claude-opus-4-6)
156
+ if (orNorm.startsWith(ourNorm + "-") && /^[a-z]+$/.test(orNorm.slice(ourNorm.length + 1))) return true;
157
+ }
158
+ return false;
159
+ }
160
+
161
+ // ─── Fetch OpenRouter models ──────────────────────────────────────────────────
162
+ interface OpenRouterModel {
163
+ id: string; // e.g. "anthropic/claude-3.5-sonnet"
164
+ name: string;
165
+ description?: string;
166
+ context_length?: number;
167
+ pricing?: {
168
+ prompt?: string; // USD per token (as string)
169
+ completion?: string;
170
+ };
171
+ architecture?: {
172
+ modality?: string;
173
+ };
174
+ }
175
+
176
+ function fetchOpenRouterModels(): Promise<OpenRouterModel[]> {
177
+ return new Promise((resolve, reject) => {
178
+ const options = {
179
+ hostname: "openrouter.ai",
180
+ path: "/api/v1/models",
181
+ method: "GET",
182
+ headers: {
183
+ "HTTP-Referer": "https://github.com/knowhow",
184
+ "X-Title": "knowhow-coverage-check",
185
+ },
186
+ };
187
+
188
+ const req = https.request(options, (res) => {
189
+ let data = "";
190
+ res.on("data", (chunk) => (data += chunk));
191
+ res.on("end", () => {
192
+ try {
193
+ const parsed = JSON.parse(data);
194
+ resolve(parsed.data || []);
195
+ } catch (e) {
196
+ reject(new Error(`Failed to parse OpenRouter response: ${e}`));
197
+ }
198
+ });
199
+ });
200
+
201
+ req.on("error", reject);
202
+ req.end();
203
+ });
204
+ }
205
+
206
+ // ─── Pricing helpers ──────────────────────────────────────────────────────────
207
+
208
+ /** Convert OpenRouter per-token price string to per-1M-token USD */
209
+ function orPricePerMillion(pricePerToken: string | undefined): number | null {
210
+ if (!pricePerToken) return null;
211
+ const v = parseFloat(pricePerToken);
212
+ if (isNaN(v)) return null;
213
+ return v * 1_000_000;
214
+ }
215
+
216
+ function formatPrice(v: number | null): string {
217
+ if (v === null) return "n/a";
218
+ if (v === 0) return "$0 (free)";
219
+ return `$${v.toFixed(4)}/1M`;
220
+ }
221
+
222
+ // ─── Analysis ─────────────────────────────────────────────────────────────────
223
+ interface MissingModel {
224
+ id: string; // bare model name (without provider/ prefix)
225
+ fullId: string; // full OR id e.g. "anthropic/claude-..."
226
+ orInputPer1M: number | null;
227
+ orOutputPer1M: number | null;
228
+ }
229
+
230
+ interface ProviderComparison {
231
+ orProvider: string;
232
+ weHaveProvider: boolean;
233
+ ourProviderKey: string | null;
234
+ orModels: string[];
235
+ modelsWeHave: string[]; // OR models we also have
236
+ modelsWeMiss: MissingModel[]; // OR models we don't have (with pricing)
237
+ modelsOnlyWe: string[]; // our models not on OR
238
+ orModelCount: number;
239
+ coveragePct: number;
240
+ }
241
+
242
+ function normalizeProviderName(orProvider: string): string | null {
243
+ // Map OpenRouter provider slugs to our internal provider keys
244
+ const MAP: Record<string, string> = {
245
+ "openai": "openai",
246
+ "anthropic": "anthropic",
247
+ "google": "google",
248
+ "x-ai": "x-ai",
249
+ "groq": "groq",
250
+ "deepseek": "deepseek",
251
+ "mistralai": "mistralai",
252
+ "nvidia": "nvidia",
253
+ "meta-llama": "meta",
254
+ "llama": "llama",
255
+ "cerebras": "cerebras",
256
+ };
257
+ return MAP[orProvider] ?? null;
258
+ }
259
+
260
+ function computeProviderComparison(
261
+ orProvider: string,
262
+ orModels: OpenRouterModel[],
263
+ ): ProviderComparison {
264
+ const ourKey = normalizeProviderName(orProvider);
265
+ const ourModels = ourKey ? OUR_PROVIDERS[ourKey] : null;
266
+ const weHaveProvider = ourKey !== null && ourModels !== undefined;
267
+
268
+ // All models (including deprecated) for matching — so deprecated models aren't shown as gaps
269
+ const allOurModels = ourKey ? ALL_OUR_PROVIDERS[ourKey] : null;
270
+
271
+ // OR model IDs within this provider (bare, without provider/ prefix)
272
+ const orModelEntries = orModels.map((m) => {
273
+ const parts = m.id.split("/");
274
+ const bareId = parts.slice(1).join("/");
275
+ return {
276
+ bareId,
277
+ fullId: m.id,
278
+ orInputPer1M: orPricePerMillion(m.pricing?.prompt),
279
+ orOutputPer1M: orPricePerMillion(m.pricing?.completion),
280
+ };
281
+ });
282
+
283
+ const orModelIds = orModelEntries.map((e) => e.bareId);
284
+
285
+ if (!weHaveProvider || !ourModels) {
286
+ return {
287
+ orProvider,
288
+ weHaveProvider: false,
289
+ ourProviderKey: null,
290
+ orModels: orModelIds,
291
+ modelsWeHave: [],
292
+ modelsWeMiss: orModelEntries.map((e) => ({
293
+ id: e.bareId,
294
+ fullId: e.fullId,
295
+ orInputPer1M: e.orInputPer1M,
296
+ orOutputPer1M: e.orOutputPer1M,
297
+ })),
298
+ modelsOnlyWe: [],
299
+ orModelCount: orModelIds.length,
300
+ coveragePct: 0,
301
+ };
302
+ }
303
+
304
+ // Normalize our model IDs for comparison
305
+ // Use ALL models (including deprecated) for matching to avoid showing deprecated as gaps
306
+ const allOurBareModels = new Set([...(allOurModels ?? ourModels)].map(stripProvider));
307
+
308
+ const modelsWeHave: string[] = [];
309
+ const modelsWeMiss: MissingModel[] = [];
310
+ for (const entry of orModelEntries) {
311
+ if (modelMatches(entry.bareId, allOurBareModels)) {
312
+ modelsWeHave.push(entry.bareId);
313
+ } else {
314
+ modelsWeMiss.push({
315
+ id: entry.bareId,
316
+ fullId: entry.fullId,
317
+ orInputPer1M: entry.orInputPer1M,
318
+ orOutputPer1M: entry.orOutputPer1M,
319
+ });
320
+ }
321
+ }
322
+
323
+ // Models we have but OR doesn't list
324
+ const modelsOnlyWe: string[] = [...ourModels]
325
+ .map(stripProvider)
326
+ .filter((ourId) => !orModelEntries.some((entry) => modelMatches(entry.bareId, new Set([ourId]))));
327
+
328
+ const coveragePct =
329
+ orModelIds.length > 0
330
+ ? Math.round((modelsWeHave.length / orModelIds.length) * 100)
331
+ : 100;
332
+
333
+ return {
334
+ orProvider,
335
+ weHaveProvider: true,
336
+ ourProviderKey: ourKey,
337
+ orModels: orModelIds,
338
+ modelsWeHave,
339
+ modelsWeMiss,
340
+ modelsOnlyWe,
341
+ orModelCount: orModelIds.length,
342
+ coveragePct,
343
+ };
344
+ }
345
+
346
+ // ─── Formatting ───────────────────────────────────────────────────────────────
347
+ function pct(n: number) {
348
+ return `${n}%`;
349
+ }
350
+
351
+ function formatMarkdown(comparisons: ProviderComparison[]): string {
352
+ const lines: string[] = [];
353
+
354
+ lines.push("# OpenRouter vs Knowhow Model Coverage");
355
+ lines.push("");
356
+ lines.push(`> Generated: ${new Date().toISOString()}`);
357
+ lines.push("");
358
+
359
+ // Summary table
360
+ lines.push("## Summary");
361
+ lines.push("");
362
+ lines.push("| Provider | We Support? | OR Models | We Have | We Miss | Coverage |");
363
+ lines.push("|----------|-------------|-----------|---------|---------|----------|");
364
+
365
+ for (const c of comparisons) {
366
+ const support = c.weHaveProvider ? "✅" : "❌";
367
+ lines.push(
368
+ `| ${c.orProvider} | ${support} | ${c.orModelCount} | ${c.modelsWeHave.length} | ${c.modelsWeMiss.length} | ${pct(c.coveragePct)} |`
369
+ );
370
+ }
371
+ lines.push("");
372
+
373
+ // Providers we don't support at all
374
+ const missing = comparisons.filter((c) => !c.weHaveProvider);
375
+ if (missing.length > 0) {
376
+ lines.push("## ❌ Providers We Don't Support");
377
+ lines.push("");
378
+ for (const c of missing) {
379
+ lines.push(`### ${c.orProvider} (${c.orModelCount} models on OpenRouter)`);
380
+ lines.push("");
381
+ lines.push("| Model | Input/1M | Output/1M |");
382
+ lines.push("|-------|----------|-----------|");
383
+ for (const m of c.modelsWeMiss) {
384
+ lines.push(`| \`${m.fullId}\` | ${formatPrice(m.orInputPer1M)} | ${formatPrice(m.orOutputPer1M)} |`);
385
+ }
386
+ lines.push("");
387
+ }
388
+ }
389
+
390
+ // Providers we support — model-level gaps
391
+ const supported = comparisons.filter((c) => c.weHaveProvider);
392
+ if (supported.length > 0) {
393
+ lines.push("## ✅ Providers We Support — Model Gaps");
394
+ lines.push("");
395
+ for (const c of supported) {
396
+ lines.push(
397
+ `### ${c.orProvider} (${pct(c.coveragePct)} coverage — ${c.modelsWeHave.length}/${c.orModelCount} OR models)`
398
+ );
399
+ lines.push("");
400
+
401
+ if (c.modelsWeMiss.length > 0) {
402
+ lines.push("**Missing from us (OpenRouter has these):**");
403
+ lines.push("");
404
+ lines.push("| Model | Input/1M | Output/1M |");
405
+ lines.push("|-------|----------|-----------|");
406
+ for (const m of c.modelsWeMiss) {
407
+ lines.push(`| \`${m.fullId}\` | ${formatPrice(m.orInputPer1M)} | ${formatPrice(m.orOutputPer1M)} |`);
408
+ }
409
+ lines.push("");
410
+ }
411
+
412
+ if (showOursOnly && c.modelsOnlyWe.length > 0) {
413
+ lines.push("**We have (OpenRouter doesn't list these):**");
414
+ lines.push("");
415
+ for (const m of c.modelsOnlyWe) {
416
+ lines.push(`- ${m}`);
417
+ }
418
+ lines.push("");
419
+ }
420
+ }
421
+ }
422
+
423
+ return lines.join("\n");
424
+ }
425
+
426
+ function printConsole(comparisons: ProviderComparison[]) {
427
+ console.log("\n╔══════════════════════════════════════════════════════╗");
428
+ console.log("║ OpenRouter vs Knowhow Model Coverage Report ║");
429
+ console.log("╚══════════════════════════════════════════════════════╝\n");
430
+
431
+ // Summary
432
+ console.log("┌─ SUMMARY ──────────────────────────────────────────────────────────┐");
433
+ console.log(
434
+ sprintf("│ %-25s %-12s %8s %8s %8s %8s │",
435
+ "Provider", "We Support?", "OR Models", "We Have", "We Miss", "Coverage")
436
+ );
437
+ console.log("├" + "─".repeat(70) + "┤");
438
+ for (const c of comparisons) {
439
+ const support = c.weHaveProvider ? "✅ yes" : "❌ no";
440
+ console.log(
441
+ sprintf("│ %-25s %-12s %8d %8d %8d %7s │",
442
+ c.orProvider, support, c.orModelCount, c.modelsWeHave.length, c.modelsWeMiss.length, pct(c.coveragePct))
443
+ );
444
+ }
445
+ console.log("└" + "─".repeat(70) + "┘\n");
446
+
447
+ // Providers we don't support
448
+ const missing = comparisons.filter((c) => !c.weHaveProvider);
449
+ if (missing.length > 0) {
450
+ console.log("❌ PROVIDERS WE DON'T SUPPORT:");
451
+ for (const c of missing) {
452
+ console.log(`\n ${c.orProvider} — ${c.orModelCount} models on OpenRouter:`);
453
+ for (const m of c.modelsWeMiss.slice(0, 10)) {
454
+ const inputStr = formatPrice(m.orInputPer1M).padEnd(14);
455
+ const outputStr = formatPrice(m.orOutputPer1M);
456
+ console.log(` • ${c.orProvider}/${m.id} [in: ${inputStr} out: ${outputStr}]`);
457
+ }
458
+ if (c.modelsWeMiss.length > 10) {
459
+ console.log(` … and ${c.modelsWeMiss.length - 10} more`);
460
+ }
461
+ }
462
+ console.log();
463
+ }
464
+
465
+ // Model gaps per supported provider
466
+ const supported = comparisons.filter((c) => c.weHaveProvider && c.modelsWeMiss.length > 0);
467
+ if (supported.length > 0 && !missingProvidersOnly) {
468
+ console.log("📋 MODEL GAPS (models on OpenRouter we don't have):");
469
+ for (const c of supported) {
470
+ console.log(`\n ${c.orProvider} — ${pct(c.coveragePct)} coverage (missing ${c.modelsWeMiss.length} models):`);
471
+ for (const m of c.modelsWeMiss.slice(0, 15)) {
472
+ const inputStr = formatPrice(m.orInputPer1M).padEnd(14);
473
+ const outputStr = formatPrice(m.orOutputPer1M);
474
+ console.log(` • ${c.orProvider}/${m.id} [in: ${inputStr} out: ${outputStr}]`);
475
+ }
476
+ if (c.modelsWeMiss.length > 15) {
477
+ console.log(` … and ${c.modelsWeMiss.length - 15} more`);
478
+ }
479
+ }
480
+ console.log();
481
+ }
482
+
483
+ // Models we have but OR doesn't
484
+ if (showOursOnly) {
485
+ const weOnly = comparisons.filter((c) => c.weHaveProvider && c.modelsOnlyWe.length > 0);
486
+ if (weOnly.length > 0) {
487
+ console.log("🔵 MODELS WE HAVE (NOT on OpenRouter):");
488
+ for (const c of weOnly) {
489
+ console.log(`\n ${c.orProvider} — ${c.modelsOnlyWe.length} exclusive models:`);
490
+ for (const m of c.modelsOnlyWe) {
491
+ console.log(` • ${m}`);
492
+ }
493
+ }
494
+ console.log();
495
+ }
496
+ }
497
+ }
498
+
499
+ // Minimal sprintf-like helper for fixed-width columns
500
+ function sprintf(fmt: string, ...args: (string | number)[]): string {
501
+ let i = 0;
502
+ return fmt.replace(/%-?(\d+)[sd]/g, (match, width) => {
503
+ const val = String(args[i++] ?? "");
504
+ const w = parseInt(width);
505
+ const leftAlign = match[1] === "-";
506
+ if (leftAlign) return val.padEnd(w);
507
+ return val.padStart(w);
508
+ });
509
+ }
510
+
511
+ // ─── Main ─────────────────────────────────────────────────────────────────────
512
+ async function main() {
513
+ console.log("⏳ Fetching OpenRouter model list...");
514
+ let allModels: OpenRouterModel[];
515
+ try {
516
+ allModels = await fetchOpenRouterModels();
517
+ } catch (e) {
518
+ console.error("❌ Failed to fetch OpenRouter models:", e);
519
+ process.exit(1);
520
+ }
521
+ console.log(`✅ Fetched ${allModels.length} models from OpenRouter\n`);
522
+
523
+ // Group by provider (first segment of model ID)
524
+ const byProvider = new Map<string, OpenRouterModel[]>();
525
+ for (const model of allModels) {
526
+ const parts = model.id.split("/");
527
+ const provider = parts[0];
528
+ if (!byProvider.has(provider)) byProvider.set(provider, []);
529
+ byProvider.get(provider)!.push(model);
530
+ }
531
+
532
+ // Apply provider filter if specified
533
+ let providers = [...byProvider.keys()].sort();
534
+ if (filterProvider) {
535
+ providers = providers.filter((p) =>
536
+ p.toLowerCase().includes(filterProvider.toLowerCase())
537
+ );
538
+ if (providers.length === 0) {
539
+ console.error(`❌ No OpenRouter providers matching "${filterProvider}"`);
540
+ process.exit(1);
541
+ }
542
+ }
543
+
544
+ // Compute comparisons
545
+ const comparisons: ProviderComparison[] = providers.map((p) =>
546
+ computeProviderComparison(p, byProvider.get(p)!)
547
+ );
548
+
549
+ // Sort: providers we support first (by coverage asc), then unsupported
550
+ comparisons.sort((a, b) => {
551
+ if (a.weHaveProvider !== b.weHaveProvider)
552
+ return a.weHaveProvider ? -1 : 1;
553
+ return a.coveragePct - b.coveragePct;
554
+ });
555
+
556
+ // Filter if --missing-providers
557
+ const toShow = missingProvidersOnly
558
+ ? comparisons.filter((c) => !c.weHaveProvider)
559
+ : comparisons;
560
+
561
+ // Print to console
562
+ printConsole(toShow);
563
+
564
+ // Write markdown if requested
565
+ if (outputFile) {
566
+ const md = formatMarkdown(toShow);
567
+ const outPath = path.resolve(process.cwd(), outputFile);
568
+ fs.writeFileSync(outPath, md, "utf-8");
569
+ console.log(`📄 Written to ${outPath}`);
570
+ }
571
+ }
572
+
573
+ main().catch((e) => {
574
+ console.error(e);
575
+ process.exit(1);
576
+ });