@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,5 @@
1
- import { HttpClient } from "./http";
1
+ import { HttpClient, HttpClientOptions } from "./http";
2
+ import { withKnowhowAttribution } from "./attribution";
2
3
  import { OpenRouterTextPricing } from "./pricing/openrouter";
3
4
 
4
5
  /**
@@ -9,8 +10,14 @@ import { OpenRouterTextPricing } from "./pricing/openrouter";
9
10
  * Set env var OPENROUTER_API_KEY to enable.
10
11
  */
11
12
  export class GenericOpenRouterClient extends HttpClient {
12
- constructor(apiKey = process.env.OPENROUTER_API_KEY) {
13
- super("https://openrouter.ai/api");
13
+ constructor(
14
+ apiKey = process.env.OPENROUTER_API_KEY,
15
+ transportOptions: HttpClientOptions = {}
16
+ ) {
17
+ super("https://openrouter.ai/api", {
18
+ ...transportOptions,
19
+ headers: withKnowhowAttribution(transportOptions.headers),
20
+ });
14
21
  if (apiKey) this.setJwt(apiKey);
15
22
  this.setPrices(OpenRouterTextPricing);
16
23
  }
@@ -11,6 +11,10 @@ import { ModelPricing, ModelCatalogEntry, completions, embeddings } from "./type
11
11
  export const FireworksModels = {
12
12
  // Moonshot AI
13
13
  KimiK3: "accounts/fireworks/models/kimi-k3",
14
+
15
+ // Meta
16
+ MuseGlimmer30b: "accounts/fireworks/models/muse-glimmer-30b",
17
+
14
18
  KimiK2p7Code: "accounts/fireworks/models/kimi-k2p7-code",
15
19
  KimiK2_6: "accounts/fireworks/models/kimi-k2-6",
16
20
  KimiK2_5: "accounts/fireworks/models/kimi-k2-5",
@@ -22,6 +26,13 @@ export const FireworksModels = {
22
26
 
23
27
  // Qwen
24
28
  Qwen3p7Plus: "accounts/fireworks/models/qwen3p7-plus",
29
+ Qwen3p8Max: "accounts/fireworks/models/qwen3p8-2p4t-a95b",
30
+
31
+ // NVIDIA
32
+ NemotronLightning3p530bA3b: "accounts/fireworks/models/nemotron-lightning-3p5-30b-a3b",
33
+
34
+ // Thinking Machines Lab
35
+ Inkling: "accounts/fireworks/models/inkling",
25
36
 
26
37
  // Deprecated Qwen Models
27
38
  Qwen3_6Plus: "accounts/fireworks/models/qwen3-6-plus",
@@ -56,6 +67,10 @@ export const FireworksTextPricing: Record<string, ModelPricing> = {
56
67
  // Moonshot AI
57
68
  [FireworksModels.KimiK3]: { input: 3.0, cache_hit: 0.30, output: 15.0 },
58
69
  [FireworksModels.KimiK2p7Code]: { input: 0.95, cache_hit: 0.19, output: 4.0 },
70
+
71
+ // Meta
72
+ [FireworksModels.MuseGlimmer30b]: { input: 0.35, cache_hit: 0.04, output: 1.50 },
73
+
59
74
  [FireworksModels.KimiK2_6]: { input: 0.95, cache_hit: 0.16, output: 4.0 },
60
75
  [FireworksModels.KimiK2_5]: { input: 0.60, cache_hit: 0.10, output: 3.0 },
61
76
 
@@ -67,6 +82,14 @@ export const FireworksTextPricing: Record<string, ModelPricing> = {
67
82
  // Qwen
68
83
  [FireworksModels.Qwen3p7Plus]: { input: 0.40, cache_hit: 0.08, output: 1.60 },
69
84
 
85
+ [FireworksModels.Qwen3p8Max]: { input: 2.00, cache_hit: 0.25, output: 6.00 },
86
+
87
+ // NVIDIA
88
+ [FireworksModels.NemotronLightning3p530bA3b]: { input: 0.05, cache_hit: 0.01, output: 0.20 },
89
+
90
+ // Thinking Machines Lab
91
+ [FireworksModels.Inkling]: { input: 1.00, cache_hit: 0.17, output: 4.05 },
92
+
70
93
  // Deprecated QWEN Models
71
94
  [FireworksModels.Qwen3_6Plus]: { input: 0.50, cache_hit: 0.10, output: 3.0, deprecated: true },
72
95
  [FireworksModels.Qwen3p6Plus]: { input: 0.50, cache_hit: 0.10, output: 3.0, deprecated: true },
@@ -81,8 +104,8 @@ export const FireworksTextPricing: Record<string, ModelPricing> = {
81
104
  [FireworksModels.Glm4_7]: { input: 0.60, cache_hit: 0.30, output: 2.20, deprecated: true },
82
105
 
83
106
  // DeepSeek AI
84
- [FireworksModels.DeepseekV4Pro]: { input: 1.74, cache_hit: 0.15, output: 3.48 },
85
- [FireworksModels.DeepseekV4Flash]: { input: 0.14, cache_hit: 0.03, output: 0.28 },
107
+ [FireworksModels.DeepseekV4Pro]: { input: 1.32, cache_hit: 0.044, output: 3.96 },
108
+ [FireworksModels.DeepseekV4Flash]: { input: 0.22, cache_hit: 0.007, output: 0.66 },
86
109
  [FireworksModels.DeepseekV3_2]: { input: 0.56, cache_hit: 0.28, output: 1.68, deprecated: true },
87
110
  [FireworksModels.DeepseekV3_1]: { input: 0.56, cache_hit: 0.28, output: 1.68, deprecated: true },
88
111
 
@@ -111,12 +134,19 @@ export const FireworksActiveTextModels: string[] = [
111
134
  FireworksModels.KimiK2p7Code,
112
135
  FireworksModels.KimiK2_6,
113
136
  FireworksModels.KimiK2_5,
137
+ // Meta
138
+ FireworksModels.MuseGlimmer30b,
114
139
  // MiniMax
115
140
  FireworksModels.MinimaxM3,
116
141
  FireworksModels.MinimaxM2_7,
117
142
  FireworksModels.MinimaxM2_5,
118
143
  // Qwen
119
144
  FireworksModels.Qwen3p7Plus,
145
+ FireworksModels.Qwen3p8Max,
146
+ // NVIDIA
147
+ FireworksModels.NemotronLightning3p530bA3b,
148
+ // Thinking Machines Lab
149
+ FireworksModels.Inkling,
120
150
  // Z.ai
121
151
  FireworksModels.Glm5p2,
122
152
  FireworksModels.Glm5p1,
@@ -231,7 +231,21 @@ export const OpenAiSearchModels: string[] = [
231
231
 
232
232
  export const OpenAiTextPricing: Record<string, ModelPricing> = {
233
233
  // gpt-5.6 series — short context / long context pricing
234
- [OpenAiModels.GPT_56_Sol]: { input: 5.0, cached_input: 0.5, cache_write: 6.25, output: 30.0, input_gt_200k: 10.0, output_gt_200k: 45.0 },
234
+ [OpenAiModels.GPT_56_Sol]: {
235
+ input: 4.0,
236
+ cached_input: 0.40,
237
+ cache_write: 5.00,
238
+ output: 30.0,
239
+ input_gt_200k: 10.0,
240
+ output_gt_200k: 30.0,
241
+ changelog: [
242
+ {
243
+ date: "2026-08-01",
244
+ note: "Launch pricing — promotional rates available through November 21, 2026",
245
+ pricing: { input: 5.0, cached_input: 0.5, cache_write: 6.25, output: 30.0, input_gt_200k: 10.0, output_gt_200k: 45.0 },
246
+ },
247
+ ],
248
+ },
235
249
  [OpenAiModels.GPT_56_Terra]: { input: 2.0, cached_input: 0.20, cache_write: 2.5, output: 12.0, input_gt_200k: 4.0, output_gt_200k: 18.0 },
236
250
  [OpenAiModels.GPT_56_Luna]: { input: 0.2, cached_input: 0.02, cache_write: 0.25, output: 1.2, input_gt_200k: 0.4, output_gt_200k: 1.8 },
237
251
  [OpenAiModels.GPT_55]: { input: 5.0, cached_input: 0.5, output: 30.0 },
@@ -1,3 +1,16 @@
1
+ /**
2
+ * A historical record of a model's pricing at a point in time.
3
+ * Use this to track price changes over time.
4
+ */
5
+ export interface PricingChangelogEntry {
6
+ /** ISO date string when this pricing became effective (e.g. "2025-04-01") */
7
+ date: string;
8
+ /** Optional human-readable note about the change (e.g. "Promotional pricing announcement") */
9
+ note?: string;
10
+ /** The pricing values that were in effect from this date */
11
+ pricing: Omit<ModelPricing, "deprecated" | "deprecationDate" | "limitedAvailability" | "replacedBy" | "reasoningLevels" | "useResponsesApi" | "changelog">;
12
+ }
13
+
1
14
  export type ModelType =
2
15
  | "completion"
3
16
  | "embedding"
@@ -30,6 +43,8 @@ export interface ModelPricing {
30
43
  reasoningLevels?: string[];
31
44
  /** If true, this model must be called via the Responses API (/v1/responses) instead of /v1/chat/completions */
32
45
  useResponsesApi?: boolean;
46
+ /** Optional changelog of historical pricing entries, ordered from oldest to newest */
47
+ changelog?: PricingChangelogEntry[];
33
48
  }
34
49
 
35
50
  export interface ModelCatalogEntry {
@@ -0,0 +1,221 @@
1
+ import {
2
+ ProviderRateLimitMetadata,
3
+ ProviderResponseMetadata,
4
+ RetryOptions,
5
+ } from "./types";
6
+
7
+ const MAX_HEADER_VALUE_LENGTH = 256;
8
+ const MAX_HEADERS = 24;
9
+ const MAX_TOTAL_HEADER_CHARS = 4096;
10
+ const SAFE_HEADER_NAMES = new Set([
11
+ "retry-after",
12
+ "ratelimit-limit",
13
+ "ratelimit-remaining",
14
+ "ratelimit-reset",
15
+ "request-id",
16
+ "x-request-id",
17
+ "cf-ray",
18
+ "x-amzn-requestid",
19
+ "x-amz-request-id",
20
+ ]);
21
+ const SAFE_HEADER_PREFIXES = ["x-ratelimit-", "anthropic-ratelimit-"];
22
+
23
+ type HeaderSource =
24
+ | Headers
25
+ | Record<string, unknown>
26
+ | Iterable<[string, unknown]>
27
+ | undefined;
28
+
29
+ function isSafeHeader(name: string) {
30
+ return (
31
+ SAFE_HEADER_NAMES.has(name) ||
32
+ SAFE_HEADER_PREFIXES.some((prefix) => name.startsWith(prefix))
33
+ );
34
+ }
35
+
36
+ function entriesFor(headers: HeaderSource): Iterable<[string, unknown]> {
37
+ if (!headers) return [];
38
+ if (Symbol.iterator in Object(headers)) {
39
+ return headers as Iterable<[string, unknown]>;
40
+ }
41
+ if (typeof (headers as Headers).forEach === "function") {
42
+ const entries: Array<[string, unknown]> = [];
43
+ (headers as Headers).forEach((value, name) => entries.push([name, value]));
44
+ return entries;
45
+ }
46
+ return Object.entries(headers as Record<string, unknown>);
47
+ }
48
+
49
+ function nonNegativeNumber(value: string | undefined) {
50
+ if (value === undefined || value.trim() === "") return undefined;
51
+ const number = Number(value);
52
+ return Number.isFinite(number) && number >= 0 ? number : undefined;
53
+ }
54
+
55
+ function resetAt(value: string | undefined, now: Date) {
56
+ if (!value) return undefined;
57
+ const seconds = nonNegativeNumber(value);
58
+ if (seconds !== undefined)
59
+ return new Date(now.getTime() + seconds * 1000).toISOString();
60
+
61
+ // OpenAI-compatible APIs commonly return compact durations such as `6m0s`.
62
+ const duration = [...value.trim().matchAll(/(\d+(?:\.\d+)?)(ms|s|m|h|d)/gi)];
63
+ if (
64
+ duration.length &&
65
+ duration.map((part) => part[0]).join("") === value.trim()
66
+ ) {
67
+ const milliseconds = duration.reduce((total, [, amount, unit]) => {
68
+ const multiplier = {
69
+ ms: 1,
70
+ s: 1_000,
71
+ m: 60_000,
72
+ h: 3_600_000,
73
+ d: 86_400_000,
74
+ }[unit.toLowerCase()];
75
+ return total + Number(amount) * multiplier;
76
+ }, 0);
77
+ return new Date(now.getTime() + milliseconds).toISOString();
78
+ }
79
+
80
+ const timestamp = Date.parse(value);
81
+ return Number.isNaN(timestamp)
82
+ ? undefined
83
+ : new Date(timestamp).toISOString();
84
+ }
85
+
86
+ function retryAfterSeconds(value: string | undefined, now: Date) {
87
+ const seconds = nonNegativeNumber(value);
88
+ if (seconds !== undefined) return seconds;
89
+ if (!value) return undefined;
90
+ const timestamp = Date.parse(value);
91
+ return Number.isNaN(timestamp)
92
+ ? undefined
93
+ : Math.max(0, (timestamp - now.getTime()) / 1000);
94
+ }
95
+
96
+ /**
97
+ * Derives one JSON-safe shape from standard, OpenAI-compatible, and Anthropic
98
+ * rate-limit headers. Callers receive this even when a provider supplied none.
99
+ */
100
+ export function normalizeRateLimitMetadata(
101
+ headers: Record<string, string>,
102
+ now = new Date()
103
+ ): ProviderRateLimitMetadata {
104
+ const header = (...names: string[]) =>
105
+ names.map((name) => headers[name]).find((value) => value !== undefined);
106
+ const limit = nonNegativeNumber(
107
+ header(
108
+ "anthropic-ratelimit-requests-limit",
109
+ "ratelimit-limit",
110
+ "x-ratelimit-limit-requests",
111
+ "x-ratelimit-limit"
112
+ )
113
+ );
114
+ const remaining = nonNegativeNumber(
115
+ header(
116
+ "anthropic-ratelimit-requests-remaining",
117
+ "ratelimit-remaining",
118
+ "x-ratelimit-remaining-requests",
119
+ "x-ratelimit-remaining"
120
+ )
121
+ );
122
+ const requestResetAt = resetAt(
123
+ header(
124
+ "anthropic-ratelimit-requests-reset",
125
+ "ratelimit-reset",
126
+ "x-ratelimit-reset-requests",
127
+ "x-ratelimit-reset"
128
+ ),
129
+ now
130
+ );
131
+ const tokenLimit = nonNegativeNumber(
132
+ header("anthropic-ratelimit-tokens-limit", "x-ratelimit-limit-tokens")
133
+ );
134
+ const tokenRemaining = nonNegativeNumber(
135
+ header(
136
+ "anthropic-ratelimit-tokens-remaining",
137
+ "x-ratelimit-remaining-tokens"
138
+ )
139
+ );
140
+ const tokenResetAt = resetAt(
141
+ header("anthropic-ratelimit-tokens-reset", "x-ratelimit-reset-tokens"),
142
+ now
143
+ );
144
+ const retryAfter = retryAfterSeconds(header("retry-after"), now);
145
+ return {
146
+ ...(limit !== undefined && { limit }),
147
+ ...(remaining !== undefined && { remaining }),
148
+ ...(requestResetAt && { resetAt: requestResetAt }),
149
+ ...(retryAfter !== undefined && { retryAfterSeconds: retryAfter }),
150
+ ...(tokenLimit !== undefined && { tokenLimit }),
151
+ ...(tokenRemaining !== undefined && { tokenRemaining }),
152
+ ...(tokenResetAt && { tokenResetAt }),
153
+ };
154
+ }
155
+
156
+ /**
157
+ * Converts provider headers to the deliberately small telemetry-safe surface.
158
+ * This function never returns request bodies, credentials, cookies, or arbitrary
159
+ * provider headers.
160
+ */
161
+ export function normalizeResponseMetadata(input: {
162
+ statusCode?: number;
163
+ headers?: HeaderSource;
164
+ requestId?: string | null;
165
+ observedAt?: Date;
166
+ }): ProviderResponseMetadata {
167
+ const headers: Record<string, string> = {};
168
+ let totalChars = 0;
169
+
170
+ for (const [rawName, rawValue] of entriesFor(input.headers)) {
171
+ const name = String(rawName).toLowerCase();
172
+ if (
173
+ !isSafeHeader(name) ||
174
+ Object.prototype.hasOwnProperty.call(headers, name)
175
+ )
176
+ continue;
177
+ const value = String(rawValue).slice(0, MAX_HEADER_VALUE_LENGTH);
178
+ if (
179
+ Object.keys(headers).length >= MAX_HEADERS ||
180
+ totalChars + name.length + value.length > MAX_TOTAL_HEADER_CHARS
181
+ )
182
+ break;
183
+ headers[name] = value;
184
+ totalChars += name.length + value.length;
185
+ }
186
+
187
+ const requestId =
188
+ input.requestId ??
189
+ headers["request-id"] ??
190
+ headers["x-request-id"] ??
191
+ headers["x-amzn-requestid"] ??
192
+ headers["x-amz-request-id"];
193
+ return {
194
+ ...(typeof input.statusCode === "number" && {
195
+ statusCode: input.statusCode,
196
+ }),
197
+ rateLimit: normalizeRateLimitMetadata(headers, input.observedAt),
198
+ ...(Object.keys(headers).length > 0 && { headers }),
199
+ ...(requestId && {
200
+ requestId: String(requestId).slice(0, MAX_HEADER_VALUE_LENGTH),
201
+ }),
202
+ };
203
+ }
204
+
205
+ /** Emits one visible HTTP response synchronously and isolates observer failures. */
206
+ export function emitResponseMetadata(
207
+ options: Pick<RetryOptions, "onResponseMetadata"> | undefined,
208
+ input: {
209
+ statusCode?: number;
210
+ headers?: HeaderSource;
211
+ requestId?: string | null;
212
+ }
213
+ ) {
214
+ const observer = options?.onResponseMetadata;
215
+ if (!observer) return;
216
+ try {
217
+ observer(normalizeResponseMetadata(input));
218
+ } catch {
219
+ // Observability must never affect a provider request or its original error.
220
+ }
221
+ }
@@ -104,6 +104,36 @@ export interface Tool {
104
104
  };
105
105
  }
106
106
 
107
+ /**
108
+ * Provider-neutral rate-limit information derived from response headers.
109
+ * Reset values are ISO-8601 strings so callback payloads remain JSON-safe.
110
+ */
111
+ export interface ProviderRateLimitMetadata {
112
+ limit?: number;
113
+ remaining?: number;
114
+ resetAt?: string;
115
+ retryAfterSeconds?: number;
116
+ tokenLimit?: number;
117
+ tokenRemaining?: number;
118
+ tokenResetAt?: string;
119
+ }
120
+
121
+ /**
122
+ * Safe telemetry details for one provider response. `rateLimit` is normalized
123
+ * by the client for every observed response. `headers`, when present, contains
124
+ * only the bounded allowlisted raw headers used to derive it.
125
+ */
126
+ export interface ProviderResponseMetadata {
127
+ statusCode?: number;
128
+ rateLimit: ProviderRateLimitMetadata;
129
+ headers?: Record<string, string>;
130
+ requestId?: string;
131
+ }
132
+
133
+ export type ProviderResponseMetadataObserver = (
134
+ metadata: ProviderResponseMetadata
135
+ ) => void;
136
+
107
137
  export interface ToolCall {
108
138
  id: string;
109
139
  type: "function";
@@ -134,6 +164,11 @@ export interface RetryOptions {
134
164
  * attempt is cancelled immediately and no further retries are made.
135
165
  */
136
166
  signal?: AbortSignal;
167
+ /**
168
+ * Synchronous observer invoked once for each provider HTTP response visible to
169
+ * this client, including errors and streaming handshakes.
170
+ */
171
+ onResponseMetadata?: ProviderResponseMetadataObserver;
137
172
  }
138
173
 
139
174
  /**
@@ -150,10 +185,13 @@ export const REASONING_EFFORTS = [
150
185
  "xhigh",
151
186
  "max",
152
187
  ] as const;
153
- export type ReasoningEffort = (typeof REASONING_EFFORTS)[number];
188
+ export type ReasoningEffort = typeof REASONING_EFFORTS[number];
154
189
 
155
190
  export function isReasoningEffort(value: unknown): value is ReasoningEffort {
156
- return typeof value === "string" && REASONING_EFFORTS.includes(value as ReasoningEffort);
191
+ return (
192
+ typeof value === "string" &&
193
+ REASONING_EFFORTS.includes(value as ReasoningEffort)
194
+ );
157
195
  }
158
196
 
159
197
  export interface CompletionOptions extends RetryOptions {
@@ -438,7 +476,9 @@ export interface GenericClient {
438
476
  setKey(key: string): void;
439
477
  createChatCompletion(options: CompletionOptions): Promise<CompletionResponse>;
440
478
  /** Optional streaming variant — yields incremental tokens then a final done chunk. */
441
- createChatCompletionStream?(options: CompletionOptions): AsyncGenerator<StreamChunk>;
479
+ createChatCompletionStream?(
480
+ options: CompletionOptions
481
+ ): AsyncGenerator<StreamChunk>;
442
482
  createEmbedding(options: EmbeddingOptions): Promise<EmbeddingResponse>;
443
483
  createAudioTranscription?(
444
484
  options: AudioTranscriptionOptions
@@ -482,5 +522,10 @@ export interface GenericClient {
482
522
  * Returns undefined for a specific model if no pricing is known.
483
523
  * Only implemented by HttpClient-based providers that have been given a pricing map via setPrices().
484
524
  */
485
- getPricing?(model?: string): import("./pricing/types").ModelPricing | Record<string, import("./pricing/types").ModelPricing> | undefined;
525
+ getPricing?(
526
+ model?: string
527
+ ):
528
+ | import("./pricing/types").ModelPricing
529
+ | Record<string, import("./pricing/types").ModelPricing>
530
+ | undefined;
486
531
  }
@@ -273,6 +273,7 @@ export function addMcpCommands(program: Command): void {
273
273
  await listRemoteSecrets();
274
274
  }
275
275
  });
276
+
276
277
  }
277
278
 
278
279
  // ──────────────────────────────────────────────────────────────────────────────
@@ -63,7 +63,7 @@ export function addLoginCommand(program: Command): void {
63
63
  return;
64
64
  }
65
65
 
66
- await login(options.jwt, options.identity);
66
+ await login(options.jwt, options.identity, options.apiUrl);
67
67
  });
68
68
  }
69
69
 
@@ -0,0 +1,77 @@
1
+ import { Command } from "commander";
2
+ import { getConfig, updateConfig } from "../config";
3
+ import {
4
+ createRemote,
5
+ DEFAULT_API_URL,
6
+ DEFAULT_REMOTE_NAME,
7
+ getConfiguredRemotes,
8
+ registerRemote,
9
+ } from "../remotes";
10
+
11
+ export function addRemoteCommand(program: Command): void {
12
+ const command = program
13
+ .command("remote")
14
+ .description("Manage Knowhow API remotes");
15
+
16
+ command
17
+ .command("add")
18
+ .description("Register a Knowhow-compatible backend")
19
+ .argument("<name>", "Remote name")
20
+ .argument("<api-url>", "API base URL")
21
+ .action(async (name: string, apiUrl: string) => {
22
+ const config = await getConfig();
23
+ if (config.remotes?.[name]) {
24
+ throw new Error(`Remote '${name}' already exists.`);
25
+ }
26
+ const remote = createRemote(name, apiUrl);
27
+ registerRemote(config, remote);
28
+ await updateConfig(config);
29
+ console.log(`Added remote '${name}' (${remote.apiUrl}).`);
30
+ });
31
+
32
+ command
33
+ .command("use")
34
+ .description("Select the remote used by Knowhow commands in this configuration")
35
+ .argument("<name>", "Remote name")
36
+ .action(async (name: string) => {
37
+ const config = await getConfig();
38
+ const remote = getConfiguredRemotes(config)[name] ??
39
+ (name === DEFAULT_REMOTE_NAME ? createRemote(DEFAULT_REMOTE_NAME, DEFAULT_API_URL) : undefined);
40
+ if (!remote) {
41
+ throw new Error(`Unknown remote '${name}'. Add it with: knowhow remote add ${name} <api-url>`);
42
+ }
43
+ registerRemote(config, remote);
44
+ config.activeRemote = name;
45
+ await updateConfig(config);
46
+ console.log(`Using remote '${name}' (${remote.apiUrl}).`);
47
+ });
48
+
49
+ command
50
+ .command("list")
51
+ .alias("ls")
52
+ .description("List configured remotes")
53
+ .action(async () => {
54
+ const config = await getConfig();
55
+ const remotes = { ...getConfiguredRemotes(config) };
56
+ if (!remotes[DEFAULT_REMOTE_NAME]) {
57
+ remotes[DEFAULT_REMOTE_NAME] = createRemote(DEFAULT_REMOTE_NAME, DEFAULT_API_URL);
58
+ }
59
+ for (const [name, remote] of Object.entries(remotes)) {
60
+ const marker = (config.activeRemote ?? DEFAULT_REMOTE_NAME) === name ? "*" : " ";
61
+ console.log(`${marker} ${name}\t${remote.apiUrl}\t${remote.jwtPath}${remote.orgId ? `\t${remote.orgId}` : ""}`);
62
+ }
63
+ });
64
+
65
+ command
66
+ .command("get-url")
67
+ .description("Print a remote API URL")
68
+ .argument("[name]", "Remote name")
69
+ .action(async (name?: string) => {
70
+ const config = await getConfig();
71
+ const selectedName = name ?? config.activeRemote ?? DEFAULT_REMOTE_NAME;
72
+ const remote = getConfiguredRemotes(config)[selectedName] ??
73
+ (selectedName === DEFAULT_REMOTE_NAME ? createRemote(DEFAULT_REMOTE_NAME, DEFAULT_API_URL) : undefined);
74
+ if (!remote) throw new Error(`Unknown remote '${selectedName}'.`);
75
+ console.log(remote.apiUrl);
76
+ });
77
+ }
package/src/config.ts CHANGED
@@ -4,7 +4,6 @@ import * as os from "os";
4
4
  import gitignoreToGlob from "gitignore-to-glob";
5
5
  import { Prompts } from "./prompts";
6
6
  import { promisify } from "util";
7
- import { KNOWHOW_API_URL } from "./services/KnowhowClient";
8
7
  import {
9
8
  Config,
10
9
  SourceRef,
@@ -113,6 +112,15 @@ const defaultConfig = {
113
112
  },
114
113
  },
115
114
 
115
+ activeRemote: "origin",
116
+ remotes: {
117
+ origin: {
118
+ name: "origin",
119
+ apiUrl: "https://api.knowhow.tyvm.ai",
120
+ jwtPath: ".knowhow/.jwt",
121
+ },
122
+ },
123
+
116
124
  syncRemote: false,
117
125
  } as Config;
118
126