@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/LICENSE +21 -0
- package/README.md +43 -0
- package/dist/chunk-CfYAbeIz.mjs +13 -0
- package/dist/index.d.mts +438 -0
- package/dist/index.d.mts.map +1 -0
- package/dist/index.mjs +649 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +54 -0
- package/src/Anthropic.ts +390 -0
- package/src/codec.ts +628 -0
- package/src/index.ts +4 -0
- package/src/models.ts +34 -0
- package/src/streamEvents.ts +221 -0
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
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 & {})
|