@tyvm/knowhow 0.0.105 → 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 (209) 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 +116 -4
  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 +42 -5
  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 +7 -236
  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 +83 -2
  45. package/src/clients/pricing/xai.ts +121 -65
  46. package/src/clients/types.ts +4 -0
  47. package/src/clients/xai.ts +150 -0
  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/modules/index.ts +11 -2
  55. package/src/types.ts +56 -279
  56. package/src/worker.ts +174 -0
  57. package/tests/clients/pricing.test.ts +37 -0
  58. package/tests/manual/clients/completions.json +838 -226
  59. package/tests/manual/clients/completions.test.ts +46 -31
  60. package/ts_build/package.json +3 -2
  61. package/ts_build/src/agents/base/base.d.ts +17 -1
  62. package/ts_build/src/agents/base/base.js +82 -1
  63. package/ts_build/src/agents/base/base.js.map +1 -1
  64. package/ts_build/src/agents/tools/execCommand.js +3 -0
  65. package/ts_build/src/agents/tools/execCommand.js.map +1 -1
  66. package/ts_build/src/agents/tools/executeScript/definition.js +1 -1
  67. package/ts_build/src/agents/tools/executeScript/definition.js.map +1 -1
  68. package/ts_build/src/agents/tools/index.d.ts +0 -1
  69. package/ts_build/src/agents/tools/index.js +0 -1
  70. package/ts_build/src/agents/tools/index.js.map +1 -1
  71. package/ts_build/src/agents/tools/list.js +3 -38
  72. package/ts_build/src/agents/tools/list.js.map +1 -1
  73. package/ts_build/src/agents/tools/visionTool.d.ts +1 -1
  74. package/ts_build/src/agents/tools/writeFile.js +1 -1
  75. package/ts_build/src/agents/tools/writeFile.js.map +1 -1
  76. package/ts_build/src/ai.d.ts +1 -1
  77. package/ts_build/src/auth/browserLogin.d.ts +2 -1
  78. package/ts_build/src/auth/browserLogin.js +10 -3
  79. package/ts_build/src/auth/browserLogin.js.map +1 -1
  80. package/ts_build/src/chat/modules/RemoteSyncModule.js +1 -0
  81. package/ts_build/src/chat/modules/RemoteSyncModule.js.map +1 -1
  82. package/ts_build/src/cli.js +19 -0
  83. package/ts_build/src/cli.js.map +1 -1
  84. package/ts_build/src/clients/anthropic.d.ts +1 -82
  85. package/ts_build/src/clients/cerebras.d.ts +4 -0
  86. package/ts_build/src/clients/cerebras.js +14 -0
  87. package/ts_build/src/clients/cerebras.js.map +1 -0
  88. package/ts_build/src/clients/contextLimits.js +7 -2
  89. package/ts_build/src/clients/contextLimits.js.map +1 -1
  90. package/ts_build/src/clients/copilot.d.ts +4 -0
  91. package/ts_build/src/clients/copilot.js +15 -0
  92. package/ts_build/src/clients/copilot.js.map +1 -0
  93. package/ts_build/src/clients/deepseek.d.ts +4 -0
  94. package/ts_build/src/clients/deepseek.js +15 -0
  95. package/ts_build/src/clients/deepseek.js.map +1 -0
  96. package/ts_build/src/clients/fireworks.d.ts +4 -0
  97. package/ts_build/src/clients/fireworks.js +15 -0
  98. package/ts_build/src/clients/fireworks.js.map +1 -0
  99. package/ts_build/src/clients/gemini.d.ts +1 -0
  100. package/ts_build/src/clients/gemini.js +28 -1
  101. package/ts_build/src/clients/gemini.js.map +1 -1
  102. package/ts_build/src/clients/github.d.ts +4 -0
  103. package/ts_build/src/clients/github.js +15 -0
  104. package/ts_build/src/clients/github.js.map +1 -0
  105. package/ts_build/src/clients/groq.d.ts +4 -0
  106. package/ts_build/src/clients/groq.js +15 -0
  107. package/ts_build/src/clients/groq.js.map +1 -0
  108. package/ts_build/src/clients/http.d.ts +22 -1
  109. package/ts_build/src/clients/http.js +132 -7
  110. package/ts_build/src/clients/http.js.map +1 -1
  111. package/ts_build/src/clients/index.d.ts +14 -0
  112. package/ts_build/src/clients/index.js +94 -4
  113. package/ts_build/src/clients/index.js.map +1 -1
  114. package/ts_build/src/clients/llama.d.ts +4 -0
  115. package/ts_build/src/clients/llama.js +15 -0
  116. package/ts_build/src/clients/llama.js.map +1 -0
  117. package/ts_build/src/clients/mistral.d.ts +4 -0
  118. package/ts_build/src/clients/mistral.js +15 -0
  119. package/ts_build/src/clients/mistral.js.map +1 -0
  120. package/ts_build/src/clients/nvidia.d.ts +4 -0
  121. package/ts_build/src/clients/nvidia.js +15 -0
  122. package/ts_build/src/clients/nvidia.js.map +1 -0
  123. package/ts_build/src/clients/openai.d.ts +4 -206
  124. package/ts_build/src/clients/openai.js +27 -9
  125. package/ts_build/src/clients/openai.js.map +1 -1
  126. package/ts_build/src/clients/openrouter.d.ts +4 -0
  127. package/ts_build/src/clients/openrouter.js +15 -0
  128. package/ts_build/src/clients/openrouter.js.map +1 -0
  129. package/ts_build/src/clients/pricing/anthropic.d.ts +26 -78
  130. package/ts_build/src/clients/pricing/anthropic.js +75 -78
  131. package/ts_build/src/clients/pricing/anthropic.js.map +1 -1
  132. package/ts_build/src/clients/pricing/cerebras.d.ts +4 -0
  133. package/ts_build/src/clients/pricing/cerebras.js +11 -0
  134. package/ts_build/src/clients/pricing/cerebras.js.map +1 -0
  135. package/ts_build/src/clients/pricing/copilot.d.ts +5 -0
  136. package/ts_build/src/clients/pricing/copilot.js +35 -0
  137. package/ts_build/src/clients/pricing/copilot.js.map +1 -0
  138. package/ts_build/src/clients/pricing/deepseek.d.ts +5 -0
  139. package/ts_build/src/clients/pricing/deepseek.js +10 -0
  140. package/ts_build/src/clients/pricing/deepseek.js.map +1 -0
  141. package/ts_build/src/clients/pricing/fireworks.d.ts +5 -0
  142. package/ts_build/src/clients/pricing/fireworks.js +21 -0
  143. package/ts_build/src/clients/pricing/fireworks.js.map +1 -0
  144. package/ts_build/src/clients/pricing/github.d.ts +4 -0
  145. package/ts_build/src/clients/pricing/github.js +58 -0
  146. package/ts_build/src/clients/pricing/github.js.map +1 -0
  147. package/ts_build/src/clients/pricing/google.d.ts +59 -6
  148. package/ts_build/src/clients/pricing/google.js +214 -167
  149. package/ts_build/src/clients/pricing/google.js.map +1 -1
  150. package/ts_build/src/clients/pricing/groq.d.ts +5 -0
  151. package/ts_build/src/clients/pricing/groq.js +41 -0
  152. package/ts_build/src/clients/pricing/groq.js.map +1 -0
  153. package/ts_build/src/clients/pricing/index.d.ts +16 -5
  154. package/ts_build/src/clients/pricing/index.js +62 -7
  155. package/ts_build/src/clients/pricing/index.js.map +1 -1
  156. package/ts_build/src/clients/pricing/llama.d.ts +4 -0
  157. package/ts_build/src/clients/pricing/llama.js +14 -0
  158. package/ts_build/src/clients/pricing/llama.js.map +1 -0
  159. package/ts_build/src/clients/pricing/mistral.d.ts +5 -0
  160. package/ts_build/src/clients/pricing/mistral.js +23 -0
  161. package/ts_build/src/clients/pricing/mistral.js.map +1 -0
  162. package/ts_build/src/clients/pricing/models.d.ts +5 -4
  163. package/ts_build/src/clients/pricing/models.js +8 -162
  164. package/ts_build/src/clients/pricing/models.js.map +1 -1
  165. package/ts_build/src/clients/pricing/nvidia.d.ts +8 -0
  166. package/ts_build/src/clients/pricing/nvidia.js +96 -0
  167. package/ts_build/src/clients/pricing/nvidia.js.map +1 -0
  168. package/ts_build/src/clients/pricing/openai.d.ts +86 -197
  169. package/ts_build/src/clients/pricing/openai.js +294 -168
  170. package/ts_build/src/clients/pricing/openai.js.map +1 -1
  171. package/ts_build/src/clients/pricing/openrouter.d.ts +4 -0
  172. package/ts_build/src/clients/pricing/openrouter.js +29 -0
  173. package/ts_build/src/clients/pricing/openrouter.js.map +1 -0
  174. package/ts_build/src/clients/pricing/types.d.ts +27 -2
  175. package/ts_build/src/clients/pricing/types.js +46 -0
  176. package/ts_build/src/clients/pricing/types.js.map +1 -1
  177. package/ts_build/src/clients/pricing/xai.d.ts +37 -57
  178. package/ts_build/src/clients/pricing/xai.js +92 -59
  179. package/ts_build/src/clients/pricing/xai.js.map +1 -1
  180. package/ts_build/src/clients/types.d.ts +1 -0
  181. package/ts_build/src/clients/xai.d.ts +2 -62
  182. package/ts_build/src/clients/xai.js +121 -0
  183. package/ts_build/src/clients/xai.js.map +1 -1
  184. package/ts_build/src/fileSync.js +7 -2
  185. package/ts_build/src/fileSync.js.map +1 -1
  186. package/ts_build/src/login.js +8 -2
  187. package/ts_build/src/login.js.map +1 -1
  188. package/ts_build/src/services/AgentSyncFs.js +1 -0
  189. package/ts_build/src/services/AgentSyncFs.js.map +1 -1
  190. package/ts_build/src/services/KnowhowClient.d.ts +1 -0
  191. package/ts_build/src/services/KnowhowClient.js +7 -0
  192. package/ts_build/src/services/KnowhowClient.js.map +1 -1
  193. package/ts_build/src/services/LazyToolsService.d.ts +1 -0
  194. package/ts_build/src/services/LazyToolsService.js +3 -0
  195. package/ts_build/src/services/LazyToolsService.js.map +1 -1
  196. package/ts_build/src/services/S3.js +0 -7
  197. package/ts_build/src/services/S3.js.map +1 -1
  198. package/ts_build/src/services/modules/index.js +41 -1
  199. package/ts_build/src/services/modules/index.js.map +1 -1
  200. package/ts_build/src/types.d.ts +163 -124
  201. package/ts_build/src/types.js +33 -213
  202. package/ts_build/src/types.js.map +1 -1
  203. package/ts_build/src/worker.d.ts +4 -0
  204. package/ts_build/src/worker.js +140 -0
  205. package/ts_build/src/worker.js.map +1 -1
  206. package/ts_build/tests/clients/pricing.test.js +21 -0
  207. package/ts_build/tests/clients/pricing.test.js.map +1 -1
  208. package/ts_build/tests/manual/clients/completions.test.js +27 -24
  209. package/ts_build/tests/manual/clients/completions.test.js.map +1 -1
@@ -6,11 +6,29 @@ import {
6
6
  EmbeddingOptions,
7
7
  EmbeddingResponse,
8
8
  } from "./types";
9
+ import { ModelPricing } from "./pricing/types";
9
10
  import fs from "fs";
10
11
  import path from "path";
11
12
 
13
+ export interface HttpClientOptions {
14
+ headers?: Record<string, string>;
15
+ timeout?: number;
16
+ extra_body?: Record<string, any>;
17
+ }
18
+
12
19
  export class HttpClient implements GenericClient {
13
- constructor(private baseUrl: string, private headers = {}) {}
20
+ /** Timeout in milliseconds for HTTP requests. Default: 30000 (30s). Use 0 to disable. */
21
+ private timeout: number;
22
+ private headers: Record<string, string>;
23
+ private extra_body: Record<string, any>;
24
+ /** Optional pricing table: model id → per-million-token prices */
25
+ private pricingMap: Record<string, ModelPricing> = {};
26
+
27
+ constructor(private baseUrl: string, options: HttpClientOptions = {}) {
28
+ this.headers = options.headers ?? {};
29
+ this.timeout = options.timeout ?? 30000;
30
+ this.extra_body = options.extra_body ?? {};
31
+ }
14
32
 
15
33
  private async withRetry<T>(fn: () => Promise<T>, retries = 3): Promise<T> {
16
34
  let lastError: any;
@@ -64,6 +82,52 @@ export class HttpClient implements GenericClient {
64
82
  this.setJwt(key);
65
83
  }
66
84
 
85
+ /**
86
+ * Supply a pricing map so that createChatCompletion / createEmbedding can
87
+ * calculate a local usd_cost from usage tokens when the provider does not
88
+ * return a cost field itself.
89
+ */
90
+ setPrices(pricingMap: Record<string, ModelPricing>) {
91
+ this.pricingMap = pricingMap;
92
+ }
93
+
94
+ /**
95
+ * Calculate USD cost for a completion/embedding call from token usage.
96
+ * Returns undefined if no pricing entry exists for the model.
97
+ */
98
+ calculateCost(
99
+ model: string,
100
+ usage: { prompt_tokens?: number; completion_tokens?: number; prompt_tokens_details?: { cached_tokens?: number } } | undefined
101
+ ): number | undefined {
102
+ if (!usage) return undefined;
103
+ const pricing = this.pricingMap[model];
104
+ if (!pricing) return undefined;
105
+
106
+ const cachedInputTokens =
107
+ usage.prompt_tokens_details?.cached_tokens ?? 0;
108
+ const inputTokens = usage.prompt_tokens ?? 0;
109
+ const outputTokens = usage.completion_tokens ?? 0;
110
+
111
+ const cachedInputCost = (cachedInputTokens * (pricing.cache_hit ?? pricing.cached_input ?? 0)) / 1e6;
112
+ const inputCost = ((inputTokens - cachedInputTokens) * (pricing.input ?? 0)) / 1e6;
113
+ const outputCost = (outputTokens * (pricing.output ?? 0)) / 1e6;
114
+
115
+ return cachedInputCost + inputCost + outputCost;
116
+ }
117
+
118
+ /**
119
+ * Apply extra options (timeout, headers, extra_body) after construction.
120
+ * Used by AIClient.resolveClient to honour per-provider config overrides
121
+ * even when the client is created via a known clientClass (e.g. nvidia, groq).
122
+ */
123
+ setOptions(options: Omit<HttpClientOptions, "headers"> & { headers?: Record<string, string> }) {
124
+ if (options.timeout !== undefined) this.timeout = options.timeout;
125
+ if (options.extra_body !== undefined) this.extra_body = options.extra_body;
126
+ if (options.headers) {
127
+ this.headers = { ...this.headers, ...options.headers };
128
+ }
129
+ }
130
+
67
131
  loadJwtFile(filePath: string) {
68
132
  try {
69
133
  const jwtFile = path.join(process.cwd(), filePath);
@@ -85,7 +149,8 @@ export class HttpClient implements GenericClient {
85
149
  ...options,
86
150
  model: options.model,
87
151
  messages: options.messages,
88
- max_tokens: options.max_tokens || 3000,
152
+ max_tokens: options.max_tokens || 4000,
153
+ ...this.extra_body,
89
154
 
90
155
  ...(options.tools && {
91
156
  tools: options.tools,
@@ -96,7 +161,7 @@ export class HttpClient implements GenericClient {
96
161
  const response = await http.post(
97
162
  `${this.baseUrl}/v1/chat/completions`,
98
163
  body,
99
- { headers: this.headers as Record<string, string> }
164
+ { headers: this.headers as Record<string, string>, timeout: this.timeout }
100
165
  );
101
166
 
102
167
  const data = response.data;
@@ -116,7 +181,125 @@ export class HttpClient implements GenericClient {
116
181
  })),
117
182
  model: data.model,
118
183
  usage: data.usage,
119
- usd_cost: data.usd_cost,
184
+ usd_cost: data.usd_cost ?? this.calculateCost(options.model, data.usage),
185
+ };
186
+ });
187
+ }
188
+
189
+ /**
190
+ * Creates a completion using the Responses API (/v1/responses).
191
+ * Compatible with providers that implement the OpenAI Responses API spec
192
+ * (e.g. xAI at https://api.x.ai/v1/responses).
193
+ */
194
+ async createResponse(
195
+ options: CompletionOptions,
196
+ store = false
197
+ ): Promise<CompletionResponse> {
198
+ return this.withRetry(async () => {
199
+ // Extract system messages as instructions
200
+ const systemMessages = options.messages.filter((m) => m.role === "system");
201
+ const nonSystemMessages = options.messages.filter((m) => m.role !== "system");
202
+ const instructions = systemMessages
203
+ .map((m) => (typeof m.content === "string" ? m.content : ""))
204
+ .join("\n")
205
+ .trim() || undefined;
206
+
207
+ // Convert messages to Responses API input format
208
+ const input: any[] = nonSystemMessages.map((msg) => {
209
+ if (msg.role === "tool") {
210
+ return {
211
+ type: "function_call_output",
212
+ call_id: msg.tool_call_id,
213
+ output: typeof msg.content === "string" ? msg.content : JSON.stringify(msg.content),
214
+ };
215
+ }
216
+ if (msg.role === "assistant" && msg.tool_calls?.length) {
217
+ return (msg.tool_calls as any[]).map((tc: any) => ({
218
+ type: "function_call",
219
+ id: tc.id.startsWith("fc") ? tc.id : `fc_${tc.id}`,
220
+ call_id: tc.id,
221
+ name: tc.function.name,
222
+ arguments: tc.function.arguments,
223
+ }));
224
+ }
225
+ return {
226
+ role: msg.role,
227
+ content: typeof msg.content === "string" ? msg.content : JSON.stringify(msg.content),
228
+ };
229
+ }).flat();
230
+
231
+ const tools = options.tools?.map((tool) => ({
232
+ type: "function" as const,
233
+ name: tool.function.name,
234
+ description: tool.function.description,
235
+ parameters: tool.function.parameters as Record<string, unknown>,
236
+ strict: false,
237
+ }));
238
+
239
+ const body = {
240
+ model: options.model,
241
+ input,
242
+ ...(instructions && { instructions }),
243
+ ...(options.max_tokens && { max_output_tokens: options.max_tokens }),
244
+ ...(tools?.length && { tools, tool_choice: "auto" }),
245
+ store,
246
+ ...this.extra_body,
247
+ };
248
+
249
+ const response = await http.post(
250
+ `${this.baseUrl}/v1/responses`,
251
+ body,
252
+ { headers: this.headers as Record<string, string>, timeout: this.timeout }
253
+ );
254
+
255
+ const data = response.data;
256
+
257
+ if (data.error) {
258
+ throw new Error(JSON.stringify(data.error, null, 2));
259
+ }
260
+
261
+ // Map usage from Responses API format to Chat Completions format
262
+ const usage = data.usage
263
+ ? {
264
+ prompt_tokens: data.usage.input_tokens,
265
+ completion_tokens: data.usage.output_tokens,
266
+ total_tokens: data.usage.input_tokens + data.usage.output_tokens,
267
+ }
268
+ : undefined;
269
+
270
+ // Collect text content and tool calls from output items
271
+ let textContent: string | null = null;
272
+ const toolCalls: any[] = [];
273
+
274
+ for (const item of data.output ?? []) {
275
+ if (item.type === "message") {
276
+ for (const part of item.content ?? []) {
277
+ if (part.type === "output_text") {
278
+ textContent = (textContent ?? "") + part.text;
279
+ }
280
+ }
281
+ } else if (item.type === "function_call") {
282
+ toolCalls.push({
283
+ id: item.call_id,
284
+ type: "function",
285
+ function: { name: item.name, arguments: item.arguments },
286
+ });
287
+ }
288
+ }
289
+
290
+ return {
291
+ choices: [
292
+ {
293
+ message: {
294
+ role: "assistant",
295
+ content: textContent,
296
+ ...(toolCalls.length > 0 && { tool_calls: toolCalls }),
297
+ },
298
+ },
299
+ ],
300
+ model: data.model ?? options.model,
301
+ usage,
302
+ usd_cost: data.usd_cost ?? this.calculateCost(options.model, usage),
120
303
  };
121
304
  });
122
305
  }
@@ -129,7 +312,7 @@ export class HttpClient implements GenericClient {
129
312
  model: options.model,
130
313
  input: options.input,
131
314
  },
132
- { headers: this.headers as Record<string, string> }
315
+ { headers: this.headers as Record<string, string>, timeout: this.timeout }
133
316
  );
134
317
 
135
318
  const data = response.data;
@@ -143,7 +326,7 @@ export class HttpClient implements GenericClient {
143
326
  data: data.data,
144
327
  model: options.model,
145
328
  usage: data.usage,
146
- usd_cost: data.usd_cost,
329
+ usd_cost: data.usd_cost ?? this.calculateCost(options.model, data.usage),
147
330
  };
148
331
  });
149
332
  }
@@ -152,6 +335,7 @@ export class HttpClient implements GenericClient {
152
335
  return this.withRetry(async () => {
153
336
  const response = await http.get(`${this.baseUrl}/v1/models?type=${type}`, {
154
337
  headers: this.headers as Record<string, string>,
338
+ timeout: this.timeout,
155
339
  });
156
340
 
157
341
  const data = response.data?.data;
@@ -43,6 +43,16 @@ import type {
43
43
  ModelType,
44
44
  ModelCatalogEntry,
45
45
  } from "./pricing/types";
46
+ import { GenericCerebrasClient } from "./cerebras";
47
+ import { GenericGroqClient } from "./groq";
48
+ import { GenericGitHubModelsClient } from "./github";
49
+ import { GenericNvidiaClient } from "./nvidia";
50
+ import { GenericOpenRouterClient } from "./openrouter";
51
+ import { GenericDeepSeekClient } from "./deepseek";
52
+ import { GenericMistralClient } from "./mistral";
53
+ import { GitHubCopilotClient } from "./copilot";
54
+ import { GenericLlamaClient } from "./llama";
55
+ import { GenericFireworksClient } from "./fireworks";
46
56
  export {
47
57
  OpenAiTextPricing,
48
58
  AnthropicTextPricing,
@@ -75,6 +85,18 @@ const BUILT_IN_PROVIDER_REGISTRY: Record<string, ProviderRegistryEntry> = {
75
85
  anthropic: { clientClass: GenericAnthropicClient },
76
86
  google: { clientClass: GenericGeminiClient },
77
87
  xai: { clientClass: GenericXAIClient },
88
+ cerebras: {
89
+ clientClass: GenericCerebrasClient,
90
+ },
91
+ groq: { clientClass: GenericGroqClient },
92
+ github: { clientClass: GenericGitHubModelsClient },
93
+ nvidia: { clientClass: GenericNvidiaClient },
94
+ openrouter: { clientClass: GenericOpenRouterClient },
95
+ deepseek: { clientClass: GenericDeepSeekClient },
96
+ mistral: { clientClass: GenericMistralClient },
97
+ "github-copilot": { clientClass: GitHubCopilotClient },
98
+ llama: { clientClass: GenericLlamaClient },
99
+ fireworks: { clientClass: GenericFireworksClient },
78
100
  knowhow: {
79
101
  createClient: (entry: ModelProvider) => {
80
102
  const jwt = loadKnowhowJwt();
@@ -94,7 +116,17 @@ const DEFAULT_PROVIDERS: ModelProvider[] = [
94
116
  { provider: "anthropic", envKey: "ANTHROPIC_API_KEY" },
95
117
  { provider: "google", envKey: "GEMINI_API_KEY" },
96
118
  { provider: "xai", envKey: "XAI_API_KEY" },
119
+ { provider: "cerebras", envKey: "CEREBRAS_API_KEY" },
97
120
  { provider: "knowhow" },
121
+ { provider: "groq", envKey: "GROQ_API_KEY" },
122
+ { provider: "github", envKey: "GITHUB_TOKEN" },
123
+ { provider: "nvidia", envKey: "NVIDIA_API_KEY" },
124
+ { provider: "openrouter", envKey: "OPENROUTER_API_KEY" },
125
+ { provider: "deepseek", envKey: "DEEPSEEK_API_KEY" },
126
+ { provider: "mistral", envKey: "MISTRAL_API_KEY" },
127
+ { provider: "github-copilot", envKey: "GITHUB_COPILOT_TOKEN" },
128
+ { provider: "llama", envKey: "LLAMA_API_KEY" },
129
+ { provider: "fireworks", envKey: "FIREWORKS_API_KEY" },
98
130
  ];
99
131
 
100
132
  export class AIClient {
@@ -153,19 +185,45 @@ export class AIClient {
153
185
  // envKey-based auth: env var must be present
154
186
  const envValue = process.env[effectiveEnvKey];
155
187
  if (!envValue) return null;
156
- return new reg.clientClass(envValue);
188
+ const client = new reg.clientClass(envValue);
189
+ // Apply any extra options (timeout, headers, extra_body) from config
190
+ if (client instanceof HttpClient) {
191
+ client.setOptions({
192
+ timeout: entry.timeout,
193
+ headers: entry.headers,
194
+ extra_body: entry.extra_body,
195
+ });
196
+ if (entry.pricing) client.setPrices(entry.pricing);
197
+ }
198
+ return client;
157
199
  }
158
200
 
159
201
  // No envKey, no url — instantiate with no arg (client uses its own defaults)
160
- return new reg.clientClass();
202
+ const client = new reg.clientClass();
203
+ // Apply any extra options (timeout, headers, extra_body) from config
204
+ if (client instanceof HttpClient) {
205
+ client.setOptions({
206
+ timeout: entry.timeout,
207
+ headers: entry.headers,
208
+ extra_body: entry.extra_body,
209
+ });
210
+ if (entry.pricing) client.setPrices(entry.pricing);
211
+ }
212
+ return client;
161
213
  }
162
214
 
163
215
  // 3. HTTP provider — requires url, no clientClass in registry
164
216
  if (entry.url) {
165
- const client = new HttpClient(entry.url, entry.headers);
217
+ const client = new HttpClient(entry.url, {
218
+ headers: entry.headers,
219
+ timeout: entry.timeout,
220
+ extra_body: entry.extra_body,
221
+ });
166
222
  if (entry.jwtFile) {
167
223
  client.loadJwtFile(entry.jwtFile);
168
224
  }
225
+ // For custom HTTP providers, use entry.pricing if available
226
+ if (entry.pricing) client.setPrices(entry.pricing);
169
227
  return client;
170
228
  }
171
229
 
@@ -492,6 +550,52 @@ export class AIClient {
492
550
  return undefined;
493
551
  }
494
552
 
553
+ /**
554
+ * Normalize a model ID for fuzzy matching:
555
+ * - lowercase
556
+ * - replace dots with dashes (e.g. "claude-opus-4.7" → "claude-opus-4-7")
557
+ * - strip variant suffixes like ":thinking", ":free"
558
+ * - strip trailing date suffixes like "-20250514"
559
+ * - strip trailing "-beta", "-preview", "-latest"
560
+ */
561
+ private static normalizeModelId(id: string): string {
562
+ return id
563
+ .toLowerCase()
564
+ .replace(/\./g, "-")
565
+ .replace(/:[^:]+$/, "")
566
+ .replace(/-\d{8}$/, "")
567
+ .replace(/-(beta|preview|latest|exp|rc\d*)$/i, "");
568
+ }
569
+
570
+ /**
571
+ * Fuzzy model lookup: given a model name (possibly without date suffix,
572
+ * with dots instead of dashes, etc.), find the best matching registered model.
573
+ *
574
+ * Example: "claude-3.7-sonnet" matches "claude-3-7-sonnet-20250219"
575
+ * "gpt-4.1" matches "gpt-4.1" exactly
576
+ *
577
+ * @param modelQuery - the model name to search for (can be partial/normalized)
578
+ * @param provider - optional provider to restrict search to
579
+ */
580
+ findModelFuzzy(modelQuery: string, provider?: string): { provider: string; model: string } | undefined {
581
+ const queryNorm = AIClient.normalizeModelId(modelQuery);
582
+ const providers = provider
583
+ ? [provider]
584
+ : Object.keys(this.clientModels);
585
+
586
+ for (const p of providers) {
587
+ const models = (this.clientModels[p] as string[]) ?? [];
588
+ for (const m of models) {
589
+ const mNorm = AIClient.normalizeModelId(m);
590
+ // Exact normalized match, OR our model is a dated variant of the query
591
+ if (mNorm === queryNorm || mNorm.startsWith(queryNorm + "-")) {
592
+ return { provider: p, model: m };
593
+ }
594
+ }
595
+ }
596
+ return undefined;
597
+ }
598
+
495
599
  // detects these formats:
496
600
  // "openai", "gpt-5"
497
601
  // "knowhow", "openai/gpt-5"
@@ -822,7 +926,6 @@ export class AIClient {
822
926
  id,
823
927
  provider,
824
928
  type,
825
- displayName: id,
826
929
  pricing: p,
827
930
  });
828
931
  }
@@ -871,3 +974,12 @@ export * from "./gemini";
871
974
  export * from "./contextLimits";
872
975
  export * from "./xai";
873
976
  export * from "./knowhowMcp";
977
+ export * from "./groq";
978
+ export * from "./github";
979
+ export * from "./nvidia";
980
+ export * from "./openrouter";
981
+ export * from "./deepseek";
982
+ export * from "./mistral";
983
+ export * from "./llama";
984
+ export * from "./copilot";
985
+ export * from "./fireworks";
@@ -0,0 +1,16 @@
1
+ import { HttpClient } from "./http";
2
+ import { LlamaTextPricing } from "./pricing/llama";
3
+
4
+ /**
5
+ * Meta Llama API client — OpenAI-compatible API
6
+ * https://llama.developer.meta.com/docs/
7
+ * Direct from Meta: free Llama 3.x, Llama 4, and Cerebras/Groq-hosted variants.
8
+ * Set env var LLAMA_API_KEY to enable.
9
+ */
10
+ export class GenericLlamaClient extends HttpClient {
11
+ constructor(apiKey = process.env.LLAMA_API_KEY) {
12
+ super("https://api.llama.com/compat");
13
+ if (apiKey) this.setJwt(apiKey);
14
+ this.setPrices(LlamaTextPricing);
15
+ }
16
+ }
@@ -0,0 +1,16 @@
1
+ import { HttpClient } from "./http";
2
+ import { MistralTextPricing } from "./pricing/mistral";
3
+
4
+ /**
5
+ * Mistral AI client — OpenAI-compatible API
6
+ * https://docs.mistral.ai/api/
7
+ * Top European AI lab with Mistral Large, Codestral, and free Devstral coding model.
8
+ * Set env var MISTRAL_API_KEY to enable.
9
+ */
10
+ export class GenericMistralClient extends HttpClient {
11
+ constructor(apiKey = process.env.MISTRAL_API_KEY) {
12
+ super("https://api.mistral.ai");
13
+ if (apiKey) this.setJwt(apiKey);
14
+ this.setPrices(MistralTextPricing);
15
+ }
16
+ }
@@ -0,0 +1,16 @@
1
+ import { HttpClient } from "./http";
2
+ import { NvidiaTextPricing } from "./pricing/nvidia";
3
+
4
+ /**
5
+ * NVIDIA NIM client — OpenAI-compatible API
6
+ * https://build.nvidia.com/explore/discover
7
+ * 76+ free models including Llama, Mistral, Phi, Flux image generation.
8
+ * Set env var NVIDIA_API_KEY to enable.
9
+ */
10
+ export class GenericNvidiaClient extends HttpClient {
11
+ constructor(apiKey = process.env.NVIDIA_API_KEY) {
12
+ super("https://integrate.api.nvidia.com");
13
+ if (apiKey) this.setJwt(apiKey);
14
+ this.setPrices(NvidiaTextPricing);
15
+ }
16
+ }
@@ -34,12 +34,14 @@ import {
34
34
  EmbeddingModels,
35
35
  Models,
36
36
  OpenAiReasoningModels,
37
+ OpenAiChatModels,
37
38
  OpenAiResponsesOnlyModels,
38
39
  OpenAiImageModels,
39
40
  OpenAiVideoModels,
40
41
  OpenAiTTSModels,
41
42
  OpenAiTranscriptionModels,
42
- OpenAiEmbeddingModels,
43
+ OpenAiEmbeddingModelsList,
44
+ OpenAiRealtimeModels,
43
45
  } from "../types";
44
46
  import { ModelModality } from "./types";
45
47
 
@@ -64,7 +66,11 @@ export class GenericOpenAiClient implements GenericClient {
64
66
  reasoningEffort(
65
67
  messages: CompletionOptions["messages"]
66
68
  ): "low" | "medium" | "high" {
67
- const effortMap = {
69
+ return this.detectReasoningEffort(messages);
70
+ }
71
+
72
+ detectReasoningEffort(messages: CompletionOptions["messages"]): "low" | "medium" | "high" {
73
+ const effortMap: Record<string, "low" | "medium" | "high"> = {
68
74
  ultrathink: "high",
69
75
  "think hard": "high",
70
76
  "reason hard": "high",
@@ -96,6 +102,30 @@ export class GenericOpenAiClient implements GenericClient {
96
102
  return "medium"; // Default to medium if no specific effort is mentioned
97
103
  }
98
104
 
105
+ resolveReasoningEffort(options: CompletionOptions): "low" | "medium" | "high" {
106
+ return options.reasoning_effort ?? this.detectReasoningEffort(options.messages);
107
+ }
108
+
109
+ /**
110
+ * Resolves the reasoning effort for a specific model, clamping to the model's
111
+ * supported levels if `reasoningLevels` is set in its pricing entry.
112
+ * If the requested level is not supported, picks the lowest supported level.
113
+ */
114
+ resolveReasoningEffortForModel(options: CompletionOptions): string {
115
+ const requested = options.reasoning_effort ?? this.detectReasoningEffort(options.messages);
116
+ const pricing = OpenAiTextPricing[options.model];
117
+ const supportedLevels = pricing?.reasoningLevels;
118
+ if (!supportedLevels || supportedLevels.length === 0) {
119
+ return requested;
120
+ }
121
+ // If the requested level is supported, use it
122
+ if (supportedLevels.includes(requested)) {
123
+ return requested;
124
+ }
125
+ // Otherwise use the first (lowest) supported level
126
+ return supportedLevels[0];
127
+ }
128
+
99
129
  async createChatCompletion(
100
130
  options: CompletionOptions
101
131
  ): Promise<CompletionResponse> {
@@ -122,8 +152,8 @@ export class GenericOpenAiClient implements GenericClient {
122
152
  max_tokens: options.max_tokens,
123
153
  ...(OpenAiReasoningModels.includes(options.model) && {
124
154
  max_tokens: undefined,
125
- max_completion_tokens: Math.max(options.max_tokens, 100),
126
- reasoning_effort: this.reasoningEffort(options.messages),
155
+ max_completion_tokens: Math.max(options.max_tokens ?? 0, 16_000),
156
+ reasoning_effort: this.resolveReasoningEffort(options),
127
157
  }),
128
158
 
129
159
  ...(options.tools && {
@@ -254,7 +284,7 @@ export class GenericOpenAiClient implements GenericClient {
254
284
  // Don't limit max_output_tokens for Responses API - codex truncates tool call arguments when limited
255
285
  ...(OpenAiReasoningModels.includes(options.model) && {
256
286
  max_output_tokens: Math.max(options.max_tokens || 0, 16000),
257
- reasoning: { effort: this.reasoningEffort(options.messages) },
287
+ reasoning: { effort: this.resolveReasoningEffortForModel(options) },
258
288
  }),
259
289
  ...(tools?.length && {
260
290
  tools,
@@ -349,14 +379,14 @@ export class GenericOpenAiClient implements GenericClient {
349
379
  ("prompt_tokens_details" in usage &&
350
380
  usage.prompt_tokens_details?.cached_tokens) ||
351
381
  0;
352
- const cachedInputCost = (cachedInputTokens * pricing.cached_input) / 1e6;
382
+ const cachedInputCost = (cachedInputTokens * (pricing.cached_input ?? 0)) / 1e6;
353
383
 
354
384
  const inputTokens = usage.prompt_tokens;
355
- const inputCost = ((inputTokens - cachedInputCost) * pricing.input) / 1e6;
385
+ const inputCost = ((inputTokens - cachedInputTokens) * (pricing.input ?? 0)) / 1e6;
356
386
 
357
387
  const outputTokens =
358
388
  ("completion_tokens" in usage && usage?.completion_tokens) || 0;
359
- const outputCost = (outputTokens * pricing.output) / 1e6;
389
+ const outputCost = (outputTokens * (pricing.output ?? 0)) / 1e6;
360
390
 
361
391
  const total = cachedInputCost + inputCost + outputCost;
362
392
  return total;
@@ -365,8 +395,8 @@ export class GenericOpenAiClient implements GenericClient {
365
395
  async getModels(modality?: ModelModality): Promise<{ id: string }[]> {
366
396
  if (modality) {
367
397
  const map: Partial<Record<ModelModality, string[]>> = {
368
- completion: Object.values(Models.openai),
369
- embedding: OpenAiEmbeddingModels,
398
+ completion: [...new Set([...OpenAiChatModels, ...OpenAiResponsesOnlyModels])],
399
+ embedding: OpenAiEmbeddingModelsList,
370
400
  image: OpenAiImageModels,
371
401
  audio: [...OpenAiTTSModels, ...OpenAiTranscriptionModels],
372
402
  transcription: OpenAiTranscriptionModels,
@@ -406,7 +436,7 @@ export class GenericOpenAiClient implements GenericClient {
406
436
  }
407
437
 
408
438
  const response = await this.client.audio.transcriptions.create({
409
- file: file,
439
+ file,
410
440
  model: options.model || "whisper-1",
411
441
  language: options.language,
412
442
  prompt: options.prompt,
@@ -0,0 +1,17 @@
1
+ import { HttpClient } from "./http";
2
+ import { OpenRouterTextPricing } from "./pricing/openrouter";
3
+
4
+ /**
5
+ * OpenRouter client — OpenAI-compatible API aggregator
6
+ * https://openrouter.ai/docs
7
+ * 39+ free models; append `:free` suffix to a model id for the free variant.
8
+ * One API key gives access to models from many providers.
9
+ * Set env var OPENROUTER_API_KEY to enable.
10
+ */
11
+ export class GenericOpenRouterClient extends HttpClient {
12
+ constructor(apiKey = process.env.OPENROUTER_API_KEY) {
13
+ super("https://openrouter.ai/api");
14
+ if (apiKey) this.setJwt(apiKey);
15
+ this.setPrices(OpenRouterTextPricing);
16
+ }
17
+ }