@tyvm/knowhow 0.0.156 → 0.0.158

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 (155) hide show
  1. package/README.md +17 -1
  2. package/package.json +1 -1
  3. package/src/agents/base/base.ts +176 -39
  4. package/src/auth/browserLogin.ts +2 -13
  5. package/src/auth/environmentAuth.ts +51 -0
  6. package/src/auth/jwtStore.ts +70 -0
  7. package/src/auth/keyAuth.ts +4 -15
  8. package/src/auth/registerIdentity.ts +8 -8
  9. package/src/chat/modules/AgentModule.ts +3 -3
  10. package/src/chat/modules/RemoteSyncModule.ts +45 -17
  11. package/src/cli.ts +2 -0
  12. package/src/clients/anthropic.ts +156 -79
  13. package/src/clients/attribution.ts +53 -0
  14. package/src/clients/http.ts +211 -84
  15. package/src/clients/index.ts +45 -13
  16. package/src/clients/openai.ts +441 -220
  17. package/src/clients/openrouter.ts +10 -3
  18. package/src/clients/pricing/fireworks.ts +32 -2
  19. package/src/clients/pricing/openai.ts +15 -1
  20. package/src/clients/pricing/types.ts +15 -0
  21. package/src/clients/responseMetadata.ts +221 -0
  22. package/src/clients/types.ts +49 -4
  23. package/src/commands/mcp.ts +1 -0
  24. package/src/commands/misc.ts +1 -1
  25. package/src/commands/remote.ts +77 -0
  26. package/src/config.ts +9 -1
  27. package/src/login.ts +54 -42
  28. package/src/remotes.ts +110 -0
  29. package/src/services/KnowhowClient.ts +12 -16
  30. package/src/services/Mcp.ts +94 -7
  31. package/src/services/McpWebsocketTransport.ts +14 -11
  32. package/src/types.ts +12 -0
  33. package/tests/unit/McpWebsocketTransport.test.ts +56 -0
  34. package/tests/unit/auth/jwtStore.test.ts +55 -0
  35. package/tests/unit/auth/registerIdentity.test.ts +8 -2
  36. package/tests/unit/clients/anthropic.responseMetadata.test.ts +81 -0
  37. package/tests/unit/clients/attribution.test.ts +185 -0
  38. package/tests/unit/clients/http.responseMetadata.test.ts +86 -0
  39. package/tests/unit/clients/openai.responseMetadata.test.ts +90 -0
  40. package/tests/unit/clients/responseMetadata.test.ts +123 -0
  41. package/tests/unit/commands/remote.test.ts +79 -0
  42. package/tests/unit/login.test.ts +64 -3
  43. package/tests/unit/services/KnowhowClient.test.ts +2 -2
  44. package/tests/unit/services/Mcp.test.ts +2 -2
  45. package/ts_build/package.json +1 -1
  46. package/ts_build/src/agents/base/base.d.ts +8 -3
  47. package/ts_build/src/agents/base/base.js +112 -26
  48. package/ts_build/src/agents/base/base.js.map +1 -1
  49. package/ts_build/src/auth/browserLogin.js +2 -7
  50. package/ts_build/src/auth/browserLogin.js.map +1 -1
  51. package/ts_build/src/auth/environmentAuth.d.ts +5 -0
  52. package/ts_build/src/auth/environmentAuth.js +48 -0
  53. package/ts_build/src/auth/environmentAuth.js.map +1 -0
  54. package/ts_build/src/auth/jwtStore.d.ts +4 -0
  55. package/ts_build/src/auth/jwtStore.js +65 -0
  56. package/ts_build/src/auth/jwtStore.js.map +1 -0
  57. package/ts_build/src/auth/keyAuth.d.ts +1 -1
  58. package/ts_build/src/auth/keyAuth.js +4 -16
  59. package/ts_build/src/auth/keyAuth.js.map +1 -1
  60. package/ts_build/src/auth/registerIdentity.js +6 -7
  61. package/ts_build/src/auth/registerIdentity.js.map +1 -1
  62. package/ts_build/src/chat/modules/AgentModule.js +3 -3
  63. package/ts_build/src/chat/modules/AgentModule.js.map +1 -1
  64. package/ts_build/src/chat/modules/RemoteSyncModule.d.ts +1 -0
  65. package/ts_build/src/chat/modules/RemoteSyncModule.js +19 -7
  66. package/ts_build/src/chat/modules/RemoteSyncModule.js.map +1 -1
  67. package/ts_build/src/cli.js +2 -0
  68. package/ts_build/src/cli.js.map +1 -1
  69. package/ts_build/src/clients/anthropic.d.ts +1 -0
  70. package/ts_build/src/clients/anthropic.js +83 -38
  71. package/ts_build/src/clients/anthropic.js.map +1 -1
  72. package/ts_build/src/clients/attribution.d.ts +2 -0
  73. package/ts_build/src/clients/attribution.js +32 -0
  74. package/ts_build/src/clients/attribution.js.map +1 -0
  75. package/ts_build/src/clients/http.js +100 -20
  76. package/ts_build/src/clients/http.js.map +1 -1
  77. package/ts_build/src/clients/index.d.ts +2 -1
  78. package/ts_build/src/clients/index.js +24 -6
  79. package/ts_build/src/clients/index.js.map +1 -1
  80. package/ts_build/src/clients/openai.d.ts +2 -1
  81. package/ts_build/src/clients/openai.js +225 -99
  82. package/ts_build/src/clients/openai.js.map +1 -1
  83. package/ts_build/src/clients/openrouter.d.ts +2 -2
  84. package/ts_build/src/clients/openrouter.js +6 -2
  85. package/ts_build/src/clients/openrouter.js.map +1 -1
  86. package/ts_build/src/clients/pricing/fireworks.d.ts +4 -0
  87. package/ts_build/src/clients/pricing/fireworks.js +14 -2
  88. package/ts_build/src/clients/pricing/fireworks.js.map +1 -1
  89. package/ts_build/src/clients/pricing/openai.js +15 -1
  90. package/ts_build/src/clients/pricing/openai.js.map +1 -1
  91. package/ts_build/src/clients/pricing/types.d.ts +6 -0
  92. package/ts_build/src/clients/pricing/types.js.map +1 -1
  93. package/ts_build/src/clients/responseMetadata.d.ts +15 -0
  94. package/ts_build/src/clients/responseMetadata.js +141 -0
  95. package/ts_build/src/clients/responseMetadata.js.map +1 -0
  96. package/ts_build/src/clients/types.d.ts +18 -1
  97. package/ts_build/src/clients/types.js +2 -1
  98. package/ts_build/src/clients/types.js.map +1 -1
  99. package/ts_build/src/commands/mcp.js.map +1 -1
  100. package/ts_build/src/commands/misc.js +1 -1
  101. package/ts_build/src/commands/misc.js.map +1 -1
  102. package/ts_build/src/commands/remote.d.ts +2 -0
  103. package/ts_build/src/commands/remote.js +70 -0
  104. package/ts_build/src/commands/remote.js.map +1 -0
  105. package/ts_build/src/config.js +8 -0
  106. package/ts_build/src/config.js.map +1 -1
  107. package/ts_build/src/login.d.ts +4 -3
  108. package/ts_build/src/login.js +34 -35
  109. package/ts_build/src/login.js.map +1 -1
  110. package/ts_build/src/remotes.d.ts +14 -0
  111. package/ts_build/src/remotes.js +117 -0
  112. package/ts_build/src/remotes.js.map +1 -0
  113. package/ts_build/src/services/KnowhowClient.d.ts +1 -1
  114. package/ts_build/src/services/KnowhowClient.js +12 -14
  115. package/ts_build/src/services/KnowhowClient.js.map +1 -1
  116. package/ts_build/src/services/Mcp.d.ts +1 -1
  117. package/ts_build/src/services/Mcp.js +68 -7
  118. package/ts_build/src/services/Mcp.js.map +1 -1
  119. package/ts_build/src/services/McpWebsocketTransport.d.ts +1 -0
  120. package/ts_build/src/services/McpWebsocketTransport.js +11 -6
  121. package/ts_build/src/services/McpWebsocketTransport.js.map +1 -1
  122. package/ts_build/src/types.d.ts +13 -0
  123. package/ts_build/src/types.js.map +1 -1
  124. package/ts_build/tests/unit/McpWebsocketTransport.test.d.ts +1 -0
  125. package/ts_build/tests/unit/McpWebsocketTransport.test.js +47 -0
  126. package/ts_build/tests/unit/McpWebsocketTransport.test.js.map +1 -0
  127. package/ts_build/tests/unit/auth/jwtStore.test.d.ts +1 -0
  128. package/ts_build/tests/unit/auth/jwtStore.test.js +46 -0
  129. package/ts_build/tests/unit/auth/jwtStore.test.js.map +1 -0
  130. package/ts_build/tests/unit/auth/registerIdentity.test.js +8 -2
  131. package/ts_build/tests/unit/auth/registerIdentity.test.js.map +1 -1
  132. package/ts_build/tests/unit/clients/anthropic.responseMetadata.test.d.ts +1 -0
  133. package/ts_build/tests/unit/clients/anthropic.responseMetadata.test.js +77 -0
  134. package/ts_build/tests/unit/clients/anthropic.responseMetadata.test.js.map +1 -0
  135. package/ts_build/tests/unit/clients/attribution.test.d.ts +1 -0
  136. package/ts_build/tests/unit/clients/attribution.test.js +153 -0
  137. package/ts_build/tests/unit/clients/attribution.test.js.map +1 -0
  138. package/ts_build/tests/unit/clients/http.responseMetadata.test.d.ts +1 -0
  139. package/ts_build/tests/unit/clients/http.responseMetadata.test.js +77 -0
  140. package/ts_build/tests/unit/clients/http.responseMetadata.test.js.map +1 -0
  141. package/ts_build/tests/unit/clients/openai.responseMetadata.test.d.ts +1 -0
  142. package/ts_build/tests/unit/clients/openai.responseMetadata.test.js +86 -0
  143. package/ts_build/tests/unit/clients/openai.responseMetadata.test.js.map +1 -0
  144. package/ts_build/tests/unit/clients/responseMetadata.test.d.ts +1 -0
  145. package/ts_build/tests/unit/clients/responseMetadata.test.js +96 -0
  146. package/ts_build/tests/unit/clients/responseMetadata.test.js.map +1 -0
  147. package/ts_build/tests/unit/commands/remote.test.d.ts +1 -0
  148. package/ts_build/tests/unit/commands/remote.test.js +70 -0
  149. package/ts_build/tests/unit/commands/remote.test.js.map +1 -0
  150. package/ts_build/tests/unit/login.test.js +44 -3
  151. package/ts_build/tests/unit/login.test.js.map +1 -1
  152. package/ts_build/tests/unit/services/KnowhowClient.test.js +2 -2
  153. package/ts_build/tests/unit/services/KnowhowClient.test.js.map +1 -1
  154. package/ts_build/tests/unit/services/Mcp.test.js +2 -2
  155. package/ts_build/tests/unit/services/Mcp.test.js.map +1 -1
@@ -1,4 +1,10 @@
1
- import http, { HTTP_UNAUTHORIZED_HANDLER, RefreshableHeaders } from "../utils/http";
1
+ import http, {
2
+ HTTP_UNAUTHORIZED_HANDLER,
3
+ HttpError,
4
+ RefreshableHeaders,
5
+ } from "../utils/http";
6
+ import { emitResponseMetadata } from "./responseMetadata";
7
+
2
8
  import {
3
9
  GenericClient,
4
10
  CompletionOptions,
@@ -51,14 +57,23 @@ export class HttpClient implements GenericClient {
51
57
  * The default implementation decodes the JWT `exp` claim (< 5 min window).
52
58
  */
53
59
  needsRefresh(): boolean {
54
- const authHeader = (this.headers as Record<string, string>)["Authorization"];
60
+ const authHeader = (this.headers as Record<string, string>)[
61
+ "Authorization"
62
+ ];
55
63
  if (!authHeader) return false;
56
- const token = authHeader.startsWith("Bearer ") ? authHeader.slice(7) : authHeader;
64
+ const token = authHeader.startsWith("Bearer ")
65
+ ? authHeader.slice(7)
66
+ : authHeader;
57
67
  try {
58
68
  const parts = token.split(".");
59
69
  if (parts.length !== 3) return false;
60
- const payload = JSON.parse(Buffer.from(parts[1], "base64url").toString("utf8"));
61
- return typeof payload.exp === "number" && payload.exp * 1000 - Date.now() < 5 * 60 * 1000;
70
+ const payload = JSON.parse(
71
+ Buffer.from(parts[1], "base64url").toString("utf8")
72
+ );
73
+ return (
74
+ typeof payload.exp === "number" &&
75
+ payload.exp * 1000 - Date.now() < 5 * 60 * 1000
76
+ );
62
77
  } catch {
63
78
  return false;
64
79
  }
@@ -81,7 +96,7 @@ export class HttpClient implements GenericClient {
81
96
  Object.defineProperty(this.headers, HTTP_UNAUTHORIZED_HANDLER, {
82
97
  enumerable: false,
83
98
  configurable: true,
84
- get: () => this.refreshToken ? () => this.refreshToken!() : undefined,
99
+ get: () => (this.refreshToken ? () => this.refreshToken!() : undefined),
85
100
  });
86
101
  this.timeout = options.timeout ?? 30000;
87
102
  this.extra_body = options.extra_body ?? {};
@@ -177,7 +192,9 @@ export class HttpClient implements GenericClient {
177
192
  * Returns the pricing entry for a specific model, or all pricing entries if no model is given.
178
193
  * Returns undefined for a specific model if no pricing is known.
179
194
  */
180
- getPricing(model?: string): ModelPricing | Record<string, ModelPricing> | undefined {
195
+ getPricing(
196
+ model?: string
197
+ ): ModelPricing | Record<string, ModelPricing> | undefined {
181
198
  if (model !== undefined) {
182
199
  return this.pricingMap[model];
183
200
  }
@@ -190,19 +207,27 @@ export class HttpClient implements GenericClient {
190
207
  */
191
208
  calculateCost(
192
209
  model: string,
193
- usage: { prompt_tokens?: number; completion_tokens?: number; prompt_tokens_details?: { cached_tokens?: number } } | undefined
210
+ usage:
211
+ | {
212
+ prompt_tokens?: number;
213
+ completion_tokens?: number;
214
+ prompt_tokens_details?: { cached_tokens?: number };
215
+ }
216
+ | undefined
194
217
  ): number | undefined {
195
218
  if (!usage) return undefined;
196
219
  const pricing = this.pricingMap[model];
197
220
  if (!pricing) return undefined;
198
221
 
199
- const cachedInputTokens =
200
- usage.prompt_tokens_details?.cached_tokens ?? 0;
222
+ const cachedInputTokens = usage.prompt_tokens_details?.cached_tokens ?? 0;
201
223
  const inputTokens = usage.prompt_tokens ?? 0;
202
224
  const outputTokens = usage.completion_tokens ?? 0;
203
225
 
204
- const cachedInputCost = (cachedInputTokens * (pricing.cache_hit ?? pricing.cached_input ?? 0)) / 1e6;
205
- const inputCost = ((inputTokens - cachedInputTokens) * (pricing.input ?? 0)) / 1e6;
226
+ const cachedInputCost =
227
+ (cachedInputTokens * (pricing.cache_hit ?? pricing.cached_input ?? 0)) /
228
+ 1e6;
229
+ const inputCost =
230
+ ((inputTokens - cachedInputTokens) * (pricing.input ?? 0)) / 1e6;
206
231
  const outputCost = (outputTokens * (pricing.output ?? 0)) / 1e6;
207
232
 
208
233
  return cachedInputCost + inputCost + outputCost;
@@ -213,12 +238,21 @@ export class HttpClient implements GenericClient {
213
238
  * Used by AIClient.resolveClient to honour per-provider config overrides
214
239
  * even when the client is created via a known clientClass (e.g. nvidia, groq).
215
240
  */
216
- setOptions(options: Omit<HttpClientOptions, "headers"> & { headers?: Record<string, string> }) {
241
+ setOptions(
242
+ options: Omit<HttpClientOptions, "headers"> & {
243
+ headers?: Record<string, string>;
244
+ }
245
+ ) {
217
246
  if (options.timeout !== undefined) this.timeout = options.timeout;
218
247
  if (options.extra_body !== undefined) this.extra_body = options.extra_body;
219
248
  if (options.headers) {
220
249
  // Mutate in-place to preserve the HTTP_UNAUTHORIZED_HANDLER symbol property.
221
- Object.assign(this.headers, options.headers);
250
+ for (const [name, value] of Object.entries(options.headers)) {
251
+ const existingName = Object.keys(this.headers).find(
252
+ (candidate) => candidate.toLowerCase() === name.toLowerCase()
253
+ );
254
+ this.headers[existingName ?? name] = value;
255
+ }
222
256
  }
223
257
  }
224
258
 
@@ -240,8 +274,16 @@ export class HttpClient implements GenericClient {
240
274
  ): Promise<CompletionResponse> {
241
275
  await this.refreshIfNeeded();
242
276
  return this.withRetry(async () => {
277
+ const {
278
+ signal,
279
+ timeout,
280
+ maxRetries,
281
+ backoffMs,
282
+ onResponseMetadata,
283
+ ...requestOptions
284
+ } = options;
243
285
  const body = {
244
- ...options,
286
+ ...requestOptions,
245
287
  model: options.model,
246
288
  messages: options.messages,
247
289
  max_tokens: options.max_tokens || 4000,
@@ -253,11 +295,29 @@ export class HttpClient implements GenericClient {
253
295
  }),
254
296
  };
255
297
 
256
- const response = await http.post(
257
- `${this.baseUrl}/v1/chat/completions`,
258
- body,
259
- { headers: this.headers as Record<string, string>, timeout: this.timeout }
260
- );
298
+ let response;
299
+ try {
300
+ response = await http.post(
301
+ `${this.baseUrl}/v1/chat/completions`,
302
+ body,
303
+ {
304
+ headers: this.headers as Record<string, string>,
305
+ timeout: this.timeout,
306
+ }
307
+ );
308
+ } catch (error) {
309
+ if (error instanceof HttpError) {
310
+ emitResponseMetadata(options, {
311
+ statusCode: error.status,
312
+ headers: error.response.headers,
313
+ });
314
+ }
315
+ throw error;
316
+ }
317
+ emitResponseMetadata(options, {
318
+ statusCode: response.status,
319
+ headers: response.headers,
320
+ });
261
321
 
262
322
  const data = response.data;
263
323
 
@@ -276,7 +336,8 @@ export class HttpClient implements GenericClient {
276
336
  })),
277
337
  model: data.model,
278
338
  usage: data.usage,
279
- usd_cost: data.usd_cost ?? this.calculateCost(options.model, data.usage),
339
+ usd_cost:
340
+ data.usd_cost ?? this.calculateCost(options.model, data.usage),
280
341
  };
281
342
  });
282
343
  }
@@ -289,8 +350,16 @@ export class HttpClient implements GenericClient {
289
350
  options: CompletionOptions
290
351
  ): AsyncGenerator<StreamChunk> {
291
352
  await this.refreshIfNeeded();
353
+ const {
354
+ signal,
355
+ timeout,
356
+ maxRetries,
357
+ backoffMs,
358
+ onResponseMetadata,
359
+ ...requestOptions
360
+ } = options;
292
361
  const body = {
293
- ...options,
362
+ ...requestOptions,
294
363
  model: options.model,
295
364
  messages: options.messages,
296
365
  max_tokens: options.max_tokens || 4000,
@@ -308,11 +377,15 @@ export class HttpClient implements GenericClient {
308
377
  headers: {
309
378
  ...(this.headers as Record<string, string>),
310
379
  "Content-Type": "application/json",
311
- "Accept": "text/event-stream",
380
+ Accept: "text/event-stream",
312
381
  },
313
382
  body: JSON.stringify(body),
314
383
  signal: options.signal,
315
384
  });
385
+ emitResponseMetadata(options, {
386
+ statusCode: response.status,
387
+ headers: response.headers,
388
+ });
316
389
 
317
390
  if (!response.ok) {
318
391
  const text = await response.text().catch(() => "");
@@ -376,58 +449,76 @@ export class HttpClient implements GenericClient {
376
449
  await this.refreshIfNeeded();
377
450
  return this.withRetry(async () => {
378
451
  // Extract system messages as instructions
379
- const systemMessages = options.messages.filter((m) => m.role === "system");
380
- const nonSystemMessages = options.messages.filter((m) => m.role !== "system");
381
- const instructions = systemMessages
382
- .map((m) => (typeof m.content === "string" ? m.content : ""))
383
- .join("\n")
384
- .trim() || undefined;
452
+ const systemMessages = options.messages.filter(
453
+ (m) => m.role === "system"
454
+ );
455
+ const nonSystemMessages = options.messages.filter(
456
+ (m) => m.role !== "system"
457
+ );
458
+ const instructions =
459
+ systemMessages
460
+ .map((m) => (typeof m.content === "string" ? m.content : ""))
461
+ .join("\n")
462
+ .trim() || undefined;
385
463
 
386
464
  // Convert messages to Responses API input format
387
465
  // Responses API hard limit: 10,485,760 chars per function_call_output.output
388
466
  const MAX_TOOL_OUTPUT = 10_485_000;
389
467
  const truncateToolOutput = (s: string) =>
390
468
  s.length > MAX_TOOL_OUTPUT
391
- ? s.slice(0, MAX_TOOL_OUTPUT) + "\n\n[TRUNCATED: output exceeded 10MB API limit]"
469
+ ? s.slice(0, MAX_TOOL_OUTPUT) +
470
+ "\n\n[TRUNCATED: output exceeded 10MB API limit]"
392
471
  : s;
393
- const input: any[] = nonSystemMessages.map((msg) => {
394
- if (msg.role === "tool") {
472
+ const input: any[] = nonSystemMessages
473
+ .map((msg) => {
474
+ if (msg.role === "tool") {
475
+ return {
476
+ type: "function_call_output",
477
+ call_id: msg.tool_call_id,
478
+ output: truncateToolOutput(
479
+ typeof msg.content === "string"
480
+ ? msg.content
481
+ : JSON.stringify(msg.content)
482
+ ),
483
+ };
484
+ }
485
+ if (msg.role === "assistant" && msg.tool_calls?.length) {
486
+ // Re-inject any captured reasoning items (encrypted_content) BEFORE
487
+ // the function_call items so the model preserves its chain-of-thought
488
+ // across tool-use turns instead of re-deriving it each turn.
489
+ const reasoningItems = this.getReasoningItems(msg);
490
+ return [
491
+ ...reasoningItems,
492
+ ...(msg.tool_calls as any[]).map((tc: any) => ({
493
+ type: "function_call",
494
+ id: tc.id.startsWith("fc") ? tc.id : `fc_${tc.id}`,
495
+ call_id: tc.id,
496
+ name: tc.function.name,
497
+ arguments: tc.function.arguments,
498
+ })),
499
+ ];
500
+ }
501
+ if (msg.role === "assistant" && this.getReasoningItems(msg).length) {
502
+ return [
503
+ ...this.getReasoningItems(msg),
504
+ {
505
+ role: msg.role,
506
+ content:
507
+ typeof msg.content === "string"
508
+ ? msg.content
509
+ : JSON.stringify(msg.content),
510
+ },
511
+ ];
512
+ }
395
513
  return {
396
- type: "function_call_output",
397
- call_id: msg.tool_call_id,
398
- output: truncateToolOutput(typeof msg.content === "string" ? msg.content : JSON.stringify(msg.content)),
514
+ role: msg.role,
515
+ content:
516
+ typeof msg.content === "string"
517
+ ? msg.content
518
+ : JSON.stringify(msg.content),
399
519
  };
400
- }
401
- if (msg.role === "assistant" && msg.tool_calls?.length) {
402
- // Re-inject any captured reasoning items (encrypted_content) BEFORE
403
- // the function_call items so the model preserves its chain-of-thought
404
- // across tool-use turns instead of re-deriving it each turn.
405
- const reasoningItems = this.getReasoningItems(msg);
406
- return [
407
- ...reasoningItems,
408
- ...(msg.tool_calls as any[]).map((tc: any) => ({
409
- type: "function_call",
410
- id: tc.id.startsWith("fc") ? tc.id : `fc_${tc.id}`,
411
- call_id: tc.id,
412
- name: tc.function.name,
413
- arguments: tc.function.arguments,
414
- })),
415
- ];
416
- }
417
- if (msg.role === "assistant" && this.getReasoningItems(msg).length) {
418
- return [
419
- ...this.getReasoningItems(msg),
420
- {
421
- role: msg.role,
422
- content: typeof msg.content === "string" ? msg.content : JSON.stringify(msg.content),
423
- },
424
- ];
425
- }
426
- return {
427
- role: msg.role,
428
- content: typeof msg.content === "string" ? msg.content : JSON.stringify(msg.content),
429
- };
430
- }).flat();
520
+ })
521
+ .flat();
431
522
 
432
523
  const tools = options.tools?.map((tool) => ({
433
524
  type: "function" as const,
@@ -461,11 +552,25 @@ export class HttpClient implements GenericClient {
461
552
  ...this.extra_body,
462
553
  };
463
554
 
464
- const response = await http.post(
465
- `${this.baseUrl}/v1/responses`,
466
- body,
467
- { headers: this.headers as Record<string, string>, timeout: this.timeout }
468
- );
555
+ let response;
556
+ try {
557
+ response = await http.post(`${this.baseUrl}/v1/responses`, body, {
558
+ headers: this.headers as Record<string, string>,
559
+ timeout: this.timeout,
560
+ });
561
+ } catch (error) {
562
+ if (error instanceof HttpError) {
563
+ emitResponseMetadata(options, {
564
+ statusCode: error.status,
565
+ headers: error.response.headers,
566
+ });
567
+ }
568
+ throw error;
569
+ }
570
+ emitResponseMetadata(options, {
571
+ statusCode: response.status,
572
+ headers: response.headers,
573
+ });
469
574
 
470
575
  const data = response.data;
471
576
 
@@ -547,14 +652,32 @@ export class HttpClient implements GenericClient {
547
652
  async createEmbedding(options: EmbeddingOptions): Promise<EmbeddingResponse> {
548
653
  await this.refreshIfNeeded();
549
654
  return this.withRetry(async () => {
550
- const response = await http.post(
551
- `${this.baseUrl}/v1/embeddings`,
552
- {
553
- model: options.model,
554
- input: options.input,
555
- },
556
- { headers: this.headers as Record<string, string>, timeout: this.timeout }
557
- );
655
+ let response;
656
+ try {
657
+ response = await http.post(
658
+ `${this.baseUrl}/v1/embeddings`,
659
+ {
660
+ model: options.model,
661
+ input: options.input,
662
+ },
663
+ {
664
+ headers: this.headers as Record<string, string>,
665
+ timeout: this.timeout,
666
+ }
667
+ );
668
+ } catch (error) {
669
+ if (error instanceof HttpError) {
670
+ emitResponseMetadata(options, {
671
+ statusCode: error.status,
672
+ headers: error.response.headers,
673
+ });
674
+ }
675
+ throw error;
676
+ }
677
+ emitResponseMetadata(options, {
678
+ statusCode: response.status,
679
+ headers: response.headers,
680
+ });
558
681
 
559
682
  const data = response.data;
560
683
 
@@ -567,7 +690,8 @@ export class HttpClient implements GenericClient {
567
690
  data: data.data,
568
691
  model: options.model,
569
692
  usage: data.usage,
570
- usd_cost: data.usd_cost ?? this.calculateCost(options.model, data.usage),
693
+ usd_cost:
694
+ data.usd_cost ?? this.calculateCost(options.model, data.usage),
571
695
  };
572
696
  });
573
697
  }
@@ -575,10 +699,13 @@ export class HttpClient implements GenericClient {
575
699
  async getModels(type = "all") {
576
700
  await this.refreshIfNeeded();
577
701
  return this.withRetry(async () => {
578
- const response = await http.get(`${this.baseUrl}/v1/models?type=${type}`, {
579
- headers: this.headers as Record<string, string>,
580
- timeout: this.timeout,
581
- });
702
+ const response = await http.get(
703
+ `${this.baseUrl}/v1/models?type=${type}`,
704
+ {
705
+ headers: this.headers as Record<string, string>,
706
+ timeout: this.timeout,
707
+ }
708
+ );
582
709
 
583
710
  const data = response.data?.data;
584
711
 
@@ -31,6 +31,7 @@ import { ModelProvider } from "../types";
31
31
  import { getConfig, getConfigSync } from "../config";
32
32
  import { loadKnowhowJwt, KNOWHOW_API_URL } from "../services/KnowhowClient";
33
33
  import { keyPairExists } from "../auth/keyManager";
34
+ import { getOrgIdForApi } from "../auth/environmentAuth";
34
35
  import { ContextLimits } from "./contextLimits";
35
36
  import { OpenAiTextPricing } from "./pricing/openai";
36
37
  import { AnthropicTextPricing } from "./pricing/anthropic";
@@ -69,6 +70,7 @@ export {
69
70
  XaiImagePricing,
70
71
  XaiVideoPricing,
71
72
  };
73
+ export * from "./responseMetadata";
72
74
  export type {
73
75
  ModelPricing,
74
76
  ModelType,
@@ -82,8 +84,8 @@ export type {
82
84
  // ---------------------------------------------------------------------------
83
85
 
84
86
  type ProviderRegistryEntry = {
85
- /** Constructor that accepts up to two optional string args (e.g. apiKey or url, jwt) */
86
- clientClass?: new (arg1?: string, arg2?: string) => GenericClient;
87
+ /** Constructor that accepts the optional API key passed by the registry. */
88
+ clientClass?: new (arg1?: string) => GenericClient;
87
89
  /** Custom factory — takes precedence over clientClass */
88
90
  createClient?: (entry: ModelProvider) => GenericClient | null;
89
91
  };
@@ -111,7 +113,12 @@ const BUILT_IN_PROVIDER_REGISTRY: Record<string, ProviderRegistryEntry> = {
111
113
  createClient: (entry: ModelProvider) => {
112
114
  if (!loadKnowhowJwt()) {
113
115
  const config = getConfigSync();
114
- if (!config.orgId || (!keyPairExists(config.cliIdentityPath) && !keyPairExists())) return null;
116
+ const orgId = getOrgIdForApi(config, KNOWHOW_API_URL);
117
+ if (
118
+ !orgId ||
119
+ (!keyPairExists(config.cliIdentityPath) && !keyPairExists())
120
+ )
121
+ return null;
115
122
  }
116
123
  return new KnowhowGenericClient(KNOWHOW_API_URL);
117
124
  },
@@ -320,9 +327,10 @@ export class AIClient {
320
327
  if (entry.provider === "knowhow") {
321
328
  if (process.env.KNOWHOW_CLI) {
322
329
  const warnNeeds = (process.env.KNOWHOW_WARN_NEEDS ?? "").split(",");
323
- if (warnNeeds.includes("models")) console.warn(
324
- `⚠️ Knowhow provider is not logged in. Run 'knowhow login' to enable Knowhow models.`
325
- );
330
+ if (warnNeeds.includes("models"))
331
+ console.warn(
332
+ `⚠️ Knowhow provider is not logged in. Run 'knowhow login' to enable Knowhow models.`
333
+ );
326
334
  }
327
335
  }
328
336
  continue;
@@ -417,7 +425,9 @@ export class AIClient {
417
425
  // Model not in local registry — pass it through anyway so the provider
418
426
  // API can accept or reject it directly (e.g. newly-released models that
419
427
  // haven't been fetched into our local model list yet).
420
- console.warn(`⚠️ Model '${model}' not in local registry for provider '${provider}', attempting anyway.`);
428
+ console.warn(
429
+ `⚠️ Model '${model}' not in local registry for provider '${provider}', attempting anyway.`
430
+ );
421
431
  }
422
432
 
423
433
  return { client: this.clients[provider], provider, model };
@@ -914,7 +924,9 @@ export class AIClient {
914
924
  * For HttpClient-based providers with getPricing(), only priced models are kept.
915
925
  * For other providers (no getPricing()), all models pass through unchanged.
916
926
  */
917
- private _filterByPricing(models: Record<string, string[]>): Record<string, string[]> {
927
+ private _filterByPricing(
928
+ models: Record<string, string[]>
929
+ ): Record<string, string[]> {
918
930
  const result: Record<string, string[]> = {};
919
931
  for (const [provider, ids] of Object.entries(models)) {
920
932
  const client = this.clients[provider];
@@ -1009,7 +1021,9 @@ export class AIClient {
1009
1021
  }
1010
1022
 
1011
1023
  listAllModelsWithProvider(options?: { pricing?: boolean }) {
1012
- const models = options?.pricing ? this._filterByPricing(this.clientModels) : this.clientModels;
1024
+ const models = options?.pricing
1025
+ ? this._filterByPricing(this.clientModels)
1026
+ : this.clientModels;
1013
1027
  return Object.entries(models)
1014
1028
  .map(([provider, ids]) => ids.map((m) => ({ id: `${provider}/${m}` })))
1015
1029
  .flat();
@@ -1018,7 +1032,9 @@ export class AIClient {
1018
1032
  /**
1019
1033
  * Returns the context window limit (in tokens) for a given model.
1020
1034
  * Delegates to the registered client's getContextLimit() if available.
1021
- * Falls back to the global ContextLimits table.
1035
+ * Falls back to the global ContextLimits table, stripping any leading
1036
+ * provider prefix (e.g. "openai/gpt-5.6-sol" → "gpt-5.6-sol") so that
1037
+ * proxied providers like "knowhow" resolve correctly.
1022
1038
  */
1023
1039
  getContextLimit(
1024
1040
  provider: string,
@@ -1028,9 +1044,25 @@ export class AIClient {
1028
1044
  if (client?.getContextLimit) {
1029
1045
  return client.getContextLimit(model);
1030
1046
  }
1031
- const contextLimit = ContextLimits[model];
1032
- if (contextLimit === undefined) return undefined;
1033
- return { contextLimit, threshold: contextLimit };
1047
+ // Try the model as-is first, then strip leading "provider/" prefixes
1048
+ // (e.g. "openai/gpt-5.6-sol" used with the "knowhow" proxy provider).
1049
+ const candidates = [model];
1050
+ if (model.includes("/")) {
1051
+ const parts = model.split("/");
1052
+ for (let i = 1; i < parts.length; i++) {
1053
+ candidates.push(parts.slice(i).join("/"));
1054
+ }
1055
+ }
1056
+ for (const candidate of candidates) {
1057
+ const contextLimit = ContextLimits[candidate];
1058
+ if (contextLimit !== undefined) {
1059
+ const pricing = OpenAiTextPricing[candidate];
1060
+ const threshold =
1061
+ pricing && "input_gt_200k" in pricing ? 200_000 : contextLimit;
1062
+ return { contextLimit, threshold };
1063
+ }
1064
+ }
1065
+ return undefined;
1034
1066
  }
1035
1067
 
1036
1068
  /**