@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
@@ -14,6 +14,7 @@ import { AgentSyncFs } from "../../services/AgentSyncFs";
14
14
  import { AgentModule } from "./AgentModule";
15
15
  import { TaskInfo } from "../types";
16
16
  import { getConfig, updateConfig } from "../../config";
17
+ import { withRetry } from "../../clients/withRetry";
17
18
 
18
19
  @TraceAll()
19
20
  export class RemoteSyncModule extends BaseChatModule {
@@ -44,6 +45,22 @@ export class RemoteSyncModule extends BaseChatModule {
44
45
  this.client = new KnowhowSimpleClient(KNOWHOW_API_URL);
45
46
  }
46
47
 
48
+ /**
49
+ * Start an automatic sync without delaying agent execution. Manual syncs use
50
+ * syncTask directly so the command still waits for and reports its result.
51
+ */
52
+ public syncTaskInBackground(taskId: string): void {
53
+ void this.syncTask(taskId).catch((error: unknown) => {
54
+ // syncTask handles expected API failures itself. Keep this final guard so
55
+ // an unexpected failure can never become an unhandled rejection.
56
+ console.error(
57
+ `❌ Unexpected remote sync failure: ${
58
+ error instanceof Error ? error.message : String(error)
59
+ }`
60
+ );
61
+ });
62
+ }
63
+
47
64
  /**
48
65
  * On initialize, read config to set initial autoSync state.
49
66
  */
@@ -157,11 +174,15 @@ export class RemoteSyncModule extends BaseChatModule {
157
174
  }
158
175
 
159
176
  console.log("📡 Creating remote session for this terminal session...");
160
- const result = await this.client.createSessionPlaceholder({
161
- title: title.slice(0, 80) || "CLI Session",
162
- workerId: this.workerId,
163
- metadata: { source: "cli", createdAt: new Date().toISOString() },
164
- });
177
+ const result = await withRetry(
178
+ () =>
179
+ this.client.createSessionPlaceholder({
180
+ title: title.slice(0, 80) || "CLI Session",
181
+ workerId: this.workerId,
182
+ metadata: { source: "cli", createdAt: new Date().toISOString() },
183
+ }),
184
+ { maxRetries: 3, backoffMs: 500 }
185
+ );
165
186
 
166
187
  this.remoteSessionId = result.sessionId;
167
188
  console.log(`✅ Remote session created: ${result.sessionId}`);
@@ -177,12 +198,16 @@ export class RemoteSyncModule extends BaseChatModule {
177
198
  agentName?: string,
178
199
  modelName?: string
179
200
  ): Promise<{ messageId: string; taskId: string | undefined }> {
180
- const result = await this.client.createMessagePlaceholder(sessionId, {
181
- content: prompt,
182
- agentName,
183
- modelName,
184
- metadata: { source: "cli", createdAt: new Date().toISOString() },
185
- });
201
+ const result = await withRetry(
202
+ () =>
203
+ this.client.createMessagePlaceholder(sessionId, {
204
+ content: prompt,
205
+ agentName,
206
+ modelName,
207
+ metadata: { source: "cli", createdAt: new Date().toISOString() },
208
+ }),
209
+ { maxRetries: 3, backoffMs: 500 }
210
+ );
186
211
  return { messageId: result.messageId, taskId: result.taskId };
187
212
  }
188
213
 
@@ -273,7 +298,10 @@ export class RemoteSyncModule extends BaseChatModule {
273
298
  try {
274
299
  // Step 1: Ensure remote session exists
275
300
  console.log("\n🔐 Checking KnowHow API credentials...");
276
- const me = await this.client.me();
301
+ const me = await withRetry(() => this.client.me(), {
302
+ maxRetries: 3,
303
+ backoffMs: 500,
304
+ });
277
305
  console.log(`✅ Authenticated as ${me.data?.email || "unknown"}`);
278
306
 
279
307
  const title =
@@ -341,13 +369,13 @@ export class RemoteSyncModule extends BaseChatModule {
341
369
 
342
370
  this.syncedMessageCount++;
343
371
 
344
- const baseUrl = KNOWHOW_API_URL.replace("/api", "").replace(
345
- "api.",
346
- ""
347
- );
372
+ const chatUrl = new URL(KNOWHOW_API_URL);
373
+ chatUrl.hostname = chatUrl.hostname.replace(/^api\./, "");
374
+ chatUrl.pathname = chatUrl.pathname.replace(/\/api\/?$/, "");
375
+ chatUrl.pathname = `${chatUrl.pathname.replace(/\/$/, "")}/chat/${sessionId}`;
348
376
  console.log(
349
377
  `\n💾 Local session updated with remote IDs.\n` +
350
- `🌐 View your task at: ${baseUrl}/chat/${sessionId}`
378
+ `🌐 View your task at: ${chatUrl.toString()}`
351
379
  );
352
380
  } catch (error: any) {
353
381
  console.error(
package/src/cli.ts CHANGED
@@ -63,6 +63,7 @@ import { addAgentsCommand } from "./commands/agents";
63
63
  import { addHashCommand } from "./commands/hash";
64
64
  import { addSandboxCommand } from "./commands/sandbox";
65
65
  import { addProcessesCommand } from "./commands/processes";
66
+ import { addRemoteCommand } from "./commands/remote";
66
67
 
67
68
  // Handle unhandled promise rejections gracefully — particularly from MCP SDK
68
69
  // which fires errors via event emitters that can bypass Promise.allSettled.
@@ -106,6 +107,7 @@ async function main() {
106
107
  // Register all commands
107
108
  addInitCommand(program);
108
109
  addLoginCommand(program);
110
+ addRemoteCommand(program);
109
111
  addUpdateCommand(program);
110
112
  addGenerateCommand(program);
111
113
  addEmbedCommands(program);
@@ -1,5 +1,6 @@
1
1
  import Anthropic from "@anthropic-ai/sdk";
2
2
  import { wait } from "../utils";
3
+ import { emitResponseMetadata } from "./responseMetadata";
3
4
  import { AnthropicTextPricing, AnthropicModels } from "./pricing";
4
5
  import { ContextLimits } from "./contextLimits";
5
6
  import { ModelModality } from "./types";
@@ -27,12 +28,16 @@ type Usage = Anthropic.Usage;
27
28
  type ThinkingParam = Anthropic.Messages.ThinkingConfigParam;
28
29
 
29
30
  const alwaysOnModels: readonly string[] = [
30
- AnthropicModels.Sonnet5, AnthropicModels.Fable5,
31
- AnthropicModels.Mythos5, AnthropicModels.MythosPreview,
31
+ AnthropicModels.Sonnet5,
32
+ AnthropicModels.Fable5,
33
+ AnthropicModels.Mythos5,
34
+ AnthropicModels.MythosPreview,
32
35
  ];
33
36
  const optInModels: readonly string[] = [
34
- AnthropicModels.Opus4_8, AnthropicModels.Opus4_7,
35
- AnthropicModels.Opus4_6, AnthropicModels.Sonnet4_6,
37
+ AnthropicModels.Opus4_8,
38
+ AnthropicModels.Opus4_7,
39
+ AnthropicModels.Opus4_6,
40
+ AnthropicModels.Sonnet4_6,
36
41
  ];
37
42
 
38
43
  export class GenericAnthropicClient implements GenericClient {
@@ -43,10 +48,32 @@ export class GenericAnthropicClient implements GenericClient {
43
48
  this.setKey(apiKey || process.env.ANTHROPIC_API_KEY || "");
44
49
  }
45
50
 
51
+ private emitErrorMetadata(options: CompletionOptions, error: unknown) {
52
+ if (!error || typeof error !== "object") return;
53
+ const apiError = error as {
54
+ status?: number;
55
+ headers?: Headers | Record<string, unknown>;
56
+ request_id?: string;
57
+ requestID?: string;
58
+ response?: Response;
59
+ };
60
+ const statusCode = apiError.status ?? apiError.response?.status;
61
+ const headers = apiError.headers ?? apiError.response?.headers;
62
+ if (typeof statusCode === "number" || headers) {
63
+ emitResponseMetadata(options, {
64
+ statusCode,
65
+ headers,
66
+ requestId: apiError.request_id ?? apiError.requestID,
67
+ });
68
+ }
69
+ }
70
+
46
71
  setKey(apiKey: string) {
47
72
  this.apiKey = apiKey;
48
73
  this.client = new Anthropic({
49
74
  apiKey: apiKey || process.env.ANTHROPIC_API_KEY,
75
+ // Do not hide retry responses from the response-scoped observer.
76
+ maxRetries: 0,
50
77
  });
51
78
  }
52
79
 
@@ -54,7 +81,9 @@ export class GenericAnthropicClient implements GenericClient {
54
81
  const lastTool = tools[tools.length - 1];
55
82
 
56
83
  if (lastTool) {
57
- lastTool.cache_control = longTtl ? { type: "ephemeral", ttl: "1h" } as any : { type: "ephemeral" };
84
+ lastTool.cache_control = longTtl
85
+ ? ({ type: "ephemeral", ttl: "1h" } as any)
86
+ : { type: "ephemeral" };
58
87
  }
59
88
  }
60
89
 
@@ -65,10 +94,12 @@ export class GenericAnthropicClient implements GenericClient {
65
94
  getThinkingParam(
66
95
  model: string,
67
96
  reasoning_effort?: CompletionOptions["reasoning_effort"],
68
- reasoning_summary?: boolean,
97
+ reasoning_summary?: boolean
69
98
  ): ThinkingParam | undefined {
70
99
  const effortIsNone = reasoning_effort === "none";
71
- const withSummary = reasoning_summary ? ({ display: "summarized" } as const) : {};
100
+ const withSummary = reasoning_summary
101
+ ? ({ display: "summarized" } as const)
102
+ : {};
72
103
 
73
104
  if (alwaysOnModels.includes(model)) {
74
105
  return effortIsNone
@@ -112,13 +143,13 @@ export class GenericAnthropicClient implements GenericClient {
112
143
  * Removes unsupported fields like additionalProperties, $ref, $defs, positional.
113
144
  */
114
145
  private cleanSchemaForAnthropic(schema: any): any {
115
- if (!schema || typeof schema !== 'object') {
146
+ if (!schema || typeof schema !== "object") {
116
147
  return schema;
117
148
  }
118
149
 
119
150
  // Handle arrays
120
151
  if (Array.isArray(schema)) {
121
- return schema.map(item => this.cleanSchemaForAnthropic(item));
152
+ return schema.map((item) => this.cleanSchemaForAnthropic(item));
122
153
  }
123
154
 
124
155
  const cleaned: any = {};
@@ -130,10 +161,10 @@ export class GenericAnthropicClient implements GenericClient {
130
161
 
131
162
  // Skip unsupported properties
132
163
  if (
133
- key === 'additionalProperties' ||
134
- key === '$ref' ||
135
- key === '$defs' ||
136
- key === 'positional'
164
+ key === "additionalProperties" ||
165
+ key === "$ref" ||
166
+ key === "$defs" ||
167
+ key === "positional"
137
168
  ) {
138
169
  continue;
139
170
  }
@@ -141,7 +172,7 @@ export class GenericAnthropicClient implements GenericClient {
141
172
  const value = schema[key];
142
173
 
143
174
  // Recursively clean nested objects
144
- if (typeof value === 'object' && value !== null) {
175
+ if (typeof value === "object" && value !== null) {
145
176
  cleaned[key] = this.cleanSchemaForAnthropic(value);
146
177
  }
147
178
  // Copy primitive values as-is
@@ -160,7 +191,9 @@ export class GenericAnthropicClient implements GenericClient {
160
191
  const transformed = tools.map((tool) => ({
161
192
  name: tool.function.name || "",
162
193
  description: tool.function.description || "",
163
- input_schema: this.cleanSchemaForAnthropic(tool.function.parameters) as any,
194
+ input_schema: this.cleanSchemaForAnthropic(
195
+ tool.function.parameters
196
+ ) as any,
164
197
  }));
165
198
 
166
199
  this.handleToolCaching(transformed, longTtl);
@@ -440,7 +473,7 @@ export class GenericAnthropicClient implements GenericClient {
440
473
  const thinkingParam = this.getThinkingParam(
441
474
  options.model,
442
475
  options.reasoning_effort,
443
- options.reasoning_summary,
476
+ options.reasoning_summary
444
477
  );
445
478
  const effortOutputConfig = this.getEffortOutputConfig(
446
479
  options.model,
@@ -456,28 +489,41 @@ export class GenericAnthropicClient implements GenericClient {
456
489
 
457
490
  const tools = this.transformTools(options.tools, longTtl);
458
491
  try {
459
- const response = await this.client.messages.create({
460
- model: options.model,
461
- messages: claudeMessages,
462
- system: systemMessage
463
- ? [
464
- {
465
- text: systemMessage,
466
- cache_control: longTtl ? ({ type: "ephemeral", ttl: "1h" } as any) : { type: "ephemeral" },
467
- type: "text",
492
+ const result = await this.client.messages
493
+ .create(
494
+ {
495
+ model: options.model,
496
+ messages: claudeMessages,
497
+ system: systemMessage
498
+ ? [
499
+ {
500
+ text: systemMessage,
501
+ cache_control: longTtl
502
+ ? ({ type: "ephemeral", ttl: "1h" } as any)
503
+ : { type: "ephemeral" },
504
+ type: "text",
505
+ },
506
+ ]
507
+ : undefined,
508
+ max_tokens: options.max_tokens || 8000,
509
+ ...(thinkingParam && { thinking: thinkingParam }),
510
+ ...(effortOutputConfig && { output_config: effortOutputConfig }),
511
+ ...(tools.length && {
512
+ tool_choice: {
513
+ type: options.tool_choice ?? "auto",
468
514
  },
469
- ]
470
- : undefined,
471
- max_tokens: options.max_tokens || 8000,
472
- ...(thinkingParam && { thinking: thinkingParam }),
473
- ...(effortOutputConfig && { output_config: effortOutputConfig }),
474
- ...(tools.length && {
475
- tool_choice: {
476
- type: options.tool_choice ?? "auto",
515
+ tools,
516
+ }),
477
517
  },
478
- tools,
479
- }),
480
- }, { signal: options.signal });
518
+ { signal: options.signal }
519
+ )
520
+ .withResponse();
521
+ const { data: response, response: nativeResponse, request_id } = result;
522
+ emitResponseMetadata(options, {
523
+ statusCode: nativeResponse.status,
524
+ headers: nativeResponse.headers,
525
+ requestId: request_id,
526
+ });
481
527
 
482
528
  if (!response.content || !response.content.length) {
483
529
  console.log("no content in Anthropic response", response);
@@ -496,10 +542,11 @@ export class GenericAnthropicClient implements GenericClient {
496
542
  ? { provider: "anthropic", items: thinkingBlocks }
497
543
  : undefined;
498
544
  // Extract a human-readable summary from thinking blocks (when present).
499
- const reasoningSummary = thinkingBlocks
500
- .map((b) => ("thinking" in b ? (b as any).thinking : ""))
501
- .filter(Boolean)
502
- .join("\n") || undefined;
545
+ const reasoningSummary =
546
+ thinkingBlocks
547
+ .map((b) => ("thinking" in b ? (b as any).thinking : ""))
548
+ .filter(Boolean)
549
+ .join("\n") || undefined;
503
550
 
504
551
  // Determine which resulting choice should carry the thinking blocks. They
505
552
  // are re-injected before the first tool_use on the next turn, so prefer
@@ -509,16 +556,18 @@ export class GenericAnthropicClient implements GenericClient {
509
556
  const nonThinking = response.content.filter(
510
557
  (c) => c.type !== "thinking" && c.type !== "redacted_thinking"
511
558
  );
512
- const firstToolUseIdx = nonThinking.findIndex((c) => c.type === "tool_use");
559
+ const firstToolUseIdx = nonThinking.findIndex(
560
+ (c) => c.type === "tool_use"
561
+ );
513
562
  const carryIdx = firstToolUseIdx >= 0 ? firstToolUseIdx : 0;
514
563
 
515
564
  return {
516
565
  choices: nonThinking.map((c, idx) => {
517
- // Only the FIRST resulting assistant message carries the thinking
518
- // blocks (Anthropic requires a single thinking block sequence at the
519
- // start of the assistant turn); attaching to every choice would
520
- // duplicate them once the messages are re-transformed.
521
- const carryReasoning = idx === carryIdx;
566
+ // Only the FIRST resulting assistant message carries the thinking
567
+ // blocks (Anthropic requires a single thinking block sequence at the
568
+ // start of the assistant turn); attaching to every choice would
569
+ // duplicate them once the messages are re-transformed.
570
+ const carryReasoning = idx === carryIdx;
522
571
  if (c.type === "tool_use") {
523
572
  return {
524
573
  message: {
@@ -534,8 +583,10 @@ export class GenericAnthropicClient implements GenericClient {
534
583
  },
535
584
  },
536
585
  ],
537
- ...(carryReasoning && reasoningDetails && { _reasoning_details: reasoningDetails }),
538
- ...(carryReasoning && reasoningSummary && { reasoning_summary: reasoningSummary }),
586
+ ...(carryReasoning &&
587
+ reasoningDetails && { _reasoning_details: reasoningDetails }),
588
+ ...(carryReasoning &&
589
+ reasoningSummary && { reasoning_summary: reasoningSummary }),
539
590
  },
540
591
  };
541
592
  } else {
@@ -544,36 +595,48 @@ export class GenericAnthropicClient implements GenericClient {
544
595
  role: "assistant",
545
596
  content: "text" in c ? c.text : c.type,
546
597
  tool_calls: [],
547
- ...(carryReasoning && reasoningDetails && { _reasoning_details: reasoningDetails }),
548
- ...(carryReasoning && reasoningSummary && { reasoning_summary: reasoningSummary }),
598
+ ...(carryReasoning &&
599
+ reasoningDetails && { _reasoning_details: reasoningDetails }),
600
+ ...(carryReasoning &&
601
+ reasoningSummary && { reasoning_summary: reasoningSummary }),
549
602
  },
550
603
  };
551
604
  }
552
605
  }),
553
606
 
554
607
  model: options.model,
555
- usage: response.usage ? {
556
- input_tokens: response.usage.input_tokens ?? 0,
557
- output_tokens: response.usage.output_tokens ?? 0,
558
- prompt_tokens: response.usage.input_tokens ?? 0,
559
- completion_tokens: response.usage.output_tokens ?? 0,
560
- total_tokens: (response.usage.input_tokens ?? 0) + (response.usage.output_tokens ?? 0),
561
- cache_creation_input_tokens: response.usage.cache_creation_input_tokens ?? 0,
562
- cache_read_input_tokens: response.usage.cache_read_input_tokens ?? 0,
563
- ...((response.usage as any).thinking_input_tokens != null ||
564
- (response.usage as any).output_tokens_details?.thinking_tokens != null
565
- ? {
566
- output_tokens_details: {
567
- thinking_tokens:
568
- (response.usage as any).thinking_input_tokens ??
569
- (response.usage as any).output_tokens_details?.thinking_tokens ?? 0,
570
- },
571
- }
572
- : {}),
573
- } : undefined,
608
+ usage: response.usage
609
+ ? {
610
+ input_tokens: response.usage.input_tokens ?? 0,
611
+ output_tokens: response.usage.output_tokens ?? 0,
612
+ prompt_tokens: response.usage.input_tokens ?? 0,
613
+ completion_tokens: response.usage.output_tokens ?? 0,
614
+ total_tokens:
615
+ (response.usage.input_tokens ?? 0) +
616
+ (response.usage.output_tokens ?? 0),
617
+ cache_creation_input_tokens:
618
+ response.usage.cache_creation_input_tokens ?? 0,
619
+ cache_read_input_tokens:
620
+ response.usage.cache_read_input_tokens ?? 0,
621
+ ...((response.usage as any).thinking_input_tokens != null ||
622
+ (response.usage as any).output_tokens_details?.thinking_tokens !=
623
+ null
624
+ ? {
625
+ output_tokens_details: {
626
+ thinking_tokens:
627
+ (response.usage as any).thinking_input_tokens ??
628
+ (response.usage as any).output_tokens_details
629
+ ?.thinking_tokens ??
630
+ 0,
631
+ },
632
+ }
633
+ : {}),
634
+ }
635
+ : undefined,
574
636
  usd_cost: this.calculateCost(options.model, response.usage),
575
637
  };
576
638
  } catch (err) {
639
+ this.emitErrorMetadata(options, err);
577
640
  if ("headers" in err && err.headers?.["x-should-retry"] === "true") {
578
641
  console.warn("Retrying failed request", err);
579
642
  await wait(2500);
@@ -589,25 +652,37 @@ export class GenericAnthropicClient implements GenericClient {
589
652
  return AnthropicTextPricing;
590
653
  }
591
654
 
592
- getPricing(model?: string): import("./pricing/types").ModelPricing | Record<string, import("./pricing/types").ModelPricing> | undefined {
655
+ getPricing(
656
+ model?: string
657
+ ):
658
+ | import("./pricing/types").ModelPricing
659
+ | Record<string, import("./pricing/types").ModelPricing>
660
+ | undefined {
593
661
  if (model !== undefined) {
594
- return AnthropicTextPricing[model as keyof typeof AnthropicTextPricing] as import("./pricing/types").ModelPricing | undefined;
662
+ return AnthropicTextPricing[
663
+ model as keyof typeof AnthropicTextPricing
664
+ ] as import("./pricing/types").ModelPricing | undefined;
595
665
  }
596
- return AnthropicTextPricing as unknown as Record<string, import("./pricing/types").ModelPricing>;
666
+ return AnthropicTextPricing as unknown as Record<
667
+ string,
668
+ import("./pricing/types").ModelPricing
669
+ >;
597
670
  }
598
671
 
599
672
  calculateCost(model: string, usage: Usage): number | undefined {
600
673
  const rawP = this.pricesPerMillion()[model];
601
674
  // Fall back to pricing file for unknown/newer models
602
- const fallback = AnthropicTextPricing[model as keyof typeof AnthropicTextPricing];
675
+ const fallback =
676
+ AnthropicTextPricing[model as keyof typeof AnthropicTextPricing];
603
677
  // Second fallback: short aliases like "claude-sonnet-4-5" don't have their
604
678
  // own entry but map to a dated version like "claude-sonnet-4-5-20250929".
605
679
  // Find the first pricing entry whose key starts with the short model name.
606
- const aliasFallback = !rawP && !fallback
607
- ? Object.entries(AnthropicTextPricing).find(([key]) =>
608
- key.startsWith(model + "-")
609
- )?.[1]
610
- : undefined;
680
+ const aliasFallback =
681
+ !rawP && !fallback
682
+ ? Object.entries(AnthropicTextPricing).find(([key]) =>
683
+ key.startsWith(model + "-")
684
+ )?.[1]
685
+ : undefined;
611
686
  const p: any = rawP || fallback || aliasFallback || undefined;
612
687
  if (!p) return undefined;
613
688
 
@@ -663,7 +738,9 @@ export class GenericAnthropicClient implements GenericClient {
663
738
  throw new Error("Anthropic does not support audio transcription");
664
739
  }
665
740
 
666
- getContextLimit(model: string): { contextLimit: number; threshold: number } | undefined {
741
+ getContextLimit(
742
+ model: string
743
+ ): { contextLimit: number; threshold: number } | undefined {
667
744
  const contextLimit = ContextLimits[model];
668
745
  if (contextLimit === undefined) return undefined;
669
746
  const pricing = AnthropicTextPricing[model];
@@ -0,0 +1,53 @@
1
+ import packageJson from "../../package.json";
2
+
3
+ /**
4
+ * Default Knowhow attribution headers.
5
+ *
6
+ * - `HTTP-Referer` – OpenRouter analytics / billing attribution.
7
+ * - `X-Title` – Generic app-name header; supported by OpenRouter
8
+ * and other providers that follow the same convention.
9
+ * - `X-OpenRouter-Title` – Legacy OpenRouter-specific alias (kept for
10
+ * back-compat; `X-Title` is preferred going forward).
11
+ * - `User-Agent` – Standard SDK identification.
12
+ */
13
+ export const KNOWHOW_ATTRIBUTION_HEADERS: Readonly<Record<string, string>> = Object.freeze({
14
+ "HTTP-Referer": "https://knowhow.tyvm.ai",
15
+ "X-Title": "Knowhow",
16
+ "X-OpenRouter-Title": "Knowhow",
17
+ "User-Agent": `knowhow/${packageJson.version}`,
18
+ });
19
+
20
+ /**
21
+ * Adds Knowhow's application attribution to an outbound OpenRouter request.
22
+ * Later header sets win case-insensitively, so callers can override defaults
23
+ * without producing duplicate keys.
24
+ */
25
+ export function withKnowhowAttribution(
26
+ /**
27
+ * Common overrides:
28
+ * - `"X-Title"` / `"X-OpenRouter-Title"` – set your own app name
29
+ * - `"HTTP-Referer"` – set your own referer URL
30
+ * - any additional custom headers are merged in as-is
31
+ */
32
+ ...headerSets: Array<Record<string, string> | undefined>
33
+ ): Record<string, string> {
34
+ const result: Record<string, string> = { ...KNOWHOW_ATTRIBUTION_HEADERS };
35
+
36
+ for (const headers of headerSets) {
37
+ if (!headers) continue;
38
+ for (const [key, value] of Object.entries(headers)) {
39
+ const existingKey = Object.keys(result).find(
40
+ (candidate) => candidate.toLowerCase() === key.toLowerCase()
41
+ );
42
+ if (existingKey) {
43
+ // Keep the original casing so SDK-owned defaults such as User-Agent
44
+ // are replaced rather than duplicated in their intermediate objects.
45
+ result[existingKey] = value;
46
+ } else {
47
+ result[key] = value;
48
+ }
49
+ }
50
+ }
51
+
52
+ return result;
53
+ }