@effect-uai/anthropic 0.11.0 → 0.12.1
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/dist/Anthropic.d.mts +20 -4
- package/dist/Anthropic.d.mts.map +1 -1
- package/dist/Anthropic.mjs +12 -2
- package/dist/Anthropic.mjs.map +1 -1
- package/dist/AnthropicTools.d.mts +12 -4
- package/dist/AnthropicTools.d.mts.map +1 -1
- package/dist/AnthropicTools.mjs +1 -1
- package/dist/AnthropicTools.mjs.map +1 -1
- package/dist/anthropicTools.test.d.mts +1 -1
- package/dist/anthropicTools.test.mjs +13 -13637
- package/dist/anthropicTools.test.mjs.map +1 -1
- package/dist/codec.d.mts +16 -4
- package/dist/codec.d.mts.map +1 -1
- package/dist/codec.mjs +25 -11
- package/dist/codec.mjs.map +1 -1
- package/dist/{magic-string.es-PAZgLX4h.mjs → magic-string.es-CUAnjKYC.mjs} +14 -13
- package/dist/{magic-string.es-PAZgLX4h.mjs.map → magic-string.es-CUAnjKYC.mjs.map} +1 -1
- package/dist/models.d.mts.map +1 -1
- package/dist/streamEvents.d.mts +0 -2
- package/dist/streamEvents.d.mts.map +1 -1
- package/dist/streamEvents.mjs +1 -1
- package/dist/streamEvents.mjs.map +1 -1
- package/dist/test.DNmyFkvJ-N3QqMRwZ.mjs +13658 -0
- package/dist/test.DNmyFkvJ-N3QqMRwZ.mjs.map +1 -0
- package/dist/usage.test.d.mts +1 -0
- package/dist/usage.test.mjs +46 -0
- package/dist/usage.test.mjs.map +1 -0
- package/package.json +5 -5
- package/src/Anthropic.ts +40 -7
- package/src/AnthropicTools.ts +4 -6
- package/src/anthropicTools.test.ts +1 -0
- package/src/codec.ts +128 -112
- package/src/usage.test.ts +54 -0
- /package/dist/{chunk-CfYAbeIz.mjs → rolldown-runtime-D7D4PA-g.mjs} +0 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { emptyAccumulator, mergeUsage } from "./codec.mjs";
|
|
2
|
+
import { applyEvent } from "./streamEvents.mjs";
|
|
3
|
+
import { n as describe, r as it, t as globalExpect } from "./test.DNmyFkvJ-N3QqMRwZ.mjs";
|
|
4
|
+
//#region src/usage.test.ts
|
|
5
|
+
const wire = {
|
|
6
|
+
input_tokens: 94,
|
|
7
|
+
output_tokens: 210,
|
|
8
|
+
cache_read_input_tokens: 31851,
|
|
9
|
+
cache_creation_input_tokens: 604
|
|
10
|
+
};
|
|
11
|
+
describe("mergeUsage", () => {
|
|
12
|
+
it("keeps all three input buckets, not just cache reads", () => {
|
|
13
|
+
const usage = mergeUsage(emptyAccumulator, wire).usage;
|
|
14
|
+
globalExpect(usage.input_tokens).toBe(94);
|
|
15
|
+
globalExpect(usage.input_tokens_details?.cached_tokens).toBe(31851);
|
|
16
|
+
globalExpect(usage.input_tokens_details?.cache_write_tokens).toBe(604);
|
|
17
|
+
});
|
|
18
|
+
it("totals every input bucket plus output", () => {
|
|
19
|
+
const usage = mergeUsage(emptyAccumulator, wire).usage;
|
|
20
|
+
globalExpect(usage.total_tokens).toBe(32759);
|
|
21
|
+
});
|
|
22
|
+
});
|
|
23
|
+
describe("streamed usage", () => {
|
|
24
|
+
it("computes a non-stale total across message_start and message_delta", () => {
|
|
25
|
+
const usage = applyEvent(applyEvent(emptyAccumulator, {
|
|
26
|
+
type: "message_start",
|
|
27
|
+
message: { usage: {
|
|
28
|
+
input_tokens: 94,
|
|
29
|
+
output_tokens: 1,
|
|
30
|
+
cache_read_input_tokens: 31851,
|
|
31
|
+
cache_creation_input_tokens: 604
|
|
32
|
+
} }
|
|
33
|
+
}), {
|
|
34
|
+
type: "message_delta",
|
|
35
|
+
delta: { stop_reason: "end_turn" },
|
|
36
|
+
usage: { output_tokens: 210 }
|
|
37
|
+
}).usage;
|
|
38
|
+
globalExpect(usage.output_tokens).toBe(210);
|
|
39
|
+
globalExpect(usage.input_tokens_details?.cache_write_tokens).toBe(604);
|
|
40
|
+
globalExpect(usage.total_tokens).toBe(32759);
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
//#endregion
|
|
44
|
+
export {};
|
|
45
|
+
|
|
46
|
+
//# sourceMappingURL=usage.test.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"usage.test.mjs","names":[],"sources":["../src/usage.test.ts"],"sourcesContent":["import { describe, expect, it } from \"vitest\"\nimport { type WireUsage, emptyAccumulator, mergeUsage } from \"./codec.js\"\nimport { type ProviderEvent, applyEvent } from \"./streamEvents.js\"\n\n// A completed cached turn: input split across post-breakpoint tokens, cache\n// reads, and cache writes, all billed separately.\nconst wire: WireUsage = {\n input_tokens: 94,\n output_tokens: 210,\n cache_read_input_tokens: 31851,\n cache_creation_input_tokens: 604,\n}\n\ndescribe(\"mergeUsage\", () => {\n it(\"keeps all three input buckets, not just cache reads\", () => {\n const usage = mergeUsage(emptyAccumulator, wire).usage\n expect(usage.input_tokens).toBe(94)\n expect(usage.input_tokens_details?.cached_tokens).toBe(31851)\n expect(usage.input_tokens_details?.cache_write_tokens).toBe(604)\n })\n\n it(\"totals every input bucket plus output\", () => {\n const usage = mergeUsage(emptyAccumulator, wire).usage\n expect(usage.total_tokens).toBe(94 + 31851 + 604 + 210)\n })\n})\n\ndescribe(\"streamed usage\", () => {\n // Anthropic sends input + cache on message_start and the final output_tokens\n // on message_delta. total_tokens must reflect the accumulated buckets, not\n // the (input + initial output) figure frozen at message_start.\n it(\"computes a non-stale total across message_start and message_delta\", () => {\n const start: ProviderEvent = {\n type: \"message_start\",\n message: {\n usage: {\n input_tokens: 94,\n output_tokens: 1,\n cache_read_input_tokens: 31851,\n cache_creation_input_tokens: 604,\n },\n },\n }\n const delta: ProviderEvent = {\n type: \"message_delta\",\n delta: { stop_reason: \"end_turn\" },\n usage: { output_tokens: 210 },\n }\n const usage = applyEvent(applyEvent(emptyAccumulator, start), delta).usage\n expect(usage.output_tokens).toBe(210)\n expect(usage.input_tokens_details?.cache_write_tokens).toBe(604)\n expect(usage.total_tokens).toBe(94 + 31851 + 604 + 210)\n })\n})\n"],"mappings":";;;;AAMA,MAAM,OAAkB;CACtB,cAAc;CACd,eAAe;CACf,yBAAyB;CACzB,6BAA6B;AAC/B;AAEA,SAAS,oBAAoB;CAC3B,GAAG,6DAA6D;EAC9D,MAAM,QAAQ,WAAW,kBAAkB,IAAI,CAAC,CAAC;EACjD,aAAO,MAAM,YAAY,CAAC,CAAC,KAAK,EAAE;EAClC,aAAO,MAAM,sBAAsB,aAAa,CAAC,CAAC,KAAK,KAAK;EAC5D,aAAO,MAAM,sBAAsB,kBAAkB,CAAC,CAAC,KAAK,GAAG;CACjE,CAAC;CAED,GAAG,+CAA+C;EAChD,MAAM,QAAQ,WAAW,kBAAkB,IAAI,CAAC,CAAC;EACjD,aAAO,MAAM,YAAY,CAAC,CAAC,KAAK,KAAsB;CACxD,CAAC;AACH,CAAC;AAED,SAAS,wBAAwB;CAI/B,GAAG,2EAA2E;EAiB5E,MAAM,QAAQ,WAAW,WAAW,kBAAkB;GAfpD,MAAM;GACN,SAAS,EACP,OAAO;IACL,cAAc;IACd,eAAe;IACf,yBAAyB;IACzB,6BAA6B;GAC/B,EACF;EAOwD,CAAC,GAAG;GAJ5D,MAAM;GACN,OAAO,EAAE,aAAa,WAAW;GACjC,OAAO,EAAE,eAAe,IAAI;EAEoC,CAAC,CAAC,CAAC;EACrE,aAAO,MAAM,aAAa,CAAC,CAAC,KAAK,GAAG;EACpC,aAAO,MAAM,sBAAsB,kBAAkB,CAAC,CAAC,KAAK,GAAG;EAC/D,aAAO,MAAM,YAAY,CAAC,CAAC,KAAK,KAAsB;CACxD,CAAC;AACH,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@effect-uai/anthropic",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.12.1",
|
|
4
4
|
"description": "Anthropic Messages API provider for @effect-uai/core.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ai",
|
|
@@ -43,13 +43,13 @@
|
|
|
43
43
|
"access": "public"
|
|
44
44
|
},
|
|
45
45
|
"devDependencies": {
|
|
46
|
-
"effect": "4.0.0-
|
|
47
|
-
"typescript": "^
|
|
48
|
-
"@effect-uai/core": "0.
|
|
46
|
+
"effect": "4.0.0-rc.111",
|
|
47
|
+
"typescript": "^7.0.2",
|
|
48
|
+
"@effect-uai/core": "0.12.1"
|
|
49
49
|
},
|
|
50
50
|
"peerDependencies": {
|
|
51
51
|
"@effect-uai/core": ">=0.2.0 <1",
|
|
52
|
-
"effect": ">=4.0.0-
|
|
52
|
+
"effect": ">=4.0.0-rc.111 <5.0.0"
|
|
53
53
|
},
|
|
54
54
|
"scripts": {
|
|
55
55
|
"build": "tsdown",
|
package/src/Anthropic.ts
CHANGED
|
@@ -24,6 +24,8 @@ import { type Turn, TurnEvent } from "@effect-uai/core/Turn"
|
|
|
24
24
|
import { renderProviderTools } from "./AnthropicTools.js"
|
|
25
25
|
import {
|
|
26
26
|
type Accumulator,
|
|
27
|
+
type CacheControl,
|
|
28
|
+
type CacheTtl,
|
|
27
29
|
type ThinkingConfig,
|
|
28
30
|
accumulatorToTurn,
|
|
29
31
|
buildRequestBody,
|
|
@@ -119,6 +121,21 @@ export type Config = {
|
|
|
119
121
|
* 4096 if neither is set.
|
|
120
122
|
*/
|
|
121
123
|
readonly defaultMaxTokens?: number
|
|
124
|
+
/**
|
|
125
|
+
* Prompt caching. Off by default, so the library stays unopinionated about
|
|
126
|
+
* a setting that changes billing. `true` is `{}`: cache with the 5-minute
|
|
127
|
+
* default TTL.
|
|
128
|
+
*
|
|
129
|
+
* Anthropic caches the `tools` → `system` → `messages` prefix and moves the
|
|
130
|
+
* breakpoint forward as the conversation grows, so a long system prompt and
|
|
131
|
+
* tool definitions are paid for once rather than on every turn. Read
|
|
132
|
+
* `usage.input_tokens_details.cached_tokens` to see it working.
|
|
133
|
+
*
|
|
134
|
+
* Mutating earlier history (rewriting an old message, compaction) breaks
|
|
135
|
+
* the prefix from that point on. Anthropic falls back to the longest
|
|
136
|
+
* unchanged prefix, so this costs savings but never correctness.
|
|
137
|
+
*/
|
|
138
|
+
readonly promptCaching?: boolean | { readonly ttl?: CacheTtl }
|
|
122
139
|
}
|
|
123
140
|
|
|
124
141
|
const ANTHROPIC_VERSION = "2023-06-01"
|
|
@@ -139,6 +156,24 @@ const outputConfig = (request: AnthropicRequest): Option.Option<Record<string, u
|
|
|
139
156
|
const resolvedMaxTokens = (cfg: Config, request: AnthropicRequest): number =>
|
|
140
157
|
request.maxOutputTokens ?? cfg.defaultMaxTokens ?? FALLBACK_MAX_TOKENS
|
|
141
158
|
|
|
159
|
+
/**
|
|
160
|
+
* One request-level breakpoint when caching is on. Anthropic places it on the
|
|
161
|
+
* last cacheable block itself, so nothing has to be marked per-block and the
|
|
162
|
+
* breakpoint stays correct as history grows.
|
|
163
|
+
*/
|
|
164
|
+
const cacheControlOf = (cfg: Config): Option.Option<CacheControl> =>
|
|
165
|
+
pipe(
|
|
166
|
+
Option.fromUndefinedOr(cfg.promptCaching),
|
|
167
|
+
Option.flatMap((opt) =>
|
|
168
|
+
opt === false
|
|
169
|
+
? Option.none<CacheControl>()
|
|
170
|
+
: Option.some<CacheControl>({
|
|
171
|
+
type: "ephemeral",
|
|
172
|
+
...(opt !== true && opt.ttl !== undefined && { ttl: opt.ttl }),
|
|
173
|
+
}),
|
|
174
|
+
),
|
|
175
|
+
)
|
|
176
|
+
|
|
142
177
|
const functionToolDescriptors = (
|
|
143
178
|
request: AnthropicRequest,
|
|
144
179
|
): ReadonlyArray<Record<string, unknown>> =>
|
|
@@ -302,6 +337,7 @@ const buildNativeStream = (cfg: Config) => {
|
|
|
302
337
|
toolChoice: toolChoiceWire(request),
|
|
303
338
|
userId: Option.fromUndefinedOr(request.user),
|
|
304
339
|
outputConfig: structured,
|
|
340
|
+
cacheControl: cacheControlOf(cfg),
|
|
305
341
|
})
|
|
306
342
|
|
|
307
343
|
const body = yield* Result.match(bodyResult, {
|
|
@@ -409,13 +445,10 @@ export const layer = (
|
|
|
409
445
|
const typed = Layer.effect(Anthropic, make(cfg))
|
|
410
446
|
const generic = Layer.effect(
|
|
411
447
|
LanguageModel,
|
|
412
|
-
Effect.map(
|
|
413
|
-
|
|
414
|
-
(
|
|
415
|
-
|
|
416
|
-
turn: (request) => s.turn(request as AnthropicRequest),
|
|
417
|
-
}),
|
|
418
|
-
),
|
|
448
|
+
Effect.map(make(cfg), (s): LanguageModelService => ({
|
|
449
|
+
streamTurn: (request) => s.streamTurn(request as AnthropicRequest),
|
|
450
|
+
turn: (request) => s.turn(request as AnthropicRequest),
|
|
451
|
+
})),
|
|
419
452
|
)
|
|
420
453
|
return Layer.merge(typed, generic)
|
|
421
454
|
}
|
package/src/AnthropicTools.ts
CHANGED
|
@@ -109,12 +109,10 @@ const renderOne = (
|
|
|
109
109
|
})
|
|
110
110
|
: decodeConfig(tool.config).pipe(
|
|
111
111
|
Result.map(configToWire),
|
|
112
|
-
Result.mapError(
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
}),
|
|
117
|
-
),
|
|
112
|
+
Result.mapError((): ToolRenderError => ({
|
|
113
|
+
capability: "providerTool",
|
|
114
|
+
reason: `unrecognized Anthropic provider tool config on "${tool.name}"`,
|
|
115
|
+
})),
|
|
118
116
|
)
|
|
119
117
|
|
|
120
118
|
/**
|
package/src/codec.ts
CHANGED
|
@@ -195,11 +195,9 @@ const itemToAssistantBlocks = (
|
|
|
195
195
|
function_call: (f) =>
|
|
196
196
|
pipe(
|
|
197
197
|
parseJson(f.arguments),
|
|
198
|
-
Result.map(
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
],
|
|
202
|
-
),
|
|
198
|
+
Result.map((input): ReadonlyArray<RequestAssistantContentBlock> => [
|
|
199
|
+
{ type: "tool_use", id: f.call_id, name: f.name, input },
|
|
200
|
+
]),
|
|
203
201
|
),
|
|
204
202
|
function_call_output: (): Result.Result<
|
|
205
203
|
ReadonlyArray<RequestAssistantContentBlock>,
|
|
@@ -324,6 +322,20 @@ export type ThinkingConfig = {
|
|
|
324
322
|
readonly budget_tokens: number
|
|
325
323
|
}
|
|
326
324
|
|
|
325
|
+
/** Cache lifetime. `1h` writes cost 2x base input; `5m` costs 1.25x. */
|
|
326
|
+
export type CacheTtl = "5m" | "1h"
|
|
327
|
+
|
|
328
|
+
/**
|
|
329
|
+
* Request-level cache breakpoint. Anthropic applies it to the last cacheable
|
|
330
|
+
* block and moves it forward as the conversation grows, so the cached prefix
|
|
331
|
+
* (`tools` → `system` → `messages`) follows the conversation without the
|
|
332
|
+
* caller re-placing anything.
|
|
333
|
+
*/
|
|
334
|
+
export type CacheControl = {
|
|
335
|
+
readonly type: "ephemeral"
|
|
336
|
+
readonly ttl?: CacheTtl
|
|
337
|
+
}
|
|
338
|
+
|
|
327
339
|
export type RequestBody = {
|
|
328
340
|
readonly model: string
|
|
329
341
|
readonly messages: ReadonlyArray<RequestMessage>
|
|
@@ -338,6 +350,7 @@ export type RequestBody = {
|
|
|
338
350
|
readonly tool_choice?: Record<string, unknown>
|
|
339
351
|
readonly metadata?: { readonly user_id: string }
|
|
340
352
|
readonly output_config?: Record<string, unknown>
|
|
353
|
+
readonly cache_control?: CacheControl
|
|
341
354
|
readonly stream: true
|
|
342
355
|
}
|
|
343
356
|
|
|
@@ -354,57 +367,60 @@ export const buildRequestBody = (params: {
|
|
|
354
367
|
readonly toolChoice: Option.Option<Record<string, unknown>>
|
|
355
368
|
readonly userId: Option.Option<string>
|
|
356
369
|
readonly outputConfig: Option.Option<Record<string, unknown>>
|
|
370
|
+
readonly cacheControl: Option.Option<CacheControl>
|
|
357
371
|
}): Result.Result<RequestBody, JsonParseError> =>
|
|
358
372
|
pipe(
|
|
359
373
|
groupedMessages(params.history),
|
|
360
|
-
Result.map(
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
onSome: (system) => ({ system }),
|
|
368
|
-
}),
|
|
369
|
-
...Option.match(params.temperature, {
|
|
370
|
-
onNone: () => ({}),
|
|
371
|
-
onSome: (temperature) => ({ temperature }),
|
|
372
|
-
}),
|
|
373
|
-
...Option.match(params.topP, {
|
|
374
|
-
onNone: () => ({}),
|
|
375
|
-
onSome: (top_p) => ({ top_p }),
|
|
376
|
-
}),
|
|
377
|
-
...Option.match(params.topK, {
|
|
378
|
-
onNone: () => ({}),
|
|
379
|
-
onSome: (top_k) => ({ top_k }),
|
|
380
|
-
}),
|
|
381
|
-
...Option.match(params.stopSequences, {
|
|
382
|
-
onNone: () => ({}),
|
|
383
|
-
onSome: (stop_sequences) => ({ stop_sequences }),
|
|
384
|
-
}),
|
|
385
|
-
...Option.match(params.thinking, {
|
|
386
|
-
onNone: () => ({}),
|
|
387
|
-
onSome: (thinking) => ({ thinking }),
|
|
388
|
-
}),
|
|
389
|
-
...Option.match(params.tools, {
|
|
390
|
-
onNone: () => ({}),
|
|
391
|
-
onSome: (tools) => ({ tools }),
|
|
392
|
-
}),
|
|
393
|
-
...Option.match(params.toolChoice, {
|
|
394
|
-
onNone: () => ({}),
|
|
395
|
-
onSome: (tool_choice) => ({ tool_choice }),
|
|
396
|
-
}),
|
|
397
|
-
...Option.match(params.userId, {
|
|
398
|
-
onNone: () => ({}),
|
|
399
|
-
onSome: (user_id) => ({ metadata: { user_id } }),
|
|
400
|
-
}),
|
|
401
|
-
...Option.match(params.outputConfig, {
|
|
402
|
-
onNone: () => ({}),
|
|
403
|
-
onSome: (output_config) => ({ output_config }),
|
|
404
|
-
}),
|
|
405
|
-
stream: true,
|
|
374
|
+
Result.map((messages): RequestBody => ({
|
|
375
|
+
model: params.model,
|
|
376
|
+
messages,
|
|
377
|
+
max_tokens: params.maxTokens,
|
|
378
|
+
...Option.match(systemFromHistory(params.history), {
|
|
379
|
+
onNone: () => ({}),
|
|
380
|
+
onSome: (system) => ({ system }),
|
|
406
381
|
}),
|
|
407
|
-
|
|
382
|
+
...Option.match(params.temperature, {
|
|
383
|
+
onNone: () => ({}),
|
|
384
|
+
onSome: (temperature) => ({ temperature }),
|
|
385
|
+
}),
|
|
386
|
+
...Option.match(params.topP, {
|
|
387
|
+
onNone: () => ({}),
|
|
388
|
+
onSome: (top_p) => ({ top_p }),
|
|
389
|
+
}),
|
|
390
|
+
...Option.match(params.topK, {
|
|
391
|
+
onNone: () => ({}),
|
|
392
|
+
onSome: (top_k) => ({ top_k }),
|
|
393
|
+
}),
|
|
394
|
+
...Option.match(params.stopSequences, {
|
|
395
|
+
onNone: () => ({}),
|
|
396
|
+
onSome: (stop_sequences) => ({ stop_sequences }),
|
|
397
|
+
}),
|
|
398
|
+
...Option.match(params.thinking, {
|
|
399
|
+
onNone: () => ({}),
|
|
400
|
+
onSome: (thinking) => ({ thinking }),
|
|
401
|
+
}),
|
|
402
|
+
...Option.match(params.tools, {
|
|
403
|
+
onNone: () => ({}),
|
|
404
|
+
onSome: (tools) => ({ tools }),
|
|
405
|
+
}),
|
|
406
|
+
...Option.match(params.toolChoice, {
|
|
407
|
+
onNone: () => ({}),
|
|
408
|
+
onSome: (tool_choice) => ({ tool_choice }),
|
|
409
|
+
}),
|
|
410
|
+
...Option.match(params.userId, {
|
|
411
|
+
onNone: () => ({}),
|
|
412
|
+
onSome: (user_id) => ({ metadata: { user_id } }),
|
|
413
|
+
}),
|
|
414
|
+
...Option.match(params.outputConfig, {
|
|
415
|
+
onNone: () => ({}),
|
|
416
|
+
onSome: (output_config) => ({ output_config }),
|
|
417
|
+
}),
|
|
418
|
+
...Option.match(params.cacheControl, {
|
|
419
|
+
onNone: () => ({}),
|
|
420
|
+
onSome: (cache_control) => ({ cache_control }),
|
|
421
|
+
}),
|
|
422
|
+
stream: true,
|
|
423
|
+
})),
|
|
408
424
|
)
|
|
409
425
|
|
|
410
426
|
// ---------------------------------------------------------------------------
|
|
@@ -503,23 +519,34 @@ export const setStopReason = (acc: Accumulator, reason: string): Accumulator =>
|
|
|
503
519
|
stopReason: Option.some(reason),
|
|
504
520
|
})
|
|
505
521
|
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
522
|
+
// Anthropic splits input across three billed buckets: `input_tokens`
|
|
523
|
+
// (post-breakpoint), cache reads, and cache writes. All three are kept;
|
|
524
|
+
// `input_tokens` is not the input total. Each field carries over from the
|
|
525
|
+
// prior value since a streamed turn spreads usage across events (input +
|
|
526
|
+
// cache on message_start, output_tokens on message_delta), so total_tokens is
|
|
527
|
+
// summed from the accumulated buckets, not one event.
|
|
509
528
|
export const mergeUsage = (acc: Accumulator, wire: WireUsage): Accumulator => {
|
|
510
|
-
const
|
|
529
|
+
const prev = acc.usage
|
|
530
|
+
const input_tokens = wire.input_tokens ?? prev.input_tokens
|
|
531
|
+
const output_tokens = wire.output_tokens ?? prev.output_tokens
|
|
532
|
+
const cached_tokens = wire.cache_read_input_tokens ?? prev.input_tokens_details?.cached_tokens
|
|
533
|
+
const cache_write_tokens =
|
|
534
|
+
wire.cache_creation_input_tokens ?? prev.input_tokens_details?.cache_write_tokens
|
|
535
|
+
|
|
536
|
+
const buckets = [input_tokens, cached_tokens, cache_write_tokens, output_tokens]
|
|
537
|
+
const total_tokens = buckets.every((v) => v === undefined)
|
|
538
|
+
? undefined
|
|
539
|
+
: buckets.reduce<number>((sum, v) => sum + (v ?? 0), 0)
|
|
540
|
+
|
|
541
|
+
const details: Items.InputTokensDetails = {
|
|
542
|
+
...(cached_tokens !== undefined && { cached_tokens }),
|
|
543
|
+
...(cache_write_tokens !== undefined && { cache_write_tokens }),
|
|
544
|
+
}
|
|
511
545
|
const usage: Items.Usage = {
|
|
512
|
-
...
|
|
513
|
-
...(
|
|
514
|
-
...(
|
|
515
|
-
...(
|
|
516
|
-
wire.output_tokens !== undefined && {
|
|
517
|
-
total_tokens: wire.input_tokens + wire.output_tokens,
|
|
518
|
-
}),
|
|
519
|
-
...Option.match(cached, {
|
|
520
|
-
onNone: () => ({}),
|
|
521
|
-
onSome: (cached_tokens) => ({ input_tokens_details: { cached_tokens } }),
|
|
522
|
-
}),
|
|
546
|
+
...(input_tokens !== undefined && { input_tokens }),
|
|
547
|
+
...(output_tokens !== undefined && { output_tokens }),
|
|
548
|
+
...(total_tokens !== undefined && { total_tokens }),
|
|
549
|
+
...(Object.keys(details).length > 0 && { input_tokens_details: details }),
|
|
523
550
|
}
|
|
524
551
|
return { ...acc, usage }
|
|
525
552
|
}
|
|
@@ -545,52 +572,41 @@ const blocksByIndex = (acc: Accumulator): ReadonlyArray<BlockBuffer> =>
|
|
|
545
572
|
|
|
546
573
|
const blockToItems = (block: BlockBuffer): ReadonlyArray<Items.HistoryItem> =>
|
|
547
574
|
Match.value(block.type).pipe(
|
|
548
|
-
Match.when(
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
},
|
|
559
|
-
],
|
|
560
|
-
),
|
|
561
|
-
Match.when(
|
|
562
|
-
"tool_use",
|
|
563
|
-
(): ReadonlyArray<Items.HistoryItem> => [
|
|
564
|
-
{
|
|
565
|
-
type: "function_call",
|
|
566
|
-
call_id: Option.getOrElse(block.id, () => ""),
|
|
567
|
-
name: Option.getOrElse(block.name, () => ""),
|
|
568
|
-
arguments: block.inputJson,
|
|
569
|
-
},
|
|
570
|
-
],
|
|
571
|
-
),
|
|
572
|
-
Match.when(
|
|
573
|
-
"thinking",
|
|
574
|
-
(): ReadonlyArray<Items.HistoryItem> => [
|
|
575
|
-
{
|
|
576
|
-
type: "reasoning",
|
|
577
|
-
...(block.thinking.length > 0 && { summary: block.thinking }),
|
|
578
|
-
...(block.signature.length > 0 && { signature: block.signature }),
|
|
579
|
-
},
|
|
580
|
-
],
|
|
581
|
-
),
|
|
582
|
-
Match.when(
|
|
583
|
-
"redacted_thinking",
|
|
584
|
-
(): ReadonlyArray<Items.HistoryItem> => [
|
|
585
|
-
{
|
|
586
|
-
type: "reasoning",
|
|
587
|
-
...Option.match(block.redactedData, {
|
|
588
|
-
onNone: () => ({}),
|
|
589
|
-
onSome: (signature) => ({ signature }),
|
|
590
|
-
}),
|
|
591
|
-
},
|
|
592
|
-
],
|
|
575
|
+
Match.when("text", (): ReadonlyArray<Items.HistoryItem> =>
|
|
576
|
+
block.text.length === 0
|
|
577
|
+
? []
|
|
578
|
+
: [
|
|
579
|
+
{
|
|
580
|
+
type: "message",
|
|
581
|
+
role: "assistant",
|
|
582
|
+
content: [{ type: "output_text", text: block.text }],
|
|
583
|
+
},
|
|
584
|
+
],
|
|
593
585
|
),
|
|
586
|
+
Match.when("tool_use", (): ReadonlyArray<Items.HistoryItem> => [
|
|
587
|
+
{
|
|
588
|
+
type: "function_call",
|
|
589
|
+
call_id: Option.getOrElse(block.id, () => ""),
|
|
590
|
+
name: Option.getOrElse(block.name, () => ""),
|
|
591
|
+
arguments: block.inputJson,
|
|
592
|
+
},
|
|
593
|
+
]),
|
|
594
|
+
Match.when("thinking", (): ReadonlyArray<Items.HistoryItem> => [
|
|
595
|
+
{
|
|
596
|
+
type: "reasoning",
|
|
597
|
+
...(block.thinking.length > 0 && { summary: block.thinking }),
|
|
598
|
+
...(block.signature.length > 0 && { signature: block.signature }),
|
|
599
|
+
},
|
|
600
|
+
]),
|
|
601
|
+
Match.when("redacted_thinking", (): ReadonlyArray<Items.HistoryItem> => [
|
|
602
|
+
{
|
|
603
|
+
type: "reasoning",
|
|
604
|
+
...Option.match(block.redactedData, {
|
|
605
|
+
onNone: () => ({}),
|
|
606
|
+
onSome: (signature) => ({ signature }),
|
|
607
|
+
}),
|
|
608
|
+
},
|
|
609
|
+
]),
|
|
594
610
|
Match.exhaustive,
|
|
595
611
|
)
|
|
596
612
|
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest"
|
|
2
|
+
import { type WireUsage, emptyAccumulator, mergeUsage } from "./codec.js"
|
|
3
|
+
import { type ProviderEvent, applyEvent } from "./streamEvents.js"
|
|
4
|
+
|
|
5
|
+
// A completed cached turn: input split across post-breakpoint tokens, cache
|
|
6
|
+
// reads, and cache writes, all billed separately.
|
|
7
|
+
const wire: WireUsage = {
|
|
8
|
+
input_tokens: 94,
|
|
9
|
+
output_tokens: 210,
|
|
10
|
+
cache_read_input_tokens: 31851,
|
|
11
|
+
cache_creation_input_tokens: 604,
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
describe("mergeUsage", () => {
|
|
15
|
+
it("keeps all three input buckets, not just cache reads", () => {
|
|
16
|
+
const usage = mergeUsage(emptyAccumulator, wire).usage
|
|
17
|
+
expect(usage.input_tokens).toBe(94)
|
|
18
|
+
expect(usage.input_tokens_details?.cached_tokens).toBe(31851)
|
|
19
|
+
expect(usage.input_tokens_details?.cache_write_tokens).toBe(604)
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
it("totals every input bucket plus output", () => {
|
|
23
|
+
const usage = mergeUsage(emptyAccumulator, wire).usage
|
|
24
|
+
expect(usage.total_tokens).toBe(94 + 31851 + 604 + 210)
|
|
25
|
+
})
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
describe("streamed usage", () => {
|
|
29
|
+
// Anthropic sends input + cache on message_start and the final output_tokens
|
|
30
|
+
// on message_delta. total_tokens must reflect the accumulated buckets, not
|
|
31
|
+
// the (input + initial output) figure frozen at message_start.
|
|
32
|
+
it("computes a non-stale total across message_start and message_delta", () => {
|
|
33
|
+
const start: ProviderEvent = {
|
|
34
|
+
type: "message_start",
|
|
35
|
+
message: {
|
|
36
|
+
usage: {
|
|
37
|
+
input_tokens: 94,
|
|
38
|
+
output_tokens: 1,
|
|
39
|
+
cache_read_input_tokens: 31851,
|
|
40
|
+
cache_creation_input_tokens: 604,
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
}
|
|
44
|
+
const delta: ProviderEvent = {
|
|
45
|
+
type: "message_delta",
|
|
46
|
+
delta: { stop_reason: "end_turn" },
|
|
47
|
+
usage: { output_tokens: 210 },
|
|
48
|
+
}
|
|
49
|
+
const usage = applyEvent(applyEvent(emptyAccumulator, start), delta).usage
|
|
50
|
+
expect(usage.output_tokens).toBe(210)
|
|
51
|
+
expect(usage.input_tokens_details?.cache_write_tokens).toBe(604)
|
|
52
|
+
expect(usage.total_tokens).toBe(94 + 31851 + 604 + 210)
|
|
53
|
+
})
|
|
54
|
+
})
|
|
File without changes
|