@effect-uai/anthropic 0.1.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/src/codec.ts ADDED
@@ -0,0 +1,628 @@
1
+ import { Array as Arr, Match, Option, Order, Result, Schema, pipe } from "effect"
2
+ import * as Items from "@effect-uai/core/Items"
3
+ import { JsonParseError } from "@effect-uai/core/JSONL"
4
+ import { matchType } from "@effect-uai/core/Match"
5
+ import type { Turn } from "@effect-uai/core/Turn"
6
+
7
+ // ---------------------------------------------------------------------------
8
+ // Wire schemas - subset of Anthropic Messages API we consume.
9
+ // Reference: https://platform.claude.com/docs/en/api/messages
10
+ // ---------------------------------------------------------------------------
11
+
12
+ const WireTextBlock = Schema.Struct({
13
+ type: Schema.Literal("text"),
14
+ text: Schema.String,
15
+ })
16
+
17
+ const WireToolUseBlock = Schema.Struct({
18
+ type: Schema.Literal("tool_use"),
19
+ id: Schema.String,
20
+ name: Schema.String,
21
+ input: Schema.Unknown,
22
+ })
23
+
24
+ const WireThinkingBlock = Schema.Struct({
25
+ type: Schema.Literal("thinking"),
26
+ thinking: Schema.String,
27
+ signature: Schema.optional(Schema.String),
28
+ })
29
+
30
+ const WireRedactedThinkingBlock = Schema.Struct({
31
+ type: Schema.Literal("redacted_thinking"),
32
+ data: Schema.String,
33
+ })
34
+
35
+ export const WireContentBlock = Schema.Union([
36
+ WireTextBlock,
37
+ WireToolUseBlock,
38
+ WireThinkingBlock,
39
+ WireRedactedThinkingBlock,
40
+ ])
41
+ export type WireContentBlock = typeof WireContentBlock.Type
42
+
43
+ const WireUsage = Schema.Struct({
44
+ input_tokens: Schema.optional(Schema.Number),
45
+ output_tokens: Schema.optional(Schema.Number),
46
+ cache_creation_input_tokens: Schema.optional(Schema.NullOr(Schema.Number)),
47
+ cache_read_input_tokens: Schema.optional(Schema.NullOr(Schema.Number)),
48
+ })
49
+ export type WireUsage = typeof WireUsage.Type
50
+
51
+ // ---------------------------------------------------------------------------
52
+ // History → request body
53
+ // ---------------------------------------------------------------------------
54
+
55
+ interface RequestTextContent {
56
+ readonly type: "text"
57
+ readonly text: string
58
+ }
59
+
60
+ interface RequestToolResultContent {
61
+ readonly type: "tool_result"
62
+ readonly tool_use_id: string
63
+ readonly content: string
64
+ }
65
+
66
+ interface RequestToolUseContent {
67
+ readonly type: "tool_use"
68
+ readonly id: string
69
+ readonly name: string
70
+ readonly input: unknown
71
+ }
72
+
73
+ interface RequestThinkingContent {
74
+ readonly type: "thinking"
75
+ readonly thinking: string
76
+ readonly signature?: string
77
+ }
78
+
79
+ interface RequestRedactedThinkingContent {
80
+ readonly type: "redacted_thinking"
81
+ readonly data: string
82
+ }
83
+
84
+ interface RequestImageContent {
85
+ readonly type: "image"
86
+ readonly source:
87
+ | { readonly type: "url"; readonly url: string }
88
+ | { readonly type: "base64"; readonly media_type: string; readonly data: string }
89
+ }
90
+
91
+ type RequestUserContentBlock = RequestTextContent | RequestToolResultContent | RequestImageContent
92
+
93
+ type RequestAssistantContentBlock =
94
+ | RequestTextContent
95
+ | RequestToolUseContent
96
+ | RequestThinkingContent
97
+ | RequestRedactedThinkingContent
98
+
99
+ interface RequestUserMessage {
100
+ readonly role: "user"
101
+ readonly content: ReadonlyArray<RequestUserContentBlock>
102
+ }
103
+
104
+ interface RequestAssistantMessage {
105
+ readonly role: "assistant"
106
+ readonly content: ReadonlyArray<RequestAssistantContentBlock>
107
+ }
108
+
109
+ type RequestMessage = RequestUserMessage | RequestAssistantMessage
110
+
111
+ const blockText = Match.type<Items.ContentBlock>().pipe(
112
+ matchType("input_text", (b) => b.text),
113
+ matchType("input_image", () => ""),
114
+ matchType("output_text", (b) => b.text),
115
+ matchType("refusal", (b) => b.text),
116
+ Match.exhaustive,
117
+ )
118
+
119
+ const messageText = (message: Items.Message): string => message.content.map(blockText).join("")
120
+
121
+ const userContentBlock = (
122
+ block: Items.ContentBlock,
123
+ ): Result.Result<RequestUserContentBlock, void> =>
124
+ Match.value(block).pipe(
125
+ matchType("input_text", (b) =>
126
+ b.text.length === 0
127
+ ? Result.failVoid
128
+ : Result.succeed({ type: "text" as const, text: b.text }),
129
+ ),
130
+ matchType("input_image", (b) =>
131
+ Result.succeed({
132
+ type: "image" as const,
133
+ source:
134
+ b.source._tag === "url"
135
+ ? { type: "url" as const, url: b.source.url }
136
+ : {
137
+ type: "base64" as const,
138
+ media_type: b.source.media_type,
139
+ data: b.source.data,
140
+ },
141
+ }),
142
+ ),
143
+ // Assistant content; never appears on a user message in practice. Skip.
144
+ matchType("output_text", () => Result.failVoid),
145
+ matchType("refusal", () => Result.failVoid),
146
+ Match.exhaustive,
147
+ )
148
+
149
+ const parseJson = (s: string): Result.Result<unknown, JsonParseError> =>
150
+ Result.try({
151
+ try: () => JSON.parse(s) as unknown,
152
+ catch: (cause) => new JsonParseError({ line: s, cause }),
153
+ })
154
+
155
+ type RoleBucket = "user" | "assistant" | "system"
156
+
157
+ const roleBucket = (item: Items.Item): RoleBucket =>
158
+ Match.value(item).pipe(
159
+ matchType("message", (m) => m.role),
160
+ matchType("function_call", () => "assistant" as const),
161
+ matchType("function_call_output", () => "user" as const),
162
+ matchType("reasoning", () => "assistant" as const),
163
+ Match.exhaustive,
164
+ )
165
+
166
+ const itemToUserBlocks = (item: Items.Item): ReadonlyArray<RequestUserContentBlock> =>
167
+ Match.value(item).pipe(
168
+ matchType(
169
+ "message",
170
+ (m): ReadonlyArray<RequestUserContentBlock> =>
171
+ m.role === "user" ? pipe(m.content, Arr.filterMap(userContentBlock)) : [],
172
+ ),
173
+ matchType("function_call", (): ReadonlyArray<RequestUserContentBlock> => []),
174
+ matchType(
175
+ "function_call_output",
176
+ (o): ReadonlyArray<RequestUserContentBlock> => [
177
+ { type: "tool_result", tool_use_id: o.call_id, content: o.output },
178
+ ],
179
+ ),
180
+ matchType("reasoning", (): ReadonlyArray<RequestUserContentBlock> => []),
181
+ Match.exhaustive,
182
+ )
183
+
184
+ const itemToAssistantBlocks = (
185
+ item: Items.Item,
186
+ ): Result.Result<ReadonlyArray<RequestAssistantContentBlock>, JsonParseError> =>
187
+ Match.value(item).pipe(
188
+ matchType(
189
+ "message",
190
+ (m): Result.Result<ReadonlyArray<RequestAssistantContentBlock>, JsonParseError> => {
191
+ const text = messageText(m)
192
+ return Result.succeed(
193
+ m.role === "assistant" && text.length > 0 ? [{ type: "text", text }] : [],
194
+ )
195
+ },
196
+ ),
197
+ matchType("function_call", (f) =>
198
+ pipe(
199
+ parseJson(f.arguments),
200
+ Result.map(
201
+ (input): ReadonlyArray<RequestAssistantContentBlock> => [
202
+ { type: "tool_use", id: f.call_id, name: f.name, input },
203
+ ],
204
+ ),
205
+ ),
206
+ ),
207
+ matchType(
208
+ "function_call_output",
209
+ (): Result.Result<ReadonlyArray<RequestAssistantContentBlock>, JsonParseError> =>
210
+ Result.succeed([]),
211
+ ),
212
+ matchType("reasoning", (r) => {
213
+ const blocks: ReadonlyArray<RequestAssistantContentBlock> =
214
+ r.summary !== undefined
215
+ ? [
216
+ {
217
+ type: "thinking",
218
+ thinking: r.summary,
219
+ ...(r.signature !== undefined && { signature: r.signature }),
220
+ },
221
+ ]
222
+ : r.signature !== undefined
223
+ ? [{ type: "redacted_thinking", data: r.signature }]
224
+ : []
225
+ return Result.succeed(blocks)
226
+ }),
227
+ Match.exhaustive,
228
+ )
229
+
230
+ interface GroupAcc {
231
+ readonly messages: ReadonlyArray<RequestMessage>
232
+ readonly currentRole: Option.Option<"user" | "assistant">
233
+ readonly userBuf: ReadonlyArray<RequestUserContentBlock>
234
+ readonly assistantBuf: ReadonlyArray<RequestAssistantContentBlock>
235
+ }
236
+
237
+ const flushAcc = (acc: GroupAcc): ReadonlyArray<RequestMessage> =>
238
+ Option.match(acc.currentRole, {
239
+ onNone: () => acc.messages,
240
+ onSome: (role) =>
241
+ role === "user" && acc.userBuf.length > 0
242
+ ? [...acc.messages, { role: "user", content: acc.userBuf }]
243
+ : role === "assistant" && acc.assistantBuf.length > 0
244
+ ? [...acc.messages, { role: "assistant", content: acc.assistantBuf }]
245
+ : acc.messages,
246
+ })
247
+
248
+ const appendUser = (acc: GroupAcc, blocks: ReadonlyArray<RequestUserContentBlock>): GroupAcc =>
249
+ blocks.length === 0
250
+ ? acc
251
+ : Option.isSome(acc.currentRole) && acc.currentRole.value === "user"
252
+ ? { ...acc, userBuf: [...acc.userBuf, ...blocks] }
253
+ : {
254
+ messages: flushAcc(acc),
255
+ currentRole: Option.some("user"),
256
+ userBuf: blocks,
257
+ assistantBuf: [],
258
+ }
259
+
260
+ const appendAssistant = (
261
+ acc: GroupAcc,
262
+ blocks: ReadonlyArray<RequestAssistantContentBlock>,
263
+ ): GroupAcc =>
264
+ blocks.length === 0
265
+ ? acc
266
+ : Option.isSome(acc.currentRole) && acc.currentRole.value === "assistant"
267
+ ? { ...acc, assistantBuf: [...acc.assistantBuf, ...blocks] }
268
+ : {
269
+ messages: flushAcc(acc),
270
+ currentRole: Option.some("assistant"),
271
+ userBuf: [],
272
+ assistantBuf: blocks,
273
+ }
274
+
275
+ const groupStep = (acc: GroupAcc, item: Items.Item): Result.Result<GroupAcc, JsonParseError> => {
276
+ const bucket = roleBucket(item)
277
+ if (bucket === "system") return Result.succeed(acc)
278
+ if (bucket === "user") {
279
+ return Result.succeed(appendUser(acc, itemToUserBlocks(item)))
280
+ }
281
+ return pipe(
282
+ itemToAssistantBlocks(item),
283
+ Result.map((blocks) => appendAssistant(acc, blocks)),
284
+ )
285
+ }
286
+
287
+ /**
288
+ * Group consecutive same-role items into Anthropic-shaped messages.
289
+ * Anthropic requires strict user/assistant alternation; consecutive items
290
+ * from the same role are folded into one message's `content`. Fails if any
291
+ * `function_call.arguments` is not valid JSON, since Anthropic's wire shape
292
+ * requires an object input.
293
+ */
294
+ const groupedMessages = (
295
+ history: ReadonlyArray<Items.Item>,
296
+ ): Result.Result<ReadonlyArray<RequestMessage>, JsonParseError> => {
297
+ const initial: Result.Result<GroupAcc, JsonParseError> = Result.succeed({
298
+ messages: [],
299
+ currentRole: Option.none(),
300
+ userBuf: [],
301
+ assistantBuf: [],
302
+ })
303
+ return pipe(
304
+ Arr.reduce(history, initial, (acc, item) => Result.flatMap(acc, (a) => groupStep(a, item))),
305
+ Result.map(flushAcc),
306
+ )
307
+ }
308
+
309
+ const isSystemMessage = (item: Items.Item): item is Items.Message =>
310
+ item.type === "message" && item.role === "system"
311
+
312
+ const systemFromHistory = (history: ReadonlyArray<Items.Item>): Option.Option<string> => {
313
+ const texts = pipe(
314
+ history,
315
+ Arr.filterMap((item) =>
316
+ isSystemMessage(item) ? Result.succeed(messageText(item)) : Result.failVoid,
317
+ ),
318
+ Arr.filter((s) => s.length > 0),
319
+ )
320
+ return texts.length === 0 ? Option.none() : Option.some(texts.join("\n"))
321
+ }
322
+
323
+ export interface ThinkingConfig {
324
+ readonly type: "enabled"
325
+ readonly budget_tokens: number
326
+ }
327
+
328
+ export interface RequestBody {
329
+ readonly model: string
330
+ readonly messages: ReadonlyArray<RequestMessage>
331
+ readonly max_tokens: number
332
+ readonly system?: string
333
+ readonly temperature?: number
334
+ readonly top_p?: number
335
+ readonly top_k?: number
336
+ readonly stop_sequences?: ReadonlyArray<string>
337
+ readonly thinking?: ThinkingConfig
338
+ readonly tools?: ReadonlyArray<Record<string, unknown>>
339
+ readonly tool_choice?: Record<string, unknown>
340
+ readonly metadata?: { readonly user_id: string }
341
+ readonly output_config?: Record<string, unknown>
342
+ readonly stream: true
343
+ }
344
+
345
+ export const buildRequestBody = (params: {
346
+ readonly model: string
347
+ readonly history: ReadonlyArray<Items.Item>
348
+ readonly maxTokens: number
349
+ readonly temperature: Option.Option<number>
350
+ readonly topP: Option.Option<number>
351
+ readonly topK: Option.Option<number>
352
+ readonly stopSequences: Option.Option<ReadonlyArray<string>>
353
+ readonly thinking: Option.Option<ThinkingConfig>
354
+ readonly tools: Option.Option<ReadonlyArray<Record<string, unknown>>>
355
+ readonly toolChoice: Option.Option<Record<string, unknown>>
356
+ readonly userId: Option.Option<string>
357
+ readonly outputConfig: Option.Option<Record<string, unknown>>
358
+ }): Result.Result<RequestBody, JsonParseError> =>
359
+ pipe(
360
+ groupedMessages(params.history),
361
+ Result.map(
362
+ (messages): RequestBody => ({
363
+ model: params.model,
364
+ messages,
365
+ max_tokens: params.maxTokens,
366
+ ...Option.match(systemFromHistory(params.history), {
367
+ onNone: () => ({}),
368
+ onSome: (system) => ({ system }),
369
+ }),
370
+ ...Option.match(params.temperature, {
371
+ onNone: () => ({}),
372
+ onSome: (temperature) => ({ temperature }),
373
+ }),
374
+ ...Option.match(params.topP, {
375
+ onNone: () => ({}),
376
+ onSome: (top_p) => ({ top_p }),
377
+ }),
378
+ ...Option.match(params.topK, {
379
+ onNone: () => ({}),
380
+ onSome: (top_k) => ({ top_k }),
381
+ }),
382
+ ...Option.match(params.stopSequences, {
383
+ onNone: () => ({}),
384
+ onSome: (stop_sequences) => ({ stop_sequences }),
385
+ }),
386
+ ...Option.match(params.thinking, {
387
+ onNone: () => ({}),
388
+ onSome: (thinking) => ({ thinking }),
389
+ }),
390
+ ...Option.match(params.tools, {
391
+ onNone: () => ({}),
392
+ onSome: (tools) => ({ tools }),
393
+ }),
394
+ ...Option.match(params.toolChoice, {
395
+ onNone: () => ({}),
396
+ onSome: (tool_choice) => ({ tool_choice }),
397
+ }),
398
+ ...Option.match(params.userId, {
399
+ onNone: () => ({}),
400
+ onSome: (user_id) => ({ metadata: { user_id } }),
401
+ }),
402
+ ...Option.match(params.outputConfig, {
403
+ onNone: () => ({}),
404
+ onSome: (output_config) => ({ output_config }),
405
+ }),
406
+ stream: true,
407
+ }),
408
+ ),
409
+ )
410
+
411
+ // ---------------------------------------------------------------------------
412
+ // Stream-level state - assemble content blocks index-by-index, then emit
413
+ // our `Items.Item[]` when `message_stop` lands.
414
+ // ---------------------------------------------------------------------------
415
+
416
+ interface BlockBuffer {
417
+ readonly type: WireContentBlock["type"]
418
+ readonly text: string
419
+ readonly inputJson: string
420
+ readonly thinking: string
421
+ readonly signature: string
422
+ readonly id: Option.Option<string>
423
+ readonly name: Option.Option<string>
424
+ readonly redactedData: Option.Option<string>
425
+ }
426
+
427
+ const emptyBlock = (type: WireContentBlock["type"]): BlockBuffer => ({
428
+ type,
429
+ text: "",
430
+ inputJson: "",
431
+ thinking: "",
432
+ signature: "",
433
+ id: Option.none(),
434
+ name: Option.none(),
435
+ redactedData: Option.none(),
436
+ })
437
+
438
+ export interface Accumulator {
439
+ readonly blocks: Readonly<Record<number, BlockBuffer>>
440
+ readonly stopReason: Option.Option<string>
441
+ readonly usage: Items.Usage
442
+ }
443
+
444
+ export const emptyAccumulator: Accumulator = {
445
+ blocks: {},
446
+ stopReason: Option.none(),
447
+ usage: {},
448
+ }
449
+
450
+ const replaceBlock = (acc: Accumulator, index: number, block: BlockBuffer): Accumulator => ({
451
+ ...acc,
452
+ blocks: { ...acc.blocks, [index]: block },
453
+ })
454
+
455
+ const updateBlock = (
456
+ acc: Accumulator,
457
+ index: number,
458
+ patch: (block: BlockBuffer) => BlockBuffer,
459
+ ): Accumulator => replaceBlock(acc, index, patch(acc.blocks[index] ?? emptyBlock("text")))
460
+
461
+ export const startBlock = (acc: Accumulator, index: number, block: WireContentBlock): Accumulator =>
462
+ Match.value(block).pipe(
463
+ matchType("text", () => replaceBlock(acc, index, emptyBlock("text"))),
464
+ matchType("tool_use", (b) =>
465
+ replaceBlock(acc, index, {
466
+ ...emptyBlock("tool_use"),
467
+ id: Option.some(b.id),
468
+ name: Option.some(b.name),
469
+ inputJson: typeof b.input === "string" ? b.input : "",
470
+ }),
471
+ ),
472
+ matchType("thinking", () => replaceBlock(acc, index, emptyBlock("thinking"))),
473
+ matchType("redacted_thinking", (b) =>
474
+ replaceBlock(acc, index, {
475
+ ...emptyBlock("redacted_thinking"),
476
+ redactedData: Option.some(b.data),
477
+ }),
478
+ ),
479
+ Match.exhaustive,
480
+ )
481
+
482
+ export const appendTextDelta = (acc: Accumulator, index: number, text: string): Accumulator =>
483
+ updateBlock(acc, index, (b) => ({ ...b, text: b.text + text }))
484
+
485
+ export const appendInputJsonDelta = (
486
+ acc: Accumulator,
487
+ index: number,
488
+ partial: string,
489
+ ): Accumulator => updateBlock(acc, index, (b) => ({ ...b, inputJson: b.inputJson + partial }))
490
+
491
+ export const appendThinkingDelta = (
492
+ acc: Accumulator,
493
+ index: number,
494
+ thinking: string,
495
+ ): Accumulator => updateBlock(acc, index, (b) => ({ ...b, thinking: b.thinking + thinking }))
496
+
497
+ export const appendSignatureDelta = (
498
+ acc: Accumulator,
499
+ index: number,
500
+ signature: string,
501
+ ): Accumulator => updateBlock(acc, index, (b) => ({ ...b, signature: b.signature + signature }))
502
+
503
+ export const setStopReason = (acc: Accumulator, reason: string): Accumulator => ({
504
+ ...acc,
505
+ stopReason: Option.some(reason),
506
+ })
507
+
508
+ const cachedFromWire = (wire: WireUsage): Option.Option<number> =>
509
+ Option.fromNullishOr(wire.cache_read_input_tokens)
510
+
511
+ export const mergeUsage = (acc: Accumulator, wire: WireUsage): Accumulator => {
512
+ const cached = cachedFromWire(wire)
513
+ const usage: Items.Usage = {
514
+ ...acc.usage,
515
+ ...(wire.input_tokens !== undefined && { input_tokens: wire.input_tokens }),
516
+ ...(wire.output_tokens !== undefined && { output_tokens: wire.output_tokens }),
517
+ ...(wire.input_tokens !== undefined &&
518
+ wire.output_tokens !== undefined && {
519
+ total_tokens: wire.input_tokens + wire.output_tokens,
520
+ }),
521
+ ...Option.match(cached, {
522
+ onNone: () => ({}),
523
+ onSome: (cached_tokens) => ({ input_tokens_details: { cached_tokens } }),
524
+ }),
525
+ }
526
+ return { ...acc, usage }
527
+ }
528
+
529
+ const stopReasonFromAnthropic = (reason: Option.Option<string>): Turn["stop_reason"] =>
530
+ Option.match(reason, {
531
+ onNone: () => "stop" as const,
532
+ onSome: (r) =>
533
+ Match.value(r).pipe(
534
+ Match.when("tool_use", () => "tool_calls" as const),
535
+ Match.when("max_tokens", () => "max_tokens" as const),
536
+ Match.orElse(() => "stop" as const),
537
+ ),
538
+ })
539
+
540
+ const blocksByIndex = (acc: Accumulator): ReadonlyArray<BlockBuffer> =>
541
+ pipe(
542
+ Object.keys(acc.blocks),
543
+ Arr.map((k) => Number(k)),
544
+ Arr.sort(Order.Number),
545
+ Arr.map((i) => acc.blocks[i]!),
546
+ )
547
+
548
+ const blockToItems = (block: BlockBuffer): ReadonlyArray<Items.Item> =>
549
+ Match.value(block.type).pipe(
550
+ Match.when(
551
+ "text",
552
+ (): ReadonlyArray<Items.Item> =>
553
+ block.text.length === 0
554
+ ? []
555
+ : [
556
+ {
557
+ type: "message",
558
+ role: "assistant",
559
+ content: [{ type: "output_text", text: block.text }],
560
+ },
561
+ ],
562
+ ),
563
+ Match.when(
564
+ "tool_use",
565
+ (): ReadonlyArray<Items.Item> => [
566
+ {
567
+ type: "function_call",
568
+ call_id: Option.getOrElse(block.id, () => ""),
569
+ name: Option.getOrElse(block.name, () => ""),
570
+ arguments: block.inputJson,
571
+ },
572
+ ],
573
+ ),
574
+ Match.when(
575
+ "thinking",
576
+ (): ReadonlyArray<Items.Item> => [
577
+ {
578
+ type: "reasoning",
579
+ ...(block.thinking.length > 0 && { summary: block.thinking }),
580
+ ...(block.signature.length > 0 && { signature: block.signature }),
581
+ },
582
+ ],
583
+ ),
584
+ Match.when(
585
+ "redacted_thinking",
586
+ (): ReadonlyArray<Items.Item> => [
587
+ {
588
+ type: "reasoning",
589
+ ...Option.match(block.redactedData, {
590
+ onNone: () => ({}),
591
+ onSome: (signature) => ({ signature }),
592
+ }),
593
+ },
594
+ ],
595
+ ),
596
+ Match.exhaustive,
597
+ )
598
+
599
+ interface MergeAcc {
600
+ readonly out: ReadonlyArray<Items.Item>
601
+ }
602
+
603
+ const mergeStep = (acc: MergeAcc, item: Items.Item): MergeAcc => {
604
+ const last = Arr.last(acc.out)
605
+ if (
606
+ Option.isSome(last) &&
607
+ last.value.type === "message" &&
608
+ last.value.role === "assistant" &&
609
+ item.type === "message" &&
610
+ item.role === "assistant"
611
+ ) {
612
+ const merged: Items.Message = {
613
+ ...last.value,
614
+ content: [...last.value.content, ...item.content],
615
+ }
616
+ return { out: [...acc.out.slice(0, -1), merged] }
617
+ }
618
+ return { out: [...acc.out, item] }
619
+ }
620
+
621
+ const mergeAdjacentAssistantText = (items: ReadonlyArray<Items.Item>): ReadonlyArray<Items.Item> =>
622
+ Arr.reduce(items, { out: [] } as MergeAcc, mergeStep).out
623
+
624
+ export const accumulatorToTurn = (acc: Accumulator): Turn => ({
625
+ items: pipe(blocksByIndex(acc), Arr.flatMap(blockToItems), mergeAdjacentAssistantText),
626
+ usage: acc.usage,
627
+ stop_reason: stopReasonFromAnthropic(acc.stopReason),
628
+ })
package/src/index.ts ADDED
@@ -0,0 +1,4 @@
1
+ export * from "./Anthropic.js"
2
+ export * from "./models.js"
3
+ export * as codec from "./codec.js"
4
+ export * as streamEvents from "./streamEvents.js"
package/src/models.ts ADDED
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Known Anthropic model identifiers usable via the Messages API (as of
3
+ * April 2026). The `(string & {})` tail keeps autocomplete on the literals
4
+ * while still accepting any string, so newly-released models work without
5
+ * an SDK update.
6
+ *
7
+ * Reference: https://platform.claude.com/docs/en/docs/about-claude/models
8
+ *
9
+ * Latest tier:
10
+ * - `claude-opus-4-7` - most capable; agentic coding focus. Adaptive
11
+ * thinking only (no extended thinking).
12
+ * - `claude-sonnet-4-6` - speed + intelligence balance. Extended +
13
+ * adaptive thinking.
14
+ * - `claude-haiku-4-5-20251001` (alias `claude-haiku-4-5`) - fastest;
15
+ * extended thinking.
16
+ *
17
+ * Deprecated and retiring 2026-06-15:
18
+ * `claude-sonnet-4-20250514` (`claude-sonnet-4-0`),
19
+ * `claude-opus-4-20250514` (`claude-opus-4-0`).
20
+ */
21
+ export type AnthropicModel =
22
+ | "claude-opus-4-7"
23
+ | "claude-sonnet-4-6"
24
+ | "claude-haiku-4-5"
25
+ | "claude-haiku-4-5-20251001"
26
+ | "claude-opus-4-6"
27
+ | "claude-sonnet-4-5"
28
+ | "claude-sonnet-4-5-20250929"
29
+ | "claude-opus-4-5"
30
+ | "claude-opus-4-5-20251101"
31
+ | "claude-opus-4-1"
32
+ | "claude-opus-4-1-20250805"
33
+ // eslint-disable-next-line @typescript-eslint/ban-types
34
+ | (string & {})