@tyvm/knowhow 0.0.105 → 0.0.107
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.
- package/CONFIG.md +8 -5
- package/package.json +3 -2
- package/scripts/check-model-pricing.ts +509 -0
- package/scripts/compare-openrouter-coverage.ts +576 -0
- package/src/agents/base/base.ts +169 -5
- package/src/agents/tools/execCommand.ts +4 -0
- package/src/agents/tools/executeScript/definition.ts +1 -1
- package/src/agents/tools/index.ts +0 -1
- package/src/agents/tools/list.ts +3 -43
- package/src/agents/tools/writeFile.ts +1 -1
- package/src/auth/browserLogin.ts +9 -4
- package/src/chat/modules/RemoteSyncModule.ts +3 -0
- package/src/cli.ts +31 -1
- package/src/clients/anthropic.ts +8 -2
- package/src/clients/cerebras.ts +10 -0
- package/src/clients/contextLimits.ts +7 -2
- package/src/clients/copilot.ts +23 -0
- package/src/clients/deepseek.ts +16 -0
- package/src/clients/fireworks.ts +15 -0
- package/src/clients/gemini.ts +59 -4
- package/src/clients/github.ts +16 -0
- package/src/clients/groq.ts +15 -0
- package/src/clients/http.ts +194 -6
- package/src/clients/index.ts +116 -4
- package/src/clients/llama.ts +16 -0
- package/src/clients/mistral.ts +16 -0
- package/src/clients/nvidia.ts +16 -0
- package/src/clients/openai.ts +53 -12
- package/src/clients/openrouter.ts +17 -0
- package/src/clients/pricing/anthropic.ts +105 -78
- package/src/clients/pricing/cerebras.ts +11 -0
- package/src/clients/pricing/copilot.ts +60 -0
- package/src/clients/pricing/deepseek.ts +15 -0
- package/src/clients/pricing/fireworks.ts +32 -0
- package/src/clients/pricing/github.ts +69 -0
- package/src/clients/pricing/google.ts +245 -206
- package/src/clients/pricing/groq.ts +56 -0
- package/src/clients/pricing/index.ts +42 -5
- package/src/clients/pricing/llama.ts +18 -0
- package/src/clients/pricing/mistral.ts +34 -0
- package/src/clients/pricing/models.ts +7 -236
- package/src/clients/pricing/nvidia.ts +102 -0
- package/src/clients/pricing/openai.ts +348 -171
- package/src/clients/pricing/openrouter.ts +36 -0
- package/src/clients/pricing/types.ts +83 -2
- package/src/clients/pricing/xai.ts +121 -65
- package/src/clients/types.ts +28 -1
- package/src/clients/xai.ts +161 -1
- package/src/fileSync.ts +8 -2
- package/src/login.ts +11 -3
- package/src/services/AgentSyncFs.ts +36 -12
- package/src/services/KnowhowClient.ts +11 -0
- package/src/services/LazyToolsService.ts +6 -0
- package/src/services/S3.ts +0 -7
- package/src/services/modules/index.ts +11 -2
- package/src/types.ts +56 -279
- package/src/worker.ts +174 -0
- package/tests/clients/AIClient.test.ts +1 -1
- package/tests/clients/anthropic.test.ts +202 -0
- package/tests/clients/pricing.test.ts +37 -0
- package/tests/manual/clients/completions.json +838 -226
- package/tests/manual/clients/completions.test.ts +46 -31
- package/ts_build/package.json +3 -2
- package/ts_build/src/agents/base/base.d.ts +18 -1
- package/ts_build/src/agents/base/base.js +111 -4
- package/ts_build/src/agents/base/base.js.map +1 -1
- package/ts_build/src/agents/tools/execCommand.js +3 -0
- package/ts_build/src/agents/tools/execCommand.js.map +1 -1
- package/ts_build/src/agents/tools/executeScript/definition.js +1 -1
- package/ts_build/src/agents/tools/executeScript/definition.js.map +1 -1
- package/ts_build/src/agents/tools/index.d.ts +0 -1
- package/ts_build/src/agents/tools/index.js +0 -1
- package/ts_build/src/agents/tools/index.js.map +1 -1
- package/ts_build/src/agents/tools/list.js +3 -38
- package/ts_build/src/agents/tools/list.js.map +1 -1
- package/ts_build/src/agents/tools/visionTool.d.ts +1 -1
- package/ts_build/src/agents/tools/writeFile.js +1 -1
- package/ts_build/src/agents/tools/writeFile.js.map +1 -1
- package/ts_build/src/ai.d.ts +1 -1
- package/ts_build/src/auth/browserLogin.d.ts +2 -1
- package/ts_build/src/auth/browserLogin.js +10 -3
- package/ts_build/src/auth/browserLogin.js.map +1 -1
- package/ts_build/src/chat/modules/RemoteSyncModule.js +1 -0
- package/ts_build/src/chat/modules/RemoteSyncModule.js.map +1 -1
- package/ts_build/src/cli.js +19 -0
- package/ts_build/src/cli.js.map +1 -1
- package/ts_build/src/clients/anthropic.d.ts +1 -82
- package/ts_build/src/clients/anthropic.js +8 -2
- package/ts_build/src/clients/anthropic.js.map +1 -1
- package/ts_build/src/clients/cerebras.d.ts +4 -0
- package/ts_build/src/clients/cerebras.js +14 -0
- package/ts_build/src/clients/cerebras.js.map +1 -0
- package/ts_build/src/clients/contextLimits.js +7 -2
- package/ts_build/src/clients/contextLimits.js.map +1 -1
- package/ts_build/src/clients/copilot.d.ts +4 -0
- package/ts_build/src/clients/copilot.js +15 -0
- package/ts_build/src/clients/copilot.js.map +1 -0
- package/ts_build/src/clients/deepseek.d.ts +4 -0
- package/ts_build/src/clients/deepseek.js +15 -0
- package/ts_build/src/clients/deepseek.js.map +1 -0
- package/ts_build/src/clients/fireworks.d.ts +4 -0
- package/ts_build/src/clients/fireworks.js +15 -0
- package/ts_build/src/clients/fireworks.js.map +1 -0
- package/ts_build/src/clients/gemini.d.ts +1 -0
- package/ts_build/src/clients/gemini.js +38 -2
- package/ts_build/src/clients/gemini.js.map +1 -1
- package/ts_build/src/clients/github.d.ts +4 -0
- package/ts_build/src/clients/github.js +15 -0
- package/ts_build/src/clients/github.js.map +1 -0
- package/ts_build/src/clients/groq.d.ts +4 -0
- package/ts_build/src/clients/groq.js +15 -0
- package/ts_build/src/clients/groq.js.map +1 -0
- package/ts_build/src/clients/http.d.ts +22 -1
- package/ts_build/src/clients/http.js +135 -7
- package/ts_build/src/clients/http.js.map +1 -1
- package/ts_build/src/clients/index.d.ts +14 -0
- package/ts_build/src/clients/index.js +94 -4
- package/ts_build/src/clients/index.js.map +1 -1
- package/ts_build/src/clients/llama.d.ts +4 -0
- package/ts_build/src/clients/llama.js +15 -0
- package/ts_build/src/clients/llama.js.map +1 -0
- package/ts_build/src/clients/mistral.d.ts +4 -0
- package/ts_build/src/clients/mistral.js +15 -0
- package/ts_build/src/clients/mistral.js.map +1 -0
- package/ts_build/src/clients/nvidia.d.ts +4 -0
- package/ts_build/src/clients/nvidia.js +15 -0
- package/ts_build/src/clients/nvidia.js.map +1 -0
- package/ts_build/src/clients/openai.d.ts +4 -206
- package/ts_build/src/clients/openai.js +38 -10
- package/ts_build/src/clients/openai.js.map +1 -1
- package/ts_build/src/clients/openrouter.d.ts +4 -0
- package/ts_build/src/clients/openrouter.js +15 -0
- package/ts_build/src/clients/openrouter.js.map +1 -0
- package/ts_build/src/clients/pricing/anthropic.d.ts +26 -78
- package/ts_build/src/clients/pricing/anthropic.js +75 -78
- package/ts_build/src/clients/pricing/anthropic.js.map +1 -1
- package/ts_build/src/clients/pricing/cerebras.d.ts +4 -0
- package/ts_build/src/clients/pricing/cerebras.js +11 -0
- package/ts_build/src/clients/pricing/cerebras.js.map +1 -0
- package/ts_build/src/clients/pricing/copilot.d.ts +5 -0
- package/ts_build/src/clients/pricing/copilot.js +35 -0
- package/ts_build/src/clients/pricing/copilot.js.map +1 -0
- package/ts_build/src/clients/pricing/deepseek.d.ts +5 -0
- package/ts_build/src/clients/pricing/deepseek.js +10 -0
- package/ts_build/src/clients/pricing/deepseek.js.map +1 -0
- package/ts_build/src/clients/pricing/fireworks.d.ts +5 -0
- package/ts_build/src/clients/pricing/fireworks.js +21 -0
- package/ts_build/src/clients/pricing/fireworks.js.map +1 -0
- package/ts_build/src/clients/pricing/github.d.ts +4 -0
- package/ts_build/src/clients/pricing/github.js +58 -0
- package/ts_build/src/clients/pricing/github.js.map +1 -0
- package/ts_build/src/clients/pricing/google.d.ts +59 -6
- package/ts_build/src/clients/pricing/google.js +214 -167
- package/ts_build/src/clients/pricing/google.js.map +1 -1
- package/ts_build/src/clients/pricing/groq.d.ts +5 -0
- package/ts_build/src/clients/pricing/groq.js +41 -0
- package/ts_build/src/clients/pricing/groq.js.map +1 -0
- package/ts_build/src/clients/pricing/index.d.ts +16 -5
- package/ts_build/src/clients/pricing/index.js +62 -7
- package/ts_build/src/clients/pricing/index.js.map +1 -1
- package/ts_build/src/clients/pricing/llama.d.ts +4 -0
- package/ts_build/src/clients/pricing/llama.js +14 -0
- package/ts_build/src/clients/pricing/llama.js.map +1 -0
- package/ts_build/src/clients/pricing/mistral.d.ts +5 -0
- package/ts_build/src/clients/pricing/mistral.js +23 -0
- package/ts_build/src/clients/pricing/mistral.js.map +1 -0
- package/ts_build/src/clients/pricing/models.d.ts +5 -4
- package/ts_build/src/clients/pricing/models.js +8 -162
- package/ts_build/src/clients/pricing/models.js.map +1 -1
- package/ts_build/src/clients/pricing/nvidia.d.ts +8 -0
- package/ts_build/src/clients/pricing/nvidia.js +96 -0
- package/ts_build/src/clients/pricing/nvidia.js.map +1 -0
- package/ts_build/src/clients/pricing/openai.d.ts +86 -197
- package/ts_build/src/clients/pricing/openai.js +295 -168
- package/ts_build/src/clients/pricing/openai.js.map +1 -1
- package/ts_build/src/clients/pricing/openrouter.d.ts +4 -0
- package/ts_build/src/clients/pricing/openrouter.js +29 -0
- package/ts_build/src/clients/pricing/openrouter.js.map +1 -0
- package/ts_build/src/clients/pricing/types.d.ts +27 -2
- package/ts_build/src/clients/pricing/types.js +46 -0
- package/ts_build/src/clients/pricing/types.js.map +1 -1
- package/ts_build/src/clients/pricing/xai.d.ts +37 -57
- package/ts_build/src/clients/pricing/xai.js +92 -59
- package/ts_build/src/clients/pricing/xai.js.map +1 -1
- package/ts_build/src/clients/types.d.ts +12 -1
- package/ts_build/src/clients/xai.d.ts +2 -62
- package/ts_build/src/clients/xai.js +132 -1
- package/ts_build/src/clients/xai.js.map +1 -1
- package/ts_build/src/fileSync.js +7 -2
- package/ts_build/src/fileSync.js.map +1 -1
- package/ts_build/src/login.js +8 -2
- package/ts_build/src/login.js.map +1 -1
- package/ts_build/src/services/AgentSyncFs.js +1 -0
- package/ts_build/src/services/AgentSyncFs.js.map +1 -1
- package/ts_build/src/services/KnowhowClient.d.ts +1 -0
- package/ts_build/src/services/KnowhowClient.js +7 -0
- package/ts_build/src/services/KnowhowClient.js.map +1 -1
- package/ts_build/src/services/LazyToolsService.d.ts +1 -0
- package/ts_build/src/services/LazyToolsService.js +3 -0
- package/ts_build/src/services/LazyToolsService.js.map +1 -1
- package/ts_build/src/services/S3.js +0 -7
- package/ts_build/src/services/S3.js.map +1 -1
- package/ts_build/src/services/modules/index.js +41 -1
- package/ts_build/src/services/modules/index.js.map +1 -1
- package/ts_build/src/types.d.ts +163 -124
- package/ts_build/src/types.js +33 -213
- package/ts_build/src/types.js.map +1 -1
- package/ts_build/src/worker.d.ts +4 -0
- package/ts_build/src/worker.js +140 -0
- package/ts_build/src/worker.js.map +1 -1
- package/ts_build/tests/clients/AIClient.test.js +1 -1
- package/ts_build/tests/clients/AIClient.test.js.map +1 -1
- package/ts_build/tests/clients/anthropic.test.d.ts +1 -0
- package/ts_build/tests/clients/anthropic.test.js +159 -0
- package/ts_build/tests/clients/anthropic.test.js.map +1 -0
- package/ts_build/tests/clients/pricing.test.js +21 -0
- package/ts_build/tests/clients/pricing.test.js.map +1 -1
- package/ts_build/tests/manual/clients/completions.test.js +27 -24
- package/ts_build/tests/manual/clients/completions.test.js.map +1 -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
|
+
});
|