@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.
- package/package.json +1 -1
- package/src/agents/base/base.ts +15 -5
- package/src/chat/modules/AgentModule.ts +3 -3
- package/src/chat/modules/RemoteSyncModule.ts +45 -17
- package/src/clients/anthropic.ts +156 -79
- package/src/clients/attribution.ts +53 -0
- package/src/clients/http.ts +211 -84
- package/src/clients/index.ts +21 -9
- package/src/clients/openai.ts +441 -220
- package/src/clients/openrouter.ts +10 -3
- package/src/clients/pricing/openai.ts +15 -1
- package/src/clients/pricing/types.ts +15 -0
- package/src/clients/responseMetadata.ts +221 -0
- package/src/clients/types.ts +49 -4
- package/tests/unit/clients/anthropic.responseMetadata.test.ts +81 -0
- package/tests/unit/clients/attribution.test.ts +185 -0
- package/tests/unit/clients/http.responseMetadata.test.ts +86 -0
- package/tests/unit/clients/openai.responseMetadata.test.ts +90 -0
- package/tests/unit/clients/responseMetadata.test.ts +123 -0
- package/ts_build/package.json +1 -1
- package/ts_build/src/agents/base/base.js +14 -4
- package/ts_build/src/agents/base/base.js.map +1 -1
- package/ts_build/src/chat/modules/AgentModule.js +3 -3
- package/ts_build/src/chat/modules/AgentModule.js.map +1 -1
- package/ts_build/src/chat/modules/RemoteSyncModule.d.ts +1 -0
- package/ts_build/src/chat/modules/RemoteSyncModule.js +19 -7
- package/ts_build/src/chat/modules/RemoteSyncModule.js.map +1 -1
- package/ts_build/src/clients/anthropic.d.ts +1 -0
- package/ts_build/src/clients/anthropic.js +83 -38
- package/ts_build/src/clients/anthropic.js.map +1 -1
- package/ts_build/src/clients/attribution.d.ts +2 -0
- package/ts_build/src/clients/attribution.js +32 -0
- package/ts_build/src/clients/attribution.js.map +1 -0
- package/ts_build/src/clients/http.js +100 -20
- package/ts_build/src/clients/http.js.map +1 -1
- package/ts_build/src/clients/index.d.ts +2 -1
- package/ts_build/src/clients/index.js +6 -2
- package/ts_build/src/clients/index.js.map +1 -1
- package/ts_build/src/clients/openai.d.ts +2 -1
- package/ts_build/src/clients/openai.js +225 -99
- package/ts_build/src/clients/openai.js.map +1 -1
- package/ts_build/src/clients/openrouter.d.ts +2 -2
- package/ts_build/src/clients/openrouter.js +6 -2
- package/ts_build/src/clients/openrouter.js.map +1 -1
- package/ts_build/src/clients/pricing/openai.js +15 -1
- package/ts_build/src/clients/pricing/openai.js.map +1 -1
- package/ts_build/src/clients/pricing/types.d.ts +6 -0
- package/ts_build/src/clients/pricing/types.js.map +1 -1
- package/ts_build/src/clients/responseMetadata.d.ts +15 -0
- package/ts_build/src/clients/responseMetadata.js +141 -0
- package/ts_build/src/clients/responseMetadata.js.map +1 -0
- package/ts_build/src/clients/types.d.ts +18 -1
- package/ts_build/src/clients/types.js +2 -1
- package/ts_build/src/clients/types.js.map +1 -1
- package/ts_build/tests/unit/clients/anthropic.responseMetadata.test.d.ts +1 -0
- package/ts_build/tests/unit/clients/anthropic.responseMetadata.test.js +77 -0
- package/ts_build/tests/unit/clients/anthropic.responseMetadata.test.js.map +1 -0
- package/ts_build/tests/unit/clients/attribution.test.d.ts +1 -0
- package/ts_build/tests/unit/clients/attribution.test.js +153 -0
- package/ts_build/tests/unit/clients/attribution.test.js.map +1 -0
- package/ts_build/tests/unit/clients/http.responseMetadata.test.d.ts +1 -0
- package/ts_build/tests/unit/clients/http.responseMetadata.test.js +77 -0
- package/ts_build/tests/unit/clients/http.responseMetadata.test.js.map +1 -0
- package/ts_build/tests/unit/clients/openai.responseMetadata.test.d.ts +1 -0
- package/ts_build/tests/unit/clients/openai.responseMetadata.test.js +86 -0
- package/ts_build/tests/unit/clients/openai.responseMetadata.test.js.map +1 -0
- package/ts_build/tests/unit/clients/responseMetadata.test.d.ts +1 -0
- package/ts_build/tests/unit/clients/responseMetadata.test.js +96 -0
- package/ts_build/tests/unit/clients/responseMetadata.test.js.map +1 -0
package/package.json
CHANGED
package/src/agents/base/base.ts
CHANGED
|
@@ -1307,10 +1307,19 @@ export abstract class BaseAgent implements IAgent {
|
|
|
1307
1307
|
this.logStatus();
|
|
1308
1308
|
|
|
1309
1309
|
const continuation = `<Workflow>
|
|
1310
|
-
|
|
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
|
-
)}
|
|
1313
|
-
|
|
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 = `
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
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
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
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
|
|
345
|
-
|
|
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: ${
|
|
378
|
+
`🌐 View your task at: ${chatUrl.toString()}`
|
|
351
379
|
);
|
|
352
380
|
} catch (error: any) {
|
|
353
381
|
console.error(
|
package/src/clients/anthropic.ts
CHANGED
|
@@ -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,
|
|
31
|
-
AnthropicModels.
|
|
31
|
+
AnthropicModels.Sonnet5,
|
|
32
|
+
AnthropicModels.Fable5,
|
|
33
|
+
AnthropicModels.Mythos5,
|
|
34
|
+
AnthropicModels.MythosPreview,
|
|
32
35
|
];
|
|
33
36
|
const optInModels: readonly string[] = [
|
|
34
|
-
AnthropicModels.Opus4_8,
|
|
35
|
-
AnthropicModels.
|
|
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
|
|
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
|
|
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 !==
|
|
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 ===
|
|
134
|
-
key ===
|
|
135
|
-
key ===
|
|
136
|
-
key ===
|
|
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 ===
|
|
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(
|
|
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
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
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
|
-
|
|
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
|
-
|
|
479
|
-
|
|
480
|
-
|
|
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 =
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
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(
|
|
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
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
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 &&
|
|
538
|
-
|
|
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 &&
|
|
548
|
-
|
|
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
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
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(
|
|
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[
|
|
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<
|
|
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 =
|
|
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 =
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
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(
|
|
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
|
+
}
|