profclaw 2.3.0 → 2.4.0
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/CHANGELOG.md +15 -0
- package/README.md +5 -2
- package/config/pricing.yml +26 -2
- package/dist/agents/command-sandbox.d.ts +93 -0
- package/dist/agents/command-sandbox.js +235 -0
- package/dist/agents/executor-runner.d.ts +135 -0
- package/dist/agents/executor-runner.js +303 -0
- package/dist/agents/executor.d.ts +2 -0
- package/dist/agents/executor.js +35 -2
- package/dist/agents/output-truncator.d.ts +59 -0
- package/dist/agents/output-truncator.js +128 -0
- package/dist/agents/receipt/receipt-cli.d.ts +43 -0
- package/dist/agents/receipt/receipt-cli.js +82 -0
- package/dist/agents/receipt/receipt-types.d.ts +136 -0
- package/dist/agents/receipt/receipt-types.js +19 -0
- package/dist/agents/receipt/receipt-verify.d.ts +19 -0
- package/dist/agents/receipt/receipt-verify.js +176 -0
- package/dist/agents/receipt/receipt-view.d.ts +29 -0
- package/dist/agents/receipt/receipt-view.js +227 -0
- package/dist/agents/receipt/receipt-writer.d.ts +47 -0
- package/dist/agents/receipt/receipt-writer.js +186 -0
- package/dist/agents/result-store.d.ts +6 -0
- package/dist/agents/result-store.js +43 -2
- package/dist/agents/session-diff.d.ts +6 -0
- package/dist/agents/session-diff.js +10 -19
- package/dist/agents/tool-selector.d.ts +123 -0
- package/dist/agents/tool-selector.js +321 -0
- package/dist/agents/verified-run-git.d.ts +49 -0
- package/dist/agents/verified-run-git.js +82 -0
- package/dist/agents/verified-run.d.ts +149 -0
- package/dist/agents/verified-run.js +402 -0
- package/dist/agents/verifier.d.ts +64 -0
- package/dist/agents/verifier.js +186 -0
- package/dist/agents/worktree-tools.d.ts +47 -0
- package/dist/agents/worktree-tools.js +246 -0
- package/dist/chat/agentic-executor.js +14 -6
- package/dist/chat/execution/sandbox.d.ts +8 -0
- package/dist/chat/execution/sandbox.js +25 -1
- package/dist/chat/execution/tools/tts-speak.d.ts +2 -2
- package/dist/chat/meta-tool-handlers.d.ts +18 -0
- package/dist/chat/meta-tool-handlers.js +51 -0
- package/dist/chat/system-prompts.js +6 -1
- package/dist/chat/tool-handler.d.ts +2 -0
- package/dist/chat/tool-handler.js +47 -3
- package/dist/cli/commands/receipt.d.ts +7 -0
- package/dist/cli/commands/receipt.js +90 -0
- package/dist/cli/commands/run.d.ts +28 -0
- package/dist/cli/commands/run.js +204 -0
- package/dist/cli/index.js +4 -0
- package/dist/costs/token-tracker.d.ts +10 -1
- package/dist/costs/token-tracker.js +37 -5
- package/dist/providers/ai-sdk.js +33 -37
- package/dist/providers/core/models.js +59 -17
- package/dist/providers/core/types.d.ts +4 -0
- package/dist/providers/prompt-cache.d.ts +68 -0
- package/dist/providers/prompt-cache.js +165 -0
- package/dist/providers/smart-router.js +11 -3
- package/dist/routes/chat.js +22 -5
- package/dist/server.js +3 -0
- package/package.json +1 -1
package/dist/providers/ai-sdk.js
CHANGED
|
@@ -6,13 +6,14 @@
|
|
|
6
6
|
*
|
|
7
7
|
* @see https://sdk.vercel.ai/docs
|
|
8
8
|
*/
|
|
9
|
-
import { generateText, streamText, tool as createTool, jsonSchema } from 'ai';
|
|
9
|
+
import { generateText, streamText, stepCountIs, tool as createTool, jsonSchema } from 'ai';
|
|
10
10
|
// Provider SDKs are lazily imported inside ensureProvider() to reduce startup memory
|
|
11
11
|
// (each SDK is ~15MB; loading all 6 at startup costs ~100MB even if unused)
|
|
12
12
|
import { zodToJsonSchema } from 'zod-to-json-schema';
|
|
13
13
|
import { randomUUID } from 'node:crypto';
|
|
14
14
|
import { logger } from '../utils/logger.js';
|
|
15
15
|
import { normalizeToolSchema } from './schema-utils.js';
|
|
16
|
+
import { aggregateStepUsage, applyAnthropicMessageCache, applyAnthropicToolCache, calculateCachedInputCost, getCacheConfigForModel, extractCacheUsage, } from './prompt-cache.js';
|
|
16
17
|
// AI provider resilience settings (configurable via env)
|
|
17
18
|
const AI_TIMEOUT_MS = parseInt(process.env['AI_PROVIDER_TIMEOUT_MS'] ?? '120000', 10);
|
|
18
19
|
const AI_MAX_RETRIES = parseInt(process.env['AI_MAX_RETRIES'] ?? '2', 10);
|
|
@@ -866,7 +867,7 @@ class AIProviderManager {
|
|
|
866
867
|
try {
|
|
867
868
|
const result = await withRetry(() => generateText({
|
|
868
869
|
model,
|
|
869
|
-
messages,
|
|
870
|
+
messages: applyAnthropicMessageCache(messages, modelRef.provider),
|
|
870
871
|
temperature: request.temperature ?? 0.7,
|
|
871
872
|
maxOutputTokens: request.maxTokens ?? 4096,
|
|
872
873
|
abortSignal: AbortSignal.timeout(AI_TIMEOUT_MS),
|
|
@@ -874,12 +875,9 @@ class AIProviderManager {
|
|
|
874
875
|
const duration = Date.now() - startTime;
|
|
875
876
|
const modelInfo = this.getModelInfo(modelRef.model);
|
|
876
877
|
// Calculate cost - AI SDK v6 uses usage.totalTokens and similar
|
|
877
|
-
const promptTokens = result.usage
|
|
878
|
-
result.usage?.inputTokens ?? 0;
|
|
879
|
-
const completionTokens = result.usage?.completionTokens ??
|
|
880
|
-
result.usage?.outputTokens ?? 0;
|
|
878
|
+
const { promptTokens, completionTokens, cacheReadTokens, cacheWriteTokens } = extractCacheUsage(result.usage);
|
|
881
879
|
const cost = modelInfo
|
|
882
|
-
? (promptTokens
|
|
880
|
+
? calculateCachedInputCost(promptTokens, cacheReadTokens, cacheWriteTokens, modelInfo.costPer1MInput, getCacheConfigForModel(modelInfo.id)) +
|
|
883
881
|
(completionTokens / 1_000_000) * modelInfo.costPer1MOutput
|
|
884
882
|
: 0;
|
|
885
883
|
return {
|
|
@@ -893,6 +891,8 @@ class AIProviderManager {
|
|
|
893
891
|
completionTokens,
|
|
894
892
|
totalTokens: promptTokens + completionTokens,
|
|
895
893
|
cost,
|
|
894
|
+
cacheReadTokens,
|
|
895
|
+
cacheWriteTokens,
|
|
896
896
|
},
|
|
897
897
|
duration,
|
|
898
898
|
};
|
|
@@ -923,7 +923,7 @@ class AIProviderManager {
|
|
|
923
923
|
try {
|
|
924
924
|
const result = streamText({
|
|
925
925
|
model,
|
|
926
|
-
messages,
|
|
926
|
+
messages: applyAnthropicMessageCache(messages, modelRef.provider),
|
|
927
927
|
temperature: request.temperature ?? 0.7,
|
|
928
928
|
maxOutputTokens: request.maxTokens ?? 4096,
|
|
929
929
|
abortSignal: AbortSignal.timeout(AI_TIMEOUT_MS),
|
|
@@ -953,12 +953,9 @@ class AIProviderManager {
|
|
|
953
953
|
const usage = await result.usage;
|
|
954
954
|
const duration = Date.now() - startTime;
|
|
955
955
|
const modelInfo = this.getModelInfo(modelRef.model);
|
|
956
|
-
const promptTokens = usage
|
|
957
|
-
usage?.inputTokens ?? 0;
|
|
958
|
-
const completionTokens = usage?.completionTokens ??
|
|
959
|
-
usage?.outputTokens ?? 0;
|
|
956
|
+
const { promptTokens, completionTokens, cacheReadTokens, cacheWriteTokens } = extractCacheUsage(usage);
|
|
960
957
|
const cost = modelInfo
|
|
961
|
-
? (promptTokens
|
|
958
|
+
? calculateCachedInputCost(promptTokens, cacheReadTokens, cacheWriteTokens, modelInfo.costPer1MInput, getCacheConfigForModel(modelInfo.id)) +
|
|
962
959
|
(completionTokens / 1_000_000) * modelInfo.costPer1MOutput
|
|
963
960
|
: 0;
|
|
964
961
|
return {
|
|
@@ -972,6 +969,8 @@ class AIProviderManager {
|
|
|
972
969
|
completionTokens,
|
|
973
970
|
totalTokens: promptTokens + completionTokens,
|
|
974
971
|
cost,
|
|
972
|
+
cacheReadTokens,
|
|
973
|
+
cacheWriteTokens,
|
|
975
974
|
},
|
|
976
975
|
duration,
|
|
977
976
|
};
|
|
@@ -1039,10 +1038,10 @@ class AIProviderManager {
|
|
|
1039
1038
|
try {
|
|
1040
1039
|
const result = await withRetry(() => generateText({
|
|
1041
1040
|
model,
|
|
1042
|
-
messages,
|
|
1041
|
+
messages: applyAnthropicMessageCache(messages, modelRef.provider),
|
|
1043
1042
|
temperature: request.temperature ?? 0.7,
|
|
1044
1043
|
maxOutputTokens: request.maxTokens ?? 4096,
|
|
1045
|
-
tools: useNativeTools ? aiSdkTools : undefined,
|
|
1044
|
+
tools: useNativeTools ? applyAnthropicToolCache(aiSdkTools, modelRef.provider) : undefined,
|
|
1046
1045
|
abortSignal: AbortSignal.timeout(AI_TIMEOUT_MS),
|
|
1047
1046
|
}));
|
|
1048
1047
|
let finalContent = result.text;
|
|
@@ -1101,12 +1100,9 @@ class AIProviderManager {
|
|
|
1101
1100
|
}
|
|
1102
1101
|
const duration = Date.now() - startTime;
|
|
1103
1102
|
const modelInfo = this.getModelInfo(modelRef.model);
|
|
1104
|
-
const promptTokens = result.usage
|
|
1105
|
-
result.usage?.inputTokens ?? 0;
|
|
1106
|
-
const completionTokens = result.usage?.completionTokens ??
|
|
1107
|
-
result.usage?.outputTokens ?? 0;
|
|
1103
|
+
const { promptTokens, completionTokens, cacheReadTokens, cacheWriteTokens } = extractCacheUsage(result.usage);
|
|
1108
1104
|
const cost = modelInfo
|
|
1109
|
-
? (promptTokens
|
|
1105
|
+
? calculateCachedInputCost(promptTokens, cacheReadTokens, cacheWriteTokens, modelInfo.costPer1MInput, getCacheConfigForModel(modelInfo.id)) +
|
|
1110
1106
|
(completionTokens / 1_000_000) * modelInfo.costPer1MOutput
|
|
1111
1107
|
: 0;
|
|
1112
1108
|
return {
|
|
@@ -1120,6 +1116,8 @@ class AIProviderManager {
|
|
|
1120
1116
|
completionTokens,
|
|
1121
1117
|
totalTokens: promptTokens + completionTokens,
|
|
1122
1118
|
cost,
|
|
1119
|
+
cacheReadTokens,
|
|
1120
|
+
cacheWriteTokens,
|
|
1123
1121
|
},
|
|
1124
1122
|
duration,
|
|
1125
1123
|
toolCalls: detectedToolCalls.length > 0 ? detectedToolCalls : undefined,
|
|
@@ -1293,22 +1291,19 @@ class AIProviderManager {
|
|
|
1293
1291
|
try {
|
|
1294
1292
|
const result = await withRetry(() => generateText({
|
|
1295
1293
|
model,
|
|
1296
|
-
messages,
|
|
1297
|
-
tools: Object.keys(tools).length > 0 ? tools : undefined,
|
|
1294
|
+
messages: applyAnthropicMessageCache(messages, modelRef.provider),
|
|
1295
|
+
tools: Object.keys(tools).length > 0 ? applyAnthropicToolCache(tools, modelRef.provider) : undefined,
|
|
1298
1296
|
temperature: request.temperature ?? 0.7,
|
|
1299
1297
|
maxOutputTokens: request.maxTokens ?? 4096,
|
|
1300
|
-
|
|
1298
|
+
stopWhen: stepCountIs(request.maxToolRoundtrips ?? 5),
|
|
1301
1299
|
abortSignal: AbortSignal.timeout(AI_TIMEOUT_MS),
|
|
1302
1300
|
}));
|
|
1303
1301
|
const duration = Date.now() - startTime;
|
|
1304
1302
|
const modelInfo = this.getModelInfo(modelRef.model);
|
|
1305
|
-
// Calculate usage
|
|
1306
|
-
const promptTokens = result.usage
|
|
1307
|
-
result.usage?.inputTokens ?? 0;
|
|
1308
|
-
const completionTokens = result.usage?.completionTokens ??
|
|
1309
|
-
result.usage?.outputTokens ?? 0;
|
|
1303
|
+
// Calculate usage, summed across all steps (result.usage is last-step only)
|
|
1304
|
+
const { promptTokens, completionTokens, cacheReadTokens, cacheWriteTokens } = aggregateStepUsage(result.steps, result.usage);
|
|
1310
1305
|
const cost = modelInfo
|
|
1311
|
-
? (promptTokens
|
|
1306
|
+
? calculateCachedInputCost(promptTokens, cacheReadTokens, cacheWriteTokens, modelInfo.costPer1MInput, getCacheConfigForModel(modelInfo.id)) +
|
|
1312
1307
|
(completionTokens / 1_000_000) * modelInfo.costPer1MOutput
|
|
1313
1308
|
: 0;
|
|
1314
1309
|
// Check if any tool calls require approval
|
|
@@ -1328,6 +1323,8 @@ class AIProviderManager {
|
|
|
1328
1323
|
completionTokens,
|
|
1329
1324
|
totalTokens: promptTokens + completionTokens,
|
|
1330
1325
|
cost,
|
|
1326
|
+
cacheReadTokens,
|
|
1327
|
+
cacheWriteTokens,
|
|
1331
1328
|
},
|
|
1332
1329
|
duration,
|
|
1333
1330
|
toolCalls: toolCallHistory.length > 0 ? toolCallHistory : undefined,
|
|
@@ -1438,11 +1435,11 @@ class AIProviderManager {
|
|
|
1438
1435
|
try {
|
|
1439
1436
|
const result = streamText({
|
|
1440
1437
|
model,
|
|
1441
|
-
messages,
|
|
1442
|
-
tools: Object.keys(tools).length > 0 ? tools : undefined,
|
|
1438
|
+
messages: applyAnthropicMessageCache(messages, modelRef.provider),
|
|
1439
|
+
tools: Object.keys(tools).length > 0 ? applyAnthropicToolCache(tools, modelRef.provider) : undefined,
|
|
1443
1440
|
temperature: request.temperature ?? 0.7,
|
|
1444
1441
|
maxOutputTokens: request.maxTokens ?? 4096,
|
|
1445
|
-
|
|
1442
|
+
stopWhen: stepCountIs(request.maxToolRoundtrips ?? 5),
|
|
1446
1443
|
abortSignal: AbortSignal.timeout(AI_TIMEOUT_MS),
|
|
1447
1444
|
});
|
|
1448
1445
|
let fullText = '';
|
|
@@ -1453,12 +1450,9 @@ class AIProviderManager {
|
|
|
1453
1450
|
const usage = await result.usage;
|
|
1454
1451
|
const duration = Date.now() - startTime;
|
|
1455
1452
|
const modelInfo = this.getModelInfo(modelRef.model);
|
|
1456
|
-
const promptTokens = usage
|
|
1457
|
-
usage?.inputTokens ?? 0;
|
|
1458
|
-
const completionTokens = usage?.completionTokens ??
|
|
1459
|
-
usage?.outputTokens ?? 0;
|
|
1453
|
+
const { promptTokens, completionTokens, cacheReadTokens, cacheWriteTokens } = extractCacheUsage(usage);
|
|
1460
1454
|
const cost = modelInfo
|
|
1461
|
-
? (promptTokens
|
|
1455
|
+
? calculateCachedInputCost(promptTokens, cacheReadTokens, cacheWriteTokens, modelInfo.costPer1MInput, getCacheConfigForModel(modelInfo.id)) +
|
|
1462
1456
|
(completionTokens / 1_000_000) * modelInfo.costPer1MOutput
|
|
1463
1457
|
: 0;
|
|
1464
1458
|
const pendingApprovals = toolResultHistory.filter(r => r.approvalRequired);
|
|
@@ -1477,6 +1471,8 @@ class AIProviderManager {
|
|
|
1477
1471
|
completionTokens,
|
|
1478
1472
|
totalTokens: promptTokens + completionTokens,
|
|
1479
1473
|
cost,
|
|
1474
|
+
cacheReadTokens,
|
|
1475
|
+
cacheWriteTokens,
|
|
1480
1476
|
},
|
|
1481
1477
|
duration,
|
|
1482
1478
|
toolCalls: toolCallHistory.length > 0 ? toolCallHistory : undefined,
|
|
@@ -7,8 +7,11 @@ import { ProviderType } from './types.js';
|
|
|
7
7
|
// Model Aliases
|
|
8
8
|
export const MODEL_ALIASES = {
|
|
9
9
|
// Anthropic
|
|
10
|
-
opus: { provider: 'anthropic', model: 'claude-opus-
|
|
11
|
-
sonnet: { provider: 'anthropic', model: 'claude-sonnet-
|
|
10
|
+
opus: { provider: 'anthropic', model: 'claude-opus-5-5' },
|
|
11
|
+
sonnet: { provider: 'anthropic', model: 'claude-sonnet-5' },
|
|
12
|
+
fable: { provider: 'anthropic', model: 'claude-fable-5-1' },
|
|
13
|
+
'opus-legacy': { provider: 'anthropic', model: 'claude-opus-4-6' },
|
|
14
|
+
'sonnet-legacy': { provider: 'anthropic', model: 'claude-sonnet-4-5-20250929' },
|
|
12
15
|
haiku: { provider: 'anthropic', model: 'claude-haiku-4-5-20251001' },
|
|
13
16
|
// OpenAI
|
|
14
17
|
gpt: { provider: 'openai', model: 'gpt-4o' },
|
|
@@ -113,6 +116,45 @@ export const MODEL_ALIASES = {
|
|
|
113
116
|
// Model Catalog
|
|
114
117
|
export const MODEL_CATALOG = [
|
|
115
118
|
// Anthropic - All support native function calling
|
|
119
|
+
// Current Anthropic models. ids, context, max output and prices verified against
|
|
120
|
+
// https://platform.claude.com/docs/en/about-claude/models/overview (2026-09-26).
|
|
121
|
+
{
|
|
122
|
+
id: 'claude-fable-5-1',
|
|
123
|
+
name: 'Claude Fable 5.1',
|
|
124
|
+
provider: 'anthropic',
|
|
125
|
+
contextWindow: 1000000,
|
|
126
|
+
maxOutput: 128000,
|
|
127
|
+
supportsVision: true,
|
|
128
|
+
supportsStreaming: true,
|
|
129
|
+
supportsTools: true,
|
|
130
|
+
costPer1MInput: 10,
|
|
131
|
+
costPer1MOutput: 50,
|
|
132
|
+
},
|
|
133
|
+
{
|
|
134
|
+
id: 'claude-opus-5-5',
|
|
135
|
+
name: 'Claude Opus 5.5',
|
|
136
|
+
provider: 'anthropic',
|
|
137
|
+
contextWindow: 1000000,
|
|
138
|
+
maxOutput: 128000,
|
|
139
|
+
supportsVision: true,
|
|
140
|
+
supportsStreaming: true,
|
|
141
|
+
supportsTools: true,
|
|
142
|
+
costPer1MInput: 4,
|
|
143
|
+
costPer1MOutput: 20,
|
|
144
|
+
},
|
|
145
|
+
{
|
|
146
|
+
id: 'claude-sonnet-5',
|
|
147
|
+
name: 'Claude Sonnet 5',
|
|
148
|
+
provider: 'anthropic',
|
|
149
|
+
contextWindow: 1000000,
|
|
150
|
+
maxOutput: 128000,
|
|
151
|
+
supportsVision: true,
|
|
152
|
+
supportsStreaming: true,
|
|
153
|
+
supportsTools: true,
|
|
154
|
+
costPer1MInput: 2,
|
|
155
|
+
costPer1MOutput: 10,
|
|
156
|
+
},
|
|
157
|
+
// Legacy entries kept because other code and tests reference them.
|
|
116
158
|
{
|
|
117
159
|
id: 'claude-opus-4-6',
|
|
118
160
|
name: 'Claude Opus 4.6',
|
|
@@ -142,12 +184,12 @@ export const MODEL_CATALOG = [
|
|
|
142
184
|
name: 'Claude Haiku 4.5',
|
|
143
185
|
provider: 'anthropic',
|
|
144
186
|
contextWindow: 200000,
|
|
145
|
-
maxOutput:
|
|
187
|
+
maxOutput: 64000,
|
|
146
188
|
supportsVision: true,
|
|
147
189
|
supportsStreaming: true,
|
|
148
190
|
supportsTools: true,
|
|
149
|
-
costPer1MInput:
|
|
150
|
-
costPer1MOutput:
|
|
191
|
+
costPer1MInput: 1,
|
|
192
|
+
costPer1MOutput: 5,
|
|
151
193
|
},
|
|
152
194
|
// OpenAI - GPT-4+ supports function calling
|
|
153
195
|
{
|
|
@@ -381,23 +423,22 @@ export const MODEL_CATALOG = [
|
|
|
381
423
|
costPer1MInput: 30.0,
|
|
382
424
|
costPer1MOutput: 60.0,
|
|
383
425
|
},
|
|
384
|
-
//
|
|
426
|
+
// Gemma 4 (local): supports native tool calling + multimodal
|
|
385
427
|
{
|
|
386
|
-
id: '
|
|
387
|
-
name: '
|
|
428
|
+
id: 'gemma4:e4b',
|
|
429
|
+
name: 'Gemma 4 E4B (Local)',
|
|
388
430
|
provider: 'ollama',
|
|
389
|
-
contextWindow:
|
|
431
|
+
contextWindow: 128000,
|
|
390
432
|
maxOutput: 8192,
|
|
391
|
-
supportsVision:
|
|
433
|
+
supportsVision: true,
|
|
392
434
|
supportsStreaming: true,
|
|
393
435
|
supportsTools: true,
|
|
394
436
|
costPer1MInput: 0,
|
|
395
437
|
costPer1MOutput: 0,
|
|
396
438
|
},
|
|
397
|
-
// Gemma 4 (local) — supports native tool calling + multimodal
|
|
398
439
|
{
|
|
399
|
-
id: 'gemma4:
|
|
400
|
-
name: 'Gemma 4
|
|
440
|
+
id: 'gemma4:26b',
|
|
441
|
+
name: 'Gemma 4 26B (Local)',
|
|
401
442
|
provider: 'ollama',
|
|
402
443
|
contextWindow: 128000,
|
|
403
444
|
maxOutput: 8192,
|
|
@@ -407,13 +448,14 @@ export const MODEL_CATALOG = [
|
|
|
407
448
|
costPer1MInput: 0,
|
|
408
449
|
costPer1MOutput: 0,
|
|
409
450
|
},
|
|
451
|
+
// Qwen 3 (local): best small model for tool calling
|
|
410
452
|
{
|
|
411
|
-
id: '
|
|
412
|
-
name: '
|
|
453
|
+
id: 'qwen3:4b',
|
|
454
|
+
name: 'Qwen 3 4B (Local)',
|
|
413
455
|
provider: 'ollama',
|
|
414
|
-
contextWindow:
|
|
456
|
+
contextWindow: 32768,
|
|
415
457
|
maxOutput: 8192,
|
|
416
|
-
supportsVision:
|
|
458
|
+
supportsVision: false,
|
|
417
459
|
supportsStreaming: true,
|
|
418
460
|
supportsTools: true,
|
|
419
461
|
costPer1MInput: 0,
|
|
@@ -55,6 +55,10 @@ export interface ChatResponse {
|
|
|
55
55
|
completionTokens: number;
|
|
56
56
|
totalTokens: number;
|
|
57
57
|
cost: number;
|
|
58
|
+
/** Prompt tokens served from the provider cache (subset of promptTokens) */
|
|
59
|
+
cacheReadTokens?: number;
|
|
60
|
+
/** Prompt tokens written to the provider cache (subset of promptTokens) */
|
|
61
|
+
cacheWriteTokens?: number;
|
|
58
62
|
};
|
|
59
63
|
duration: number;
|
|
60
64
|
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Prompt caching helpers
|
|
3
|
+
*
|
|
4
|
+
* Anthropic requires explicit cache breakpoints (providerOptions.anthropic.cacheControl).
|
|
5
|
+
* The cached prefix order is: tools, then system, then messages. A breakpoint on the
|
|
6
|
+
* last system message therefore caches tool schemas and the system prompt together;
|
|
7
|
+
* an extra breakpoint on the last tool lets the tool block hit even if the system
|
|
8
|
+
* prompt changes. OpenAI caches automatically and only reports cached token counts.
|
|
9
|
+
*
|
|
10
|
+
* Cache multipliers are relative to the base input price and are env-configurable.
|
|
11
|
+
*/
|
|
12
|
+
import type { ModelMessage, ToolSet } from 'ai';
|
|
13
|
+
export interface PromptCacheConfig {
|
|
14
|
+
enabled: boolean;
|
|
15
|
+
ttl: '5m' | '1h';
|
|
16
|
+
/** Cost of a cache read relative to base input price */
|
|
17
|
+
readMultiplier: number;
|
|
18
|
+
/** Cost of a cache write relative to base input price */
|
|
19
|
+
writeMultiplier: number;
|
|
20
|
+
}
|
|
21
|
+
export interface CacheUsage {
|
|
22
|
+
/** Total prompt tokens, including cached reads and writes */
|
|
23
|
+
promptTokens: number;
|
|
24
|
+
completionTokens: number;
|
|
25
|
+
cacheReadTokens: number;
|
|
26
|
+
cacheWriteTokens: number;
|
|
27
|
+
}
|
|
28
|
+
/** Read cache settings from env on each call so tests and runtime changes apply. */
|
|
29
|
+
export declare function getPromptCacheConfig(env?: NodeJS.ProcessEnv): PromptCacheConfig;
|
|
30
|
+
/**
|
|
31
|
+
* Cache config for one model. An explicit PROFCLAW_CACHE_READ_MULTIPLIER always wins,
|
|
32
|
+
* otherwise the model's documented read multiplier applies.
|
|
33
|
+
*/
|
|
34
|
+
export declare function getCacheConfigForModel(model: string, env?: NodeJS.ProcessEnv): PromptCacheConfig;
|
|
35
|
+
/**
|
|
36
|
+
* Add cache breakpoints to the leading system block: one on the first system message
|
|
37
|
+
* (the stable prompt) and one on the last leading system message. Callers put volatile
|
|
38
|
+
* per-request context in a later system message so the first breakpoint still hits
|
|
39
|
+
* across requests. History after the system block stays outside the cached prefix.
|
|
40
|
+
* Returns a new array; inputs are not mutated.
|
|
41
|
+
*/
|
|
42
|
+
export declare function applyAnthropicMessageCache<T extends ModelMessage>(messages: T[], provider: string, config?: PromptCacheConfig): T[];
|
|
43
|
+
/**
|
|
44
|
+
* Return tools in a deterministic (name-sorted) order with a cache breakpoint on the
|
|
45
|
+
* last one. Object key order in the request body affects the prefix bytes, so sorting
|
|
46
|
+
* keeps the tool block byte-stable across steps and sessions.
|
|
47
|
+
*/
|
|
48
|
+
export declare function applyAnthropicToolCache<T extends ToolSet>(tools: T, provider: string, config?: PromptCacheConfig): T;
|
|
49
|
+
/**
|
|
50
|
+
* Normalize AI SDK usage (v6 shape, legacy promptTokens shape, or raw provider
|
|
51
|
+
* counters) into total prompt tokens plus cache read/write counts.
|
|
52
|
+
* In AI SDK v6, inputTokens already includes cached reads and writes.
|
|
53
|
+
*/
|
|
54
|
+
export declare function extractCacheUsage(usage: unknown): CacheUsage;
|
|
55
|
+
/**
|
|
56
|
+
* Sum usage across multi-step runs (tokens plus cache read/write). Falls back to
|
|
57
|
+
* `fallback` (the run-level usage) when there are no per-step usages, because
|
|
58
|
+
* result.usage only reflects the last step in AI SDK v6.
|
|
59
|
+
*/
|
|
60
|
+
export declare function aggregateStepUsage(steps: unknown, fallback: unknown): CacheUsage;
|
|
61
|
+
/**
|
|
62
|
+
* Input cost in USD for a prompt that may include cached tokens.
|
|
63
|
+
* Uncached tokens bill at the base rate, reads and writes at the multiplied rate.
|
|
64
|
+
*/
|
|
65
|
+
export declare function calculateCachedInputCost(promptTokens: number, cacheReadTokens: number, cacheWriteTokens: number, inputRatePer1M: number, config?: PromptCacheConfig): number;
|
|
66
|
+
/** Fraction of prompt tokens served from cache (0 when there were no prompt tokens). */
|
|
67
|
+
export declare function cacheHitRate(cacheReadTokens: number, promptTokens: number): number;
|
|
68
|
+
//# sourceMappingURL=prompt-cache.d.ts.map
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Prompt caching helpers
|
|
3
|
+
*
|
|
4
|
+
* Anthropic requires explicit cache breakpoints (providerOptions.anthropic.cacheControl).
|
|
5
|
+
* The cached prefix order is: tools, then system, then messages. A breakpoint on the
|
|
6
|
+
* last system message therefore caches tool schemas and the system prompt together;
|
|
7
|
+
* an extra breakpoint on the last tool lets the tool block hit even if the system
|
|
8
|
+
* prompt changes. OpenAI caches automatically and only reports cached token counts.
|
|
9
|
+
*
|
|
10
|
+
* Cache multipliers are relative to the base input price and are env-configurable.
|
|
11
|
+
*/
|
|
12
|
+
const DEFAULT_READ_MULTIPLIER = 0.1;
|
|
13
|
+
const DEFAULT_WRITE_MULTIPLIER = 1.25;
|
|
14
|
+
function parseMultiplier(raw, fallback) {
|
|
15
|
+
if (raw === undefined || raw.trim() === '')
|
|
16
|
+
return fallback;
|
|
17
|
+
const parsed = Number(raw);
|
|
18
|
+
return Number.isFinite(parsed) && parsed >= 0 ? parsed : fallback;
|
|
19
|
+
}
|
|
20
|
+
/** Read cache settings from env on each call so tests and runtime changes apply. */
|
|
21
|
+
export function getPromptCacheConfig(env = process.env) {
|
|
22
|
+
const disabled = (env['PROFCLAW_PROMPT_CACHE'] ?? '').toLowerCase();
|
|
23
|
+
return {
|
|
24
|
+
enabled: !['0', 'false', 'off', 'no'].includes(disabled),
|
|
25
|
+
ttl: env['PROFCLAW_PROMPT_CACHE_TTL'] === '1h' ? '1h' : '5m',
|
|
26
|
+
readMultiplier: parseMultiplier(env['PROFCLAW_CACHE_READ_MULTIPLIER'], DEFAULT_READ_MULTIPLIER),
|
|
27
|
+
writeMultiplier: parseMultiplier(env['PROFCLAW_CACHE_WRITE_MULTIPLIER'], DEFAULT_WRITE_MULTIPLIER),
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Cache reads bill at 5% of the base input price on Opus 5.5 and 2.5% on Fable 5.1,
|
|
32
|
+
* versus the 10% default elsewhere (Anthropic models overview, pricing notes).
|
|
33
|
+
*/
|
|
34
|
+
const MODEL_READ_MULTIPLIERS = [
|
|
35
|
+
[/fable-5-1/, 0.025],
|
|
36
|
+
[/opus-5-5/, 0.05],
|
|
37
|
+
];
|
|
38
|
+
/**
|
|
39
|
+
* Cache config for one model. An explicit PROFCLAW_CACHE_READ_MULTIPLIER always wins,
|
|
40
|
+
* otherwise the model's documented read multiplier applies.
|
|
41
|
+
*/
|
|
42
|
+
export function getCacheConfigForModel(model, env = process.env) {
|
|
43
|
+
const base = getPromptCacheConfig(env);
|
|
44
|
+
if ((env['PROFCLAW_CACHE_READ_MULTIPLIER'] ?? '').trim() !== '')
|
|
45
|
+
return base;
|
|
46
|
+
const match = MODEL_READ_MULTIPLIERS.find(([pattern]) => pattern.test(model));
|
|
47
|
+
return match ? { ...base, readMultiplier: match[1] } : base;
|
|
48
|
+
}
|
|
49
|
+
function cacheProviderOptions(config) {
|
|
50
|
+
return {
|
|
51
|
+
anthropic: {
|
|
52
|
+
cacheControl: config.ttl === '1h' ? { type: 'ephemeral', ttl: '1h' } : { type: 'ephemeral' },
|
|
53
|
+
},
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Add cache breakpoints to the leading system block: one on the first system message
|
|
58
|
+
* (the stable prompt) and one on the last leading system message. Callers put volatile
|
|
59
|
+
* per-request context in a later system message so the first breakpoint still hits
|
|
60
|
+
* across requests. History after the system block stays outside the cached prefix.
|
|
61
|
+
* Returns a new array; inputs are not mutated.
|
|
62
|
+
*/
|
|
63
|
+
export function applyAnthropicMessageCache(messages, provider, config = getPromptCacheConfig()) {
|
|
64
|
+
if (provider !== 'anthropic' || !config.enabled)
|
|
65
|
+
return messages;
|
|
66
|
+
let lastSystem = -1;
|
|
67
|
+
for (let i = 0; i < messages.length && messages[i].role === 'system'; i++) {
|
|
68
|
+
lastSystem = i;
|
|
69
|
+
}
|
|
70
|
+
if (lastSystem < 0)
|
|
71
|
+
return messages;
|
|
72
|
+
return messages.map((msg, i) => i === 0 || i === lastSystem
|
|
73
|
+
? { ...msg, providerOptions: { ...msg.providerOptions, ...cacheProviderOptions(config) } }
|
|
74
|
+
: msg);
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Return tools in a deterministic (name-sorted) order with a cache breakpoint on the
|
|
78
|
+
* last one. Object key order in the request body affects the prefix bytes, so sorting
|
|
79
|
+
* keeps the tool block byte-stable across steps and sessions.
|
|
80
|
+
*/
|
|
81
|
+
export function applyAnthropicToolCache(tools, provider, config = getPromptCacheConfig()) {
|
|
82
|
+
if (provider !== 'anthropic' || !config.enabled)
|
|
83
|
+
return tools;
|
|
84
|
+
const names = Object.keys(tools).sort();
|
|
85
|
+
if (names.length === 0)
|
|
86
|
+
return tools;
|
|
87
|
+
const sorted = {};
|
|
88
|
+
names.forEach((name, i) => {
|
|
89
|
+
const def = tools[name];
|
|
90
|
+
sorted[name] = i === names.length - 1
|
|
91
|
+
? { ...def, providerOptions: { ...def.providerOptions, ...cacheProviderOptions(config) } }
|
|
92
|
+
: def;
|
|
93
|
+
});
|
|
94
|
+
return sorted;
|
|
95
|
+
}
|
|
96
|
+
function num(value) {
|
|
97
|
+
return typeof value === 'number' && Number.isFinite(value) ? value : undefined;
|
|
98
|
+
}
|
|
99
|
+
function rec(value) {
|
|
100
|
+
return typeof value === 'object' && value !== null ? value : undefined;
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Normalize AI SDK usage (v6 shape, legacy promptTokens shape, or raw provider
|
|
104
|
+
* counters) into total prompt tokens plus cache read/write counts.
|
|
105
|
+
* In AI SDK v6, inputTokens already includes cached reads and writes.
|
|
106
|
+
*/
|
|
107
|
+
export function extractCacheUsage(usage) {
|
|
108
|
+
const u = rec(usage);
|
|
109
|
+
if (!u)
|
|
110
|
+
return { promptTokens: 0, completionTokens: 0, cacheReadTokens: 0, cacheWriteTokens: 0 };
|
|
111
|
+
const details = rec(u['inputTokenDetails']);
|
|
112
|
+
const raw = rec(u['raw']);
|
|
113
|
+
const cacheReadTokens = num(details?.['cacheReadTokens']) ??
|
|
114
|
+
num(u['cachedInputTokens']) ??
|
|
115
|
+
num(raw?.['cache_read_input_tokens']) ??
|
|
116
|
+
num(rec(raw?.['prompt_tokens_details'])?.['cached_tokens']) ??
|
|
117
|
+
0;
|
|
118
|
+
const cacheWriteTokens = num(details?.['cacheWriteTokens']) ??
|
|
119
|
+
num(raw?.['cache_creation_input_tokens']) ??
|
|
120
|
+
0;
|
|
121
|
+
return {
|
|
122
|
+
promptTokens: num(u['inputTokens']) ?? num(u['promptTokens']) ?? 0,
|
|
123
|
+
completionTokens: num(u['outputTokens']) ?? num(u['completionTokens']) ?? 0,
|
|
124
|
+
cacheReadTokens,
|
|
125
|
+
cacheWriteTokens,
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Sum usage across multi-step runs (tokens plus cache read/write). Falls back to
|
|
130
|
+
* `fallback` (the run-level usage) when there are no per-step usages, because
|
|
131
|
+
* result.usage only reflects the last step in AI SDK v6.
|
|
132
|
+
*/
|
|
133
|
+
export function aggregateStepUsage(steps, fallback) {
|
|
134
|
+
const usages = Array.isArray(steps)
|
|
135
|
+
? steps.map((s) => rec(s)?.['usage']).filter((u) => u !== undefined && u !== null)
|
|
136
|
+
: [];
|
|
137
|
+
if (usages.length === 0)
|
|
138
|
+
return extractCacheUsage(fallback);
|
|
139
|
+
const total = { promptTokens: 0, completionTokens: 0, cacheReadTokens: 0, cacheWriteTokens: 0 };
|
|
140
|
+
for (const usage of usages) {
|
|
141
|
+
const u = extractCacheUsage(usage);
|
|
142
|
+
total.promptTokens += u.promptTokens;
|
|
143
|
+
total.completionTokens += u.completionTokens;
|
|
144
|
+
total.cacheReadTokens += u.cacheReadTokens;
|
|
145
|
+
total.cacheWriteTokens += u.cacheWriteTokens;
|
|
146
|
+
}
|
|
147
|
+
return total;
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* Input cost in USD for a prompt that may include cached tokens.
|
|
151
|
+
* Uncached tokens bill at the base rate, reads and writes at the multiplied rate.
|
|
152
|
+
*/
|
|
153
|
+
export function calculateCachedInputCost(promptTokens, cacheReadTokens, cacheWriteTokens, inputRatePer1M, config = getPromptCacheConfig()) {
|
|
154
|
+
const cached = cacheReadTokens + cacheWriteTokens;
|
|
155
|
+
const uncached = Math.max(0, promptTokens - cached);
|
|
156
|
+
const effective = uncached +
|
|
157
|
+
cacheReadTokens * config.readMultiplier +
|
|
158
|
+
cacheWriteTokens * config.writeMultiplier;
|
|
159
|
+
return (effective / 1_000_000) * inputRatePer1M;
|
|
160
|
+
}
|
|
161
|
+
/** Fraction of prompt tokens served from cache (0 when there were no prompt tokens). */
|
|
162
|
+
export function cacheHitRate(cacheReadTokens, promptTokens) {
|
|
163
|
+
return promptTokens > 0 ? Math.min(1, cacheReadTokens / promptTokens) : 0;
|
|
164
|
+
}
|
|
165
|
+
//# sourceMappingURL=prompt-cache.js.map
|
|
@@ -328,9 +328,17 @@ function getCandidatesForTier(tier, availableProviders, signals) {
|
|
|
328
328
|
});
|
|
329
329
|
// Sort by tier strategy
|
|
330
330
|
switch (tier) {
|
|
331
|
-
case 'trivial':
|
|
332
|
-
// Cheapest first (total cost = input + output rate)
|
|
333
|
-
|
|
331
|
+
case 'trivial': {
|
|
332
|
+
// Cheapest first (total cost = input + output rate). Ties (e.g. several free local
|
|
333
|
+
// models) prefer the configured free trivial model (default Gemma 4 via Ollama).
|
|
334
|
+
const preferred = process.env['SMART_ROUTER_TRIVIAL_PREFERRED_MODEL'] ?? 'gemma4:e4b';
|
|
335
|
+
return eligible.sort((a, b) => {
|
|
336
|
+
const diff = (a.costPer1MInput + a.costPer1MOutput) - (b.costPer1MInput + b.costPer1MOutput);
|
|
337
|
+
if (diff !== 0)
|
|
338
|
+
return diff;
|
|
339
|
+
return Number(b.id === preferred) - Number(a.id === preferred);
|
|
340
|
+
});
|
|
341
|
+
}
|
|
334
342
|
case 'standard':
|
|
335
343
|
// Best value: capability (context * tool support) per dollar
|
|
336
344
|
return eligible.sort((a, b) => {
|
package/dist/routes/chat.js
CHANGED
|
@@ -53,6 +53,17 @@ let getSessionModel;
|
|
|
53
53
|
let streamAgenticChat;
|
|
54
54
|
let getGroupChatManager;
|
|
55
55
|
let trackChatUsage;
|
|
56
|
+
/** Best-effort removal of spilled tool-result temp files at request end. */
|
|
57
|
+
async function disposeToolHandler(handler) {
|
|
58
|
+
try {
|
|
59
|
+
await handler.dispose?.();
|
|
60
|
+
}
|
|
61
|
+
catch (error) {
|
|
62
|
+
logger.warn("[Chat] Tool handler dispose failed", {
|
|
63
|
+
error: error instanceof Error ? error.message : String(error),
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
}
|
|
56
67
|
async function ensureChatRuntime() {
|
|
57
68
|
if (!chatRuntimePromise) {
|
|
58
69
|
chatRuntimePromise = Promise.all([
|
|
@@ -220,7 +231,10 @@ chatRoutes.post("/send", chatRateLimiter, contentFilter(), zValidator("json", z.
|
|
|
220
231
|
// Track token usage for cost dashboard
|
|
221
232
|
if (response.usage?.totalTokens && response.model) {
|
|
222
233
|
await ensureChatRuntime();
|
|
223
|
-
trackChatUsage(response.model, response.usage.totalTokens, response.usage.promptTokens, response.usage.completionTokens
|
|
234
|
+
trackChatUsage(response.model, response.usage.totalTokens, response.usage.promptTokens, response.usage.completionTokens, {
|
|
235
|
+
cacheReadTokens: response.usage.cacheReadTokens,
|
|
236
|
+
cacheWriteTokens: response.usage.cacheWriteTokens,
|
|
237
|
+
});
|
|
224
238
|
}
|
|
225
239
|
return c.json({
|
|
226
240
|
id: response.id,
|
|
@@ -1297,6 +1311,7 @@ chatRoutes.post("/conversations/:id/messages/with-tools", chatRateLimiter, conte
|
|
|
1297
1311
|
});
|
|
1298
1312
|
// Get any pending approvals
|
|
1299
1313
|
const pendingApprovals = toolHandler.getPendingApprovals();
|
|
1314
|
+
await disposeToolHandler(toolHandler);
|
|
1300
1315
|
const inferToolCallStatus = (result) => {
|
|
1301
1316
|
if (!result || typeof result !== "object") {
|
|
1302
1317
|
return "success";
|
|
@@ -1519,6 +1534,7 @@ chatRoutes.post("/with-tools", zValidator("json", z.object({
|
|
|
1519
1534
|
});
|
|
1520
1535
|
// Check for pending approvals
|
|
1521
1536
|
const pendingApprovals = toolHandler.getPendingApprovals();
|
|
1537
|
+
await disposeToolHandler(toolHandler);
|
|
1522
1538
|
return c.json({
|
|
1523
1539
|
id: response.id,
|
|
1524
1540
|
conversationId,
|
|
@@ -1787,10 +1803,8 @@ chatRoutes.post("/conversations/:id/messages/agentic", chatRateLimiter, contentF
|
|
|
1787
1803
|
: undefined,
|
|
1788
1804
|
toolCalls: collectedToolCalls.length > 0 ? collectedToolCalls : undefined,
|
|
1789
1805
|
});
|
|
1790
|
-
//
|
|
1791
|
-
|
|
1792
|
-
trackChatUsage(finalModel, totalTokens, inputTokensTotal, outputTokensTotal);
|
|
1793
|
-
}
|
|
1806
|
+
// Cost tracking for agentic runs happens per step inside AgentExecutor;
|
|
1807
|
+
// tracking totals here as well would double count.
|
|
1794
1808
|
// Send final saved message event
|
|
1795
1809
|
await stream.write(`data: ${JSON.stringify({
|
|
1796
1810
|
type: "message_saved",
|
|
@@ -1812,6 +1826,9 @@ chatRoutes.post("/conversations/:id/messages/agentic", chatRateLimiter, contentF
|
|
|
1812
1826
|
timestamp: Date.now(),
|
|
1813
1827
|
})}\n\n`);
|
|
1814
1828
|
}
|
|
1829
|
+
finally {
|
|
1830
|
+
await disposeToolHandler(toolHandler);
|
|
1831
|
+
}
|
|
1815
1832
|
});
|
|
1816
1833
|
}
|
|
1817
1834
|
catch (error) {
|
package/dist/server.js
CHANGED
|
@@ -805,6 +805,9 @@ async function main() {
|
|
|
805
805
|
const { initTokenTracker } = await import('./costs/token-tracker.js');
|
|
806
806
|
initTokenTracker();
|
|
807
807
|
appLog.info('Token tracker initialized');
|
|
808
|
+
// Sweep stale tool-result temp dirs left by crashed runs (fire-and-forget)
|
|
809
|
+
const { ResultStore } = await import('./agents/result-store.js');
|
|
810
|
+
void ResultStore.sweepStale();
|
|
808
811
|
// Initialize agents from config
|
|
809
812
|
const { getAgentRegistry } = await import('./adapters/registry.js');
|
|
810
813
|
const registry = getAgentRegistry();
|