@tyvm/knowhow 0.0.156 → 0.0.157

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 (69) hide show
  1. package/package.json +1 -1
  2. package/src/agents/base/base.ts +15 -5
  3. package/src/chat/modules/AgentModule.ts +3 -3
  4. package/src/chat/modules/RemoteSyncModule.ts +45 -17
  5. package/src/clients/anthropic.ts +156 -79
  6. package/src/clients/attribution.ts +53 -0
  7. package/src/clients/http.ts +211 -84
  8. package/src/clients/index.ts +21 -9
  9. package/src/clients/openai.ts +441 -220
  10. package/src/clients/openrouter.ts +10 -3
  11. package/src/clients/pricing/openai.ts +15 -1
  12. package/src/clients/pricing/types.ts +15 -0
  13. package/src/clients/responseMetadata.ts +221 -0
  14. package/src/clients/types.ts +49 -4
  15. package/tests/unit/clients/anthropic.responseMetadata.test.ts +81 -0
  16. package/tests/unit/clients/attribution.test.ts +185 -0
  17. package/tests/unit/clients/http.responseMetadata.test.ts +86 -0
  18. package/tests/unit/clients/openai.responseMetadata.test.ts +90 -0
  19. package/tests/unit/clients/responseMetadata.test.ts +123 -0
  20. package/ts_build/package.json +1 -1
  21. package/ts_build/src/agents/base/base.js +14 -4
  22. package/ts_build/src/agents/base/base.js.map +1 -1
  23. package/ts_build/src/chat/modules/AgentModule.js +3 -3
  24. package/ts_build/src/chat/modules/AgentModule.js.map +1 -1
  25. package/ts_build/src/chat/modules/RemoteSyncModule.d.ts +1 -0
  26. package/ts_build/src/chat/modules/RemoteSyncModule.js +19 -7
  27. package/ts_build/src/chat/modules/RemoteSyncModule.js.map +1 -1
  28. package/ts_build/src/clients/anthropic.d.ts +1 -0
  29. package/ts_build/src/clients/anthropic.js +83 -38
  30. package/ts_build/src/clients/anthropic.js.map +1 -1
  31. package/ts_build/src/clients/attribution.d.ts +2 -0
  32. package/ts_build/src/clients/attribution.js +32 -0
  33. package/ts_build/src/clients/attribution.js.map +1 -0
  34. package/ts_build/src/clients/http.js +100 -20
  35. package/ts_build/src/clients/http.js.map +1 -1
  36. package/ts_build/src/clients/index.d.ts +2 -1
  37. package/ts_build/src/clients/index.js +6 -2
  38. package/ts_build/src/clients/index.js.map +1 -1
  39. package/ts_build/src/clients/openai.d.ts +2 -1
  40. package/ts_build/src/clients/openai.js +225 -99
  41. package/ts_build/src/clients/openai.js.map +1 -1
  42. package/ts_build/src/clients/openrouter.d.ts +2 -2
  43. package/ts_build/src/clients/openrouter.js +6 -2
  44. package/ts_build/src/clients/openrouter.js.map +1 -1
  45. package/ts_build/src/clients/pricing/openai.js +15 -1
  46. package/ts_build/src/clients/pricing/openai.js.map +1 -1
  47. package/ts_build/src/clients/pricing/types.d.ts +6 -0
  48. package/ts_build/src/clients/pricing/types.js.map +1 -1
  49. package/ts_build/src/clients/responseMetadata.d.ts +15 -0
  50. package/ts_build/src/clients/responseMetadata.js +141 -0
  51. package/ts_build/src/clients/responseMetadata.js.map +1 -0
  52. package/ts_build/src/clients/types.d.ts +18 -1
  53. package/ts_build/src/clients/types.js +2 -1
  54. package/ts_build/src/clients/types.js.map +1 -1
  55. package/ts_build/tests/unit/clients/anthropic.responseMetadata.test.d.ts +1 -0
  56. package/ts_build/tests/unit/clients/anthropic.responseMetadata.test.js +77 -0
  57. package/ts_build/tests/unit/clients/anthropic.responseMetadata.test.js.map +1 -0
  58. package/ts_build/tests/unit/clients/attribution.test.d.ts +1 -0
  59. package/ts_build/tests/unit/clients/attribution.test.js +153 -0
  60. package/ts_build/tests/unit/clients/attribution.test.js.map +1 -0
  61. package/ts_build/tests/unit/clients/http.responseMetadata.test.d.ts +1 -0
  62. package/ts_build/tests/unit/clients/http.responseMetadata.test.js +77 -0
  63. package/ts_build/tests/unit/clients/http.responseMetadata.test.js.map +1 -0
  64. package/ts_build/tests/unit/clients/openai.responseMetadata.test.d.ts +1 -0
  65. package/ts_build/tests/unit/clients/openai.responseMetadata.test.js +86 -0
  66. package/ts_build/tests/unit/clients/openai.responseMetadata.test.js.map +1 -0
  67. package/ts_build/tests/unit/clients/responseMetadata.test.d.ts +1 -0
  68. package/ts_build/tests/unit/clients/responseMetadata.test.js +96 -0
  69. package/ts_build/tests/unit/clients/responseMetadata.test.js.map +1 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tyvm/knowhow",
3
- "version": "0.0.156",
3
+ "version": "0.0.157",
4
4
  "description": "ai cli with plugins and agents",
5
5
  "main": "ts_build/src/index.js",
6
6
  "bin": {
@@ -1307,10 +1307,19 @@ export abstract class BaseAgent implements IAgent {
1307
1307
  this.logStatus();
1308
1308
 
1309
1309
  const continuation = `<Workflow>
1310
- Task terminates after you call on of these tools: ${JSON.stringify(
1310
+ This is an automated runtime instruction, not a new user message.
1311
+ The original user request remains active. Do not say that the user's
1312
+ message was empty, and do not ask the user to repeat the request.
1313
+
1314
+ Your previous response did not terminate the task because it did not
1315
+ call one of the required tools: ${JSON.stringify(
1311
1316
  this.requiredToolNames
1312
- )}.\n
1313
- User likely only sees output from the required tool call.
1317
+ )}.
1318
+ If your previous response was intended as the final response, call the
1319
+ appropriate required tool now with that response. Otherwise, continue
1320
+ working on the original request and call an appropriate required tool
1321
+ when the task is complete.
1322
+
1314
1323
  <TaskStatus>${statusMessage}</TaskStatus>
1315
1324
  </Workflow>`;
1316
1325
 
@@ -1680,7 +1689,7 @@ export abstract class BaseAgent implements IAgent {
1680
1689
  }
1681
1690
 
1682
1691
  const taskPrompt = `
1683
- Analyze all previous messages.
1692
+ TASK INTERRUPT. CONTEXT COMPACTION NEEDED. Analyze all previous messages.
1684
1693
 
1685
1694
  Generate a detailed task breakdown for this conversation, include a section for the following:
1686
1695
  1. Task List
@@ -1737,7 +1746,8 @@ export abstract class BaseAgent implements IAgent {
1737
1746
  this.log(
1738
1747
  `Compressing messages from 0 to ${compressionEnd}, resuming from ${resumeIndex}, total messages: ${messages.length}`
1739
1748
  );
1740
- const toCompressPrompt = `We are compressing our conversation to save memory.
1749
+ const toCompressPrompt = `TASK INTERRUPT. CONTEXT COMPACTION NEEDED.
1750
+ We are compressing our conversation to save memory.
1741
1751
  Please summarize the conversation so far, so that we may continue the original task with a smaller context
1742
1752
 
1743
1753
  Include the following sections:
@@ -810,7 +810,7 @@ export class AgentModule extends BaseChatModule {
810
810
  // Match new interactive threads: local-only sessions get a fresh remote
811
811
  // task when auto-sync is enabled.
812
812
  if (!remoteTaskId && this.remoteSyncModule?.isAutoSyncEnabled()) {
813
- await this.remoteSyncModule.syncTask(taskId);
813
+ this.remoteSyncModule.syncTaskInBackground(taskId);
814
814
  }
815
815
 
816
816
  // After resume finishes, revert to normal chat (non-agent mode) so the
@@ -1471,7 +1471,7 @@ export class AgentModule extends BaseChatModule {
1471
1471
  !remoteTaskId &&
1472
1472
  this.remoteSyncModule?.isAutoSyncEnabled()
1473
1473
  ) {
1474
- await this.remoteSyncModule.syncTask(result.taskId);
1474
+ this.remoteSyncModule.syncTaskInBackground(result.taskId);
1475
1475
  }
1476
1476
 
1477
1477
  await this.attachedAgentChatLoop(
@@ -1566,7 +1566,7 @@ export class AgentModule extends BaseChatModule {
1566
1566
 
1567
1567
  // If auto-sync is enabled, push this task to the remote KnowHow app
1568
1568
  if (this.remoteSyncModule?.isAutoSyncEnabled()) {
1569
- await this.remoteSyncModule.syncTask(taskId);
1569
+ this.remoteSyncModule.syncTaskInBackground(taskId);
1570
1570
  }
1571
1571
 
1572
1572
  await this.attachedAgentChatLoop(taskId, agent, formattedPrompt);
@@ -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(
@@ -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
+ }