@neurocode-ai/llm 1.18.8

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.
Files changed (152) hide show
  1. package/AGENTS.md +321 -0
  2. package/DESIGN.md +1114 -0
  3. package/README.md +131 -0
  4. package/example/call-sites.md +591 -0
  5. package/example/tutorial.ts +255 -0
  6. package/package.json +51 -0
  7. package/script/recording-cost-report.ts +250 -0
  8. package/script/setup-recording-env.ts +542 -0
  9. package/src/cache-policy.ts +111 -0
  10. package/src/index.ts +33 -0
  11. package/src/llm.ts +186 -0
  12. package/src/protocols/anthropic-messages.ts +855 -0
  13. package/src/protocols/bedrock-converse.ts +674 -0
  14. package/src/protocols/bedrock-event-stream.ts +87 -0
  15. package/src/protocols/gemini.ts +512 -0
  16. package/src/protocols/index.ts +6 -0
  17. package/src/protocols/openai-chat.ts +506 -0
  18. package/src/protocols/openai-compatible-chat.ts +24 -0
  19. package/src/protocols/openai-responses.ts +1022 -0
  20. package/src/protocols/shared.ts +326 -0
  21. package/src/protocols/utils/bedrock-auth.ts +70 -0
  22. package/src/protocols/utils/bedrock-cache.ts +37 -0
  23. package/src/protocols/utils/bedrock-media.ts +90 -0
  24. package/src/protocols/utils/cache.ts +16 -0
  25. package/src/protocols/utils/gemini-tool-schema.ts +99 -0
  26. package/src/protocols/utils/lifecycle.ts +102 -0
  27. package/src/protocols/utils/openai-options.ts +93 -0
  28. package/src/protocols/utils/tool-schema.ts +86 -0
  29. package/src/protocols/utils/tool-stream.ts +218 -0
  30. package/src/provider-error.ts +43 -0
  31. package/src/provider.ts +36 -0
  32. package/src/providers/amazon-bedrock.ts +43 -0
  33. package/src/providers/anthropic.ts +35 -0
  34. package/src/providers/azure.ts +110 -0
  35. package/src/providers/cloudflare.ts +127 -0
  36. package/src/providers/github-copilot.ts +69 -0
  37. package/src/providers/google.ts +35 -0
  38. package/src/providers/index.ts +11 -0
  39. package/src/providers/openai-compatible-profile.ts +20 -0
  40. package/src/providers/openai-compatible.ts +65 -0
  41. package/src/providers/openai-options.ts +83 -0
  42. package/src/providers/openai.ts +63 -0
  43. package/src/providers/openrouter.ts +98 -0
  44. package/src/providers/xai.ts +56 -0
  45. package/src/route/auth-options.ts +57 -0
  46. package/src/route/auth.ts +156 -0
  47. package/src/route/client.ts +436 -0
  48. package/src/route/endpoint.ts +53 -0
  49. package/src/route/executor.ts +385 -0
  50. package/src/route/framing.ts +27 -0
  51. package/src/route/index.ts +25 -0
  52. package/src/route/protocol.ts +84 -0
  53. package/src/route/transport/http.ts +155 -0
  54. package/src/route/transport/index.ts +33 -0
  55. package/src/route/transport/websocket.ts +280 -0
  56. package/src/schema/errors.ts +207 -0
  57. package/src/schema/events.ts +618 -0
  58. package/src/schema/ids.ts +43 -0
  59. package/src/schema/index.ts +5 -0
  60. package/src/schema/messages.ts +312 -0
  61. package/src/schema/options.ts +276 -0
  62. package/src/tool-runtime.ts +78 -0
  63. package/src/tool.ts +253 -0
  64. package/src/utils/record.ts +3 -0
  65. package/sst-env.d.ts +10 -0
  66. package/test/adapter.test.ts +171 -0
  67. package/test/auth-options.types.ts +168 -0
  68. package/test/auth.test.ts +103 -0
  69. package/test/cache-policy.test.ts +262 -0
  70. package/test/continuation-scenarios.ts +104 -0
  71. package/test/endpoint.test.ts +58 -0
  72. package/test/executor.test.ts +458 -0
  73. package/test/exports.test.ts +76 -0
  74. package/test/fixtures/media/restroom.png +0 -0
  75. package/test/fixtures/recordings/anthropic-messages/accepts-malformed-assistant-tool-order-with-default-patch.json +29 -0
  76. package/test/fixtures/recordings/anthropic-messages/anthropic-opus-4-7-image-tool-result.json +43 -0
  77. package/test/fixtures/recordings/anthropic-messages/claude-opus-4-7-drives-a-tool-loop.json +56 -0
  78. package/test/fixtures/recordings/anthropic-messages/rejects-malformed-assistant-tool-order-without-patch.json +29 -0
  79. package/test/fixtures/recordings/anthropic-messages/streams-text.json +29 -0
  80. package/test/fixtures/recordings/anthropic-messages/streams-tool-call.json +29 -0
  81. package/test/fixtures/recordings/anthropic-messages-cache/writes-then-reads-cache-control-on-identical-second-call.json +48 -0
  82. package/test/fixtures/recordings/bedrock-converse/drives-a-tool-loop.json +55 -0
  83. package/test/fixtures/recordings/bedrock-converse/streams-a-tool-call.json +29 -0
  84. package/test/fixtures/recordings/bedrock-converse/streams-text.json +29 -0
  85. package/test/fixtures/recordings/cloudflare-ai-gateway/cloudflare-ai-gateway-workers-ai-gpt-oss-20b-tools-tool-call.json +32 -0
  86. package/test/fixtures/recordings/cloudflare-ai-gateway/cloudflare-ai-gateway-workers-ai-llama-3-1-8b-text.json +32 -0
  87. package/test/fixtures/recordings/cloudflare-workers-ai/cloudflare-workers-ai-gpt-oss-20b-tools-tool-call.json +32 -0
  88. package/test/fixtures/recordings/cloudflare-workers-ai/cloudflare-workers-ai-llama-3-1-8b-text.json +32 -0
  89. package/test/fixtures/recordings/gemini/gemini-2-5-flash-image.json +32 -0
  90. package/test/fixtures/recordings/gemini/streams-text.json +28 -0
  91. package/test/fixtures/recordings/gemini/streams-tool-call.json +28 -0
  92. package/test/fixtures/recordings/gemini-cache/reports-cachedcontenttokencount-on-identical-second-call.json +46 -0
  93. package/test/fixtures/recordings/openai-chat/continues-after-tool-result.json +28 -0
  94. package/test/fixtures/recordings/openai-chat/drives-a-tool-loop-end-to-end.json +46 -0
  95. package/test/fixtures/recordings/openai-chat/streams-text.json +28 -0
  96. package/test/fixtures/recordings/openai-chat/streams-tool-call.json +28 -0
  97. package/test/fixtures/recordings/openai-compatible-chat/deepseek-streams-text.json +28 -0
  98. package/test/fixtures/recordings/openai-compatible-chat/groq-llama-3-3-70b-drives-a-tool-loop.json +53 -0
  99. package/test/fixtures/recordings/openai-compatible-chat/groq-streams-text.json +28 -0
  100. package/test/fixtures/recordings/openai-compatible-chat/groq-streams-tool-call.json +28 -0
  101. package/test/fixtures/recordings/openai-compatible-chat/openrouter-claude-opus-4-7-drives-a-tool-loop.json +54 -0
  102. package/test/fixtures/recordings/openai-compatible-chat/openrouter-gpt-4o-mini-drives-a-tool-loop.json +53 -0
  103. package/test/fixtures/recordings/openai-compatible-chat/openrouter-gpt-5-5-drives-a-tool-loop.json +54 -0
  104. package/test/fixtures/recordings/openai-compatible-chat/openrouter-streams-text.json +28 -0
  105. package/test/fixtures/recordings/openai-compatible-chat/openrouter-streams-tool-call.json +28 -0
  106. package/test/fixtures/recordings/openai-compatible-chat/togetherai-streams-text.json +28 -0
  107. package/test/fixtures/recordings/openai-compatible-chat/togetherai-streams-tool-call.json +28 -0
  108. package/test/fixtures/recordings/openai-responses/gpt-5-5-drives-a-tool-loop.json +54 -0
  109. package/test/fixtures/recordings/openai-responses/gpt-5-5-streams-text.json +28 -0
  110. package/test/fixtures/recordings/openai-responses/gpt-5-5-streams-tool-call.json +28 -0
  111. package/test/fixtures/recordings/openai-responses/openai-responses-gpt-5-5-image-tool-result.json +42 -0
  112. package/test/fixtures/recordings/openai-responses/openai-responses-gpt-5-5-reasoning-continuation.json +58 -0
  113. package/test/fixtures/recordings/openai-responses/openai-responses-gpt-5-5-reasoning.json +32 -0
  114. package/test/fixtures/recordings/openai-responses-cache/reports-cached-tokens-on-identical-second-call.json +46 -0
  115. package/test/generate-object.test.ts +184 -0
  116. package/test/lib/effect.ts +50 -0
  117. package/test/lib/http.ts +98 -0
  118. package/test/lib/openai-chunks.ts +27 -0
  119. package/test/lib/sse.ts +17 -0
  120. package/test/lib/tool-runtime.ts +146 -0
  121. package/test/llm.test.ts +199 -0
  122. package/test/prepare.test.ts +178 -0
  123. package/test/provider/anthropic-messages-cache.recorded.test.ts +53 -0
  124. package/test/provider/anthropic-messages.recorded.test.ts +45 -0
  125. package/test/provider/anthropic-messages.test.ts +895 -0
  126. package/test/provider/bedrock-converse-cache.recorded.test.ts +54 -0
  127. package/test/provider/bedrock-converse.test.ts +744 -0
  128. package/test/provider/cloudflare.test.ts +230 -0
  129. package/test/provider/gemini-cache.recorded.test.ts +48 -0
  130. package/test/provider/gemini.test.ts +584 -0
  131. package/test/provider/golden.recorded.test.ts +223 -0
  132. package/test/provider/openai-chat.test.ts +674 -0
  133. package/test/provider/openai-compatible-chat.test.ts +238 -0
  134. package/test/provider/openai-responses-cache.recorded.test.ts +46 -0
  135. package/test/provider/openai-responses.test.ts +1472 -0
  136. package/test/provider/openrouter.test.ts +56 -0
  137. package/test/provider-error.test.ts +30 -0
  138. package/test/provider.types.ts +41 -0
  139. package/test/recorded-golden.ts +97 -0
  140. package/test/recorded-runner.ts +100 -0
  141. package/test/recorded-scenarios.ts +531 -0
  142. package/test/recorded-test.ts +94 -0
  143. package/test/recorded-utils.ts +56 -0
  144. package/test/recorded-websocket.ts +26 -0
  145. package/test/response.test.ts +98 -0
  146. package/test/route.test.ts +43 -0
  147. package/test/schema.test.ts +86 -0
  148. package/test/tool-runtime.test.ts +818 -0
  149. package/test/tool-schema-projection.test.ts +117 -0
  150. package/test/tool-stream.test.ts +99 -0
  151. package/test/tool.types.ts +40 -0
  152. package/tsconfig.json +15 -0
@@ -0,0 +1,855 @@
1
+ import { Effect, Schema } from "effect"
2
+ import { Route } from "../route/client"
3
+ import { Auth } from "../route/auth"
4
+ import { Endpoint } from "../route/endpoint"
5
+ import { Framing } from "../route/framing"
6
+ import { Protocol } from "../route/protocol"
7
+ import {
8
+ LLMEvent,
9
+ Usage,
10
+ type CacheHint,
11
+ type FinishReason,
12
+ type JsonSchema,
13
+ type LLMRequest,
14
+ type MediaPart,
15
+ type ProviderMetadata,
16
+ type ToolCallPart,
17
+ type ToolDefinition,
18
+ type ToolContent,
19
+ type ToolResultPart,
20
+ } from "../schema"
21
+ import { JsonObject, optionalArray, optionalNull, ProviderShared } from "./shared"
22
+ import { isContextOverflow } from "../provider-error"
23
+ import * as Cache from "./utils/cache"
24
+ import { Lifecycle } from "./utils/lifecycle"
25
+ import { ToolSchemaProjection } from "./utils/tool-schema"
26
+ import { ToolStream } from "./utils/tool-stream"
27
+
28
+ const ADAPTER = "anthropic-messages"
29
+ export const DEFAULT_BASE_URL = "https://api.anthropic.com/v1"
30
+ export const PATH = "/messages"
31
+
32
+ // =============================================================================
33
+ // Request Body Schema
34
+ // =============================================================================
35
+ const AnthropicCacheControl = Schema.Struct({
36
+ type: Schema.tag("ephemeral"),
37
+ ttl: Schema.optional(Schema.Literals(["5m", "1h"])),
38
+ })
39
+
40
+ const AnthropicTextBlock = Schema.Struct({
41
+ type: Schema.tag("text"),
42
+ text: Schema.String,
43
+ cache_control: Schema.optional(AnthropicCacheControl),
44
+ })
45
+ type AnthropicTextBlock = Schema.Schema.Type<typeof AnthropicTextBlock>
46
+
47
+ const AnthropicImageBlock = Schema.Struct({
48
+ type: Schema.tag("image"),
49
+ source: Schema.Struct({
50
+ type: Schema.tag("base64"),
51
+ media_type: Schema.String,
52
+ data: Schema.String,
53
+ }),
54
+ cache_control: Schema.optional(AnthropicCacheControl),
55
+ })
56
+ type AnthropicImageBlock = Schema.Schema.Type<typeof AnthropicImageBlock>
57
+
58
+ const AnthropicThinkingBlock = Schema.Struct({
59
+ type: Schema.tag("thinking"),
60
+ thinking: Schema.String,
61
+ signature: Schema.optional(Schema.String),
62
+ cache_control: Schema.optional(AnthropicCacheControl),
63
+ })
64
+
65
+ const AnthropicToolUseBlock = Schema.Struct({
66
+ type: Schema.tag("tool_use"),
67
+ id: Schema.String,
68
+ name: Schema.String,
69
+ input: Schema.Unknown,
70
+ cache_control: Schema.optional(AnthropicCacheControl),
71
+ })
72
+ type AnthropicToolUseBlock = Schema.Schema.Type<typeof AnthropicToolUseBlock>
73
+
74
+ const AnthropicServerToolUseBlock = Schema.Struct({
75
+ type: Schema.tag("server_tool_use"),
76
+ id: Schema.String,
77
+ name: Schema.String,
78
+ input: Schema.Unknown,
79
+ cache_control: Schema.optional(AnthropicCacheControl),
80
+ })
81
+ type AnthropicServerToolUseBlock = Schema.Schema.Type<typeof AnthropicServerToolUseBlock>
82
+
83
+ // Server tool result blocks: web_search_tool_result, code_execution_tool_result,
84
+ // and web_fetch_tool_result. The provider executes the tool and inlines the
85
+ // structured result into the assistant turn — there is no client tool_result
86
+ // round-trip. We round-trip the structured `content` payload as opaque JSON so
87
+ // the next request can echo it back when continuing the conversation.
88
+ const AnthropicServerToolResultType = Schema.Literals([
89
+ "web_search_tool_result",
90
+ "code_execution_tool_result",
91
+ "web_fetch_tool_result",
92
+ ])
93
+ type AnthropicServerToolResultType = Schema.Schema.Type<typeof AnthropicServerToolResultType>
94
+
95
+ const AnthropicServerToolResultBlock = Schema.Struct({
96
+ type: AnthropicServerToolResultType,
97
+ tool_use_id: Schema.String,
98
+ content: Schema.Unknown,
99
+ cache_control: Schema.optional(AnthropicCacheControl),
100
+ })
101
+ type AnthropicServerToolResultBlock = Schema.Schema.Type<typeof AnthropicServerToolResultBlock>
102
+
103
+ // Anthropic accepts either a plain string or an ordered array of text/image
104
+ // blocks inside `tool_result.content`. The array form is required when a tool
105
+ // returns image bytes (screenshot, image search, etc.) so they can be passed
106
+ // to the model as proper image inputs instead of being JSON-stringified into
107
+ // the prompt — which silently inflates context by megabytes and can push the
108
+ // conversation over the model's token limit.
109
+ const AnthropicToolResultContent = Schema.Union([AnthropicTextBlock, AnthropicImageBlock])
110
+
111
+ const AnthropicToolResultBlock = Schema.Struct({
112
+ type: Schema.tag("tool_result"),
113
+ tool_use_id: Schema.String,
114
+ content: Schema.Union([Schema.String, Schema.Array(AnthropicToolResultContent)]),
115
+ is_error: Schema.optional(Schema.Boolean),
116
+ cache_control: Schema.optional(AnthropicCacheControl),
117
+ })
118
+
119
+ const AnthropicUserBlock = Schema.Union([AnthropicTextBlock, AnthropicImageBlock, AnthropicToolResultBlock])
120
+ type AnthropicUserBlock = Schema.Schema.Type<typeof AnthropicUserBlock>
121
+ const AnthropicAssistantBlock = Schema.Union([
122
+ AnthropicTextBlock,
123
+ AnthropicThinkingBlock,
124
+ AnthropicToolUseBlock,
125
+ AnthropicServerToolUseBlock,
126
+ AnthropicServerToolResultBlock,
127
+ ])
128
+ type AnthropicAssistantBlock = Schema.Schema.Type<typeof AnthropicAssistantBlock>
129
+ type AnthropicToolResultBlock = Schema.Schema.Type<typeof AnthropicToolResultBlock>
130
+
131
+ const AnthropicMessage = Schema.Union([
132
+ Schema.Struct({ role: Schema.Literal("user"), content: Schema.Array(AnthropicUserBlock) }),
133
+ Schema.Struct({ role: Schema.Literal("assistant"), content: Schema.Array(AnthropicAssistantBlock) }),
134
+ Schema.Struct({ role: Schema.Literal("system"), content: Schema.Array(AnthropicTextBlock) }),
135
+ ]).pipe(Schema.toTaggedUnion("role"))
136
+ type AnthropicMessage = Schema.Schema.Type<typeof AnthropicMessage>
137
+
138
+ const AnthropicTool = Schema.Struct({
139
+ name: Schema.String,
140
+ description: Schema.String,
141
+ input_schema: JsonObject,
142
+ cache_control: Schema.optional(AnthropicCacheControl),
143
+ })
144
+ type AnthropicTool = Schema.Schema.Type<typeof AnthropicTool>
145
+
146
+ const AnthropicToolChoice = Schema.Union([
147
+ Schema.Struct({ type: Schema.Literals(["auto", "any"]) }),
148
+ Schema.Struct({ type: Schema.tag("tool"), name: Schema.String }),
149
+ ])
150
+
151
+ const AnthropicThinking = Schema.Struct({
152
+ type: Schema.tag("enabled"),
153
+ budget_tokens: Schema.Number,
154
+ })
155
+
156
+ const AnthropicBodyFields = {
157
+ model: Schema.String,
158
+ system: optionalArray(AnthropicTextBlock),
159
+ messages: Schema.Array(AnthropicMessage),
160
+ tools: optionalArray(AnthropicTool),
161
+ tool_choice: Schema.optional(AnthropicToolChoice),
162
+ stream: Schema.Literal(true),
163
+ max_tokens: Schema.Number,
164
+ temperature: Schema.optional(Schema.Number),
165
+ top_p: Schema.optional(Schema.Number),
166
+ top_k: Schema.optional(Schema.Number),
167
+ stop_sequences: optionalArray(Schema.String),
168
+ thinking: Schema.optional(AnthropicThinking),
169
+ }
170
+ const AnthropicMessagesBody = Schema.Struct(AnthropicBodyFields)
171
+ export type AnthropicMessagesBody = Schema.Schema.Type<typeof AnthropicMessagesBody>
172
+
173
+ const AnthropicUsage = Schema.Struct({
174
+ input_tokens: Schema.optional(Schema.Number),
175
+ output_tokens: Schema.optional(Schema.Number),
176
+ cache_creation_input_tokens: optionalNull(Schema.Number),
177
+ cache_read_input_tokens: optionalNull(Schema.Number),
178
+ })
179
+ type AnthropicUsage = Schema.Schema.Type<typeof AnthropicUsage>
180
+
181
+ const AnthropicStreamBlock = Schema.Struct({
182
+ type: Schema.String,
183
+ id: Schema.optional(Schema.String),
184
+ name: Schema.optional(Schema.String),
185
+ text: Schema.optional(Schema.String),
186
+ thinking: Schema.optional(Schema.String),
187
+ signature: Schema.optional(Schema.String),
188
+ input: Schema.optional(Schema.Unknown),
189
+ // *_tool_result blocks arrive whole as content_block_start (no streaming
190
+ // delta) with the structured payload in `content` and the originating
191
+ // server_tool_use id in `tool_use_id`.
192
+ tool_use_id: Schema.optional(Schema.String),
193
+ content: Schema.optional(Schema.Unknown),
194
+ })
195
+
196
+ const AnthropicStreamDelta = Schema.Struct({
197
+ type: Schema.optional(Schema.String),
198
+ text: Schema.optional(Schema.String),
199
+ thinking: Schema.optional(Schema.String),
200
+ partial_json: Schema.optional(Schema.String),
201
+ signature: Schema.optional(Schema.String),
202
+ stop_reason: optionalNull(Schema.String),
203
+ stop_sequence: optionalNull(Schema.String),
204
+ })
205
+
206
+ const AnthropicEvent = Schema.Struct({
207
+ type: Schema.String,
208
+ index: Schema.optional(Schema.Number),
209
+ message: Schema.optional(Schema.Struct({ usage: Schema.optional(AnthropicUsage) })),
210
+ content_block: Schema.optional(AnthropicStreamBlock),
211
+ delta: Schema.optional(AnthropicStreamDelta),
212
+ usage: Schema.optional(AnthropicUsage),
213
+ // `type` and `message` are both required per Anthropic's spec, but
214
+ // OpenAI-compatible proxies and gateway translations occasionally drop one
215
+ // or the other; mark them optional so a partial payload still parses and
216
+ // the parser can fall back to whichever field is populated.
217
+ error: Schema.optional(
218
+ Schema.Struct({ type: Schema.optional(Schema.String), message: Schema.optional(Schema.String) }),
219
+ ),
220
+ })
221
+ type AnthropicEvent = Schema.Schema.Type<typeof AnthropicEvent>
222
+
223
+ interface ParserState {
224
+ readonly tools: ToolStream.State<number>
225
+ readonly usage?: Usage
226
+ readonly lifecycle: Lifecycle.State
227
+ }
228
+
229
+ const invalid = ProviderShared.invalidRequest
230
+
231
+ // =============================================================================
232
+ // Request Lowering
233
+ // =============================================================================
234
+ // Anthropic accepts at most 4 explicit cache_control breakpoints per request,
235
+ // across `tools`, `system`, and `messages`. Beyond the cap the API returns a
236
+ // 400 — so the lowering layer counts emitted markers and silently drops any
237
+ // that exceed it.
238
+ const ANTHROPIC_BREAKPOINT_CAP = 4
239
+
240
+ const EPHEMERAL_5M = { type: "ephemeral" as const }
241
+ const EPHEMERAL_1H = { type: "ephemeral" as const, ttl: "1h" as const }
242
+
243
+ const cacheControl = (breakpoints: Cache.Breakpoints, cache: CacheHint | undefined) => {
244
+ if (cache?.type !== "ephemeral" && cache?.type !== "persistent") return undefined
245
+ if (breakpoints.remaining <= 0) {
246
+ breakpoints.dropped += 1
247
+ return undefined
248
+ }
249
+ breakpoints.remaining -= 1
250
+ return Cache.ttlBucket(cache.ttlSeconds) === "1h" ? EPHEMERAL_1H : EPHEMERAL_5M
251
+ }
252
+
253
+ const anthropicMetadata = (metadata: Record<string, unknown>): ProviderMetadata => ({ anthropic: metadata })
254
+
255
+ const signatureFromMetadata = (metadata: ProviderMetadata | undefined): string | undefined => {
256
+ const anthropic = metadata?.anthropic
257
+ if (!ProviderShared.isRecord(anthropic)) return undefined
258
+ return typeof anthropic.signature === "string" ? anthropic.signature : undefined
259
+ }
260
+
261
+ const lowerTool = (breakpoints: Cache.Breakpoints, tool: ToolDefinition, inputSchema: JsonSchema): AnthropicTool => ({
262
+ name: tool.name,
263
+ description: tool.description,
264
+ input_schema: inputSchema,
265
+ cache_control: cacheControl(breakpoints, tool.cache),
266
+ })
267
+
268
+ const lowerToolChoice = (toolChoice: NonNullable<LLMRequest["toolChoice"]>) =>
269
+ ProviderShared.matchToolChoice("Anthropic Messages", toolChoice, {
270
+ auto: () => ({ type: "auto" as const }),
271
+ none: () => undefined,
272
+ required: () => ({ type: "any" as const }),
273
+ tool: (name) => ({ type: "tool" as const, name }),
274
+ })
275
+
276
+ const lowerToolCall = (part: ToolCallPart): AnthropicToolUseBlock => ({
277
+ type: "tool_use",
278
+ id: part.id,
279
+ name: part.name,
280
+ input: part.input,
281
+ })
282
+
283
+ const lowerServerToolCall = (part: ToolCallPart): AnthropicServerToolUseBlock => ({
284
+ type: "server_tool_use",
285
+ id: part.id,
286
+ name: part.name,
287
+ input: part.input,
288
+ })
289
+
290
+ // Server tool result blocks are typed by name. Anthropic ships three today;
291
+ // extend this list when new server tools land. The block content is the
292
+ // structured payload returned by the provider, which we round-trip as-is.
293
+ const serverToolResultType = (name: string): AnthropicServerToolResultType | undefined => {
294
+ if (name === "web_search") return "web_search_tool_result"
295
+ if (name === "code_execution") return "code_execution_tool_result"
296
+ if (name === "web_fetch") return "web_fetch_tool_result"
297
+ return undefined
298
+ }
299
+
300
+ const lowerServerToolResult = Effect.fn("AnthropicMessages.lowerServerToolResult")(function* (part: ToolResultPart) {
301
+ const wireType = serverToolResultType(part.name)
302
+ if (!wireType)
303
+ return yield* invalid(`Anthropic Messages does not know how to round-trip server tool result for ${part.name}`)
304
+ return { type: wireType, tool_use_id: part.id, content: part.result.value } satisfies AnthropicServerToolResultBlock
305
+ })
306
+
307
+ const lowerImage = Effect.fn("AnthropicMessages.lowerImage")(function* (part: MediaPart) {
308
+ const media = yield* ProviderShared.validateMedia(
309
+ "Anthropic Messages",
310
+ part,
311
+ new Set<string>(ProviderShared.IMAGE_MIMES),
312
+ )
313
+ return {
314
+ type: "image" as const,
315
+ source: {
316
+ type: "base64" as const,
317
+ media_type: media.mime,
318
+ data: media.base64,
319
+ },
320
+ } satisfies AnthropicImageBlock
321
+ })
322
+
323
+ // Tool results may carry structured text/images. Keep media as provider-native
324
+ // content instead of JSON-stringifying base64 into a prompt string.
325
+ const lowerToolResultContentItem = Effect.fn("AnthropicMessages.lowerToolResultContentItem")(function* (
326
+ item: ToolContent,
327
+ ) {
328
+ if (item.type === "text") return { type: "text" as const, text: item.text } satisfies AnthropicTextBlock
329
+ const media = yield* ProviderShared.validateToolFile(
330
+ "Anthropic Messages",
331
+ item,
332
+ new Set<string>(ProviderShared.IMAGE_MIMES),
333
+ )
334
+ return {
335
+ type: "image" as const,
336
+ source: {
337
+ type: "base64" as const,
338
+ media_type: media.mime,
339
+ data: media.base64,
340
+ },
341
+ } satisfies AnthropicImageBlock
342
+ })
343
+
344
+ const lowerToolResultContent = Effect.fn("AnthropicMessages.lowerToolResultContent")(function* (part: ToolResultPart) {
345
+ // Text / json / error results stay as a string for backward compatibility
346
+ // with existing cassettes and provider expectations.
347
+ if (part.result.type !== "content") return ProviderShared.toolResultText(part)
348
+ // Preserve the narrowed array element type when compiled through a consumer package.
349
+ const content: ReadonlyArray<ToolContent> = part.result.value
350
+ return yield* Effect.forEach(content, lowerToolResultContentItem)
351
+ })
352
+
353
+ // Mid-conversation system messages are a native Claude API feature only for
354
+ // Opus 4.8. Other Anthropic models intentionally use the same visible wrapped-
355
+ // user fallback as non-Anthropic routes rather than sending a role they reject.
356
+ const supportsNativeSystemUpdates = (request: LLMRequest) => String(request.model.id) === "claude-opus-4-8"
357
+
358
+ const endsInServerToolUse = (message: LLMRequest["messages"][number]) => {
359
+ const last = message.content.at(-1)
360
+ return message.role === "assistant" && last?.type === "tool-call" && last.providerExecuted === true
361
+ }
362
+
363
+ const canUseNativeSystemUpdate = (messages: LLMRequest["messages"], index: number) => {
364
+ const previous = messages[index - 1]
365
+ const next = messages[index + 1]
366
+ return (
367
+ previous !== undefined &&
368
+ previous.role !== "system" &&
369
+ (previous.role === "user" || previous.role === "tool" || endsInServerToolUse(previous)) &&
370
+ next?.role !== "system" &&
371
+ (next === undefined || next.role === "assistant")
372
+ )
373
+ }
374
+
375
+ const splitsLocalToolResults = (messages: LLMRequest["messages"], index: number) => {
376
+ const pending = new Set<string>()
377
+ for (const message of messages.slice(0, index)) {
378
+ for (const part of message.content) {
379
+ if (message.role === "assistant" && part.type === "tool-call" && part.providerExecuted !== true)
380
+ pending.add(part.id)
381
+ if (message.role === "tool" && part.type === "tool-result") pending.delete(part.id)
382
+ }
383
+ }
384
+ return pending.size > 0
385
+ }
386
+
387
+ const lowerNativeSystemUpdate = Effect.fn("AnthropicMessages.lowerNativeSystemUpdate")(function* (
388
+ message: LLMRequest["messages"][number],
389
+ breakpoints: Cache.Breakpoints,
390
+ ) {
391
+ const content = yield* ProviderShared.systemUpdateText("Anthropic Messages", message)
392
+ return {
393
+ role: "system" as const,
394
+ content: content.map((part) => ({
395
+ type: "text" as const,
396
+ text: part.text,
397
+ cache_control: cacheControl(breakpoints, part.cache),
398
+ })),
399
+ }
400
+ })
401
+
402
+ const lowerMessages = Effect.fn("AnthropicMessages.lowerMessages")(function* (
403
+ request: LLMRequest,
404
+ breakpoints: Cache.Breakpoints,
405
+ ) {
406
+ const messages: AnthropicMessage[] = []
407
+
408
+ for (const [index, message] of request.messages.entries()) {
409
+ if (message.role === "system") {
410
+ if (splitsLocalToolResults(request.messages, index))
411
+ return yield* invalid("Anthropic Messages system updates cannot split a local tool call from its tool result")
412
+ if (supportsNativeSystemUpdates(request) && canUseNativeSystemUpdate(request.messages, index)) {
413
+ messages.push(yield* lowerNativeSystemUpdate(message, breakpoints))
414
+ continue
415
+ }
416
+ const part = yield* ProviderShared.wrappedSystemUpdate("Anthropic Messages", message)
417
+ const block = { type: "text" as const, text: part.text, cache_control: cacheControl(breakpoints, part.cache) }
418
+ const previous = messages.at(-1)
419
+ if (previous?.role === "user")
420
+ messages[messages.length - 1] = { role: "user", content: [...previous.content, block] }
421
+ else messages.push({ role: "user", content: [block] })
422
+ continue
423
+ }
424
+
425
+ if (message.role === "user") {
426
+ const content: AnthropicUserBlock[] = []
427
+ for (const part of message.content) {
428
+ if (part.type === "text") {
429
+ content.push({ type: "text", text: part.text, cache_control: cacheControl(breakpoints, part.cache) })
430
+ continue
431
+ }
432
+ if (part.type === "media") {
433
+ content.push(yield* lowerImage(part))
434
+ continue
435
+ }
436
+ return yield* ProviderShared.unsupportedContent("Anthropic Messages", "user", ["text", "media"])
437
+ }
438
+ messages.push({ role: "user", content })
439
+ continue
440
+ }
441
+
442
+ if (message.role === "assistant") {
443
+ const content: AnthropicAssistantBlock[] = []
444
+ for (const part of message.content) {
445
+ if (part.type === "text") {
446
+ content.push({ type: "text", text: part.text, cache_control: cacheControl(breakpoints, part.cache) })
447
+ continue
448
+ }
449
+ if (part.type === "reasoning") {
450
+ content.push({
451
+ type: "thinking",
452
+ thinking: part.text,
453
+ signature: part.encrypted ?? signatureFromMetadata(part.providerMetadata),
454
+ })
455
+ continue
456
+ }
457
+ if (part.type === "tool-call") {
458
+ content.push(part.providerExecuted ? lowerServerToolCall(part) : lowerToolCall(part))
459
+ continue
460
+ }
461
+ if (part.type === "tool-result" && part.providerExecuted) {
462
+ content.push(yield* lowerServerToolResult(part))
463
+ continue
464
+ }
465
+ return yield* invalid(
466
+ `Anthropic Messages assistant messages only support text, reasoning, and tool-call content for now`,
467
+ )
468
+ }
469
+ messages.push({ role: "assistant", content })
470
+ continue
471
+ }
472
+
473
+ const content: AnthropicToolResultBlock[] = []
474
+ for (const part of message.content) {
475
+ if (!ProviderShared.supportsContent(part, ["tool-result"]))
476
+ return yield* ProviderShared.unsupportedContent("Anthropic Messages", "tool", ["tool-result"])
477
+ content.push({
478
+ type: "tool_result",
479
+ tool_use_id: part.id,
480
+ content: yield* lowerToolResultContent(part),
481
+ is_error: part.result.type === "error" ? true : undefined,
482
+ cache_control: cacheControl(breakpoints, part.cache),
483
+ })
484
+ }
485
+ messages.push({ role: "user", content })
486
+ }
487
+
488
+ return messages
489
+ })
490
+
491
+ const anthropicOptions = (request: LLMRequest) => request.providerOptions?.anthropic
492
+
493
+ const lowerThinking = Effect.fn("AnthropicMessages.lowerThinking")(function* (request: LLMRequest) {
494
+ const thinking = anthropicOptions(request)?.thinking
495
+ if (!ProviderShared.isRecord(thinking) || thinking.type !== "enabled") return undefined
496
+ const budget =
497
+ typeof thinking.budgetTokens === "number"
498
+ ? thinking.budgetTokens
499
+ : typeof thinking.budget_tokens === "number"
500
+ ? thinking.budget_tokens
501
+ : undefined
502
+ if (budget === undefined) return yield* invalid("Anthropic thinking provider option requires budgetTokens")
503
+ return { type: "enabled" as const, budget_tokens: budget }
504
+ })
505
+
506
+ const fromRequest = Effect.fn("AnthropicMessages.fromRequest")(function* (request: LLMRequest) {
507
+ const toolChoice = request.toolChoice ? yield* lowerToolChoice(request.toolChoice) : undefined
508
+ const generation = request.generation
509
+ const toolSchemaCompatibility = request.model.compatibility?.toolSchema
510
+ const outputLimit = request.model.defaults?.limits?.output ?? request.model.route.defaults.limits?.output ?? 4096
511
+ // Allocate the 4-breakpoint budget in invalidation order: tools → system →
512
+ // messages. Tools live highest in the cache hierarchy, so when callers
513
+ // over-mark we keep their tool hints and shed the message-tail ones first.
514
+ const breakpoints = Cache.newBreakpoints(ANTHROPIC_BREAKPOINT_CAP)
515
+ const tools =
516
+ request.tools.length === 0 || request.toolChoice?.type === "none"
517
+ ? undefined
518
+ : request.tools.map((tool) =>
519
+ lowerTool(
520
+ breakpoints,
521
+ tool,
522
+ ToolSchemaProjection.modelCompatibility(tool.inputSchema, toolSchemaCompatibility),
523
+ ),
524
+ )
525
+ const system =
526
+ request.system.length === 0
527
+ ? undefined
528
+ : request.system.map((part) => ({
529
+ type: "text" as const,
530
+ text: part.text,
531
+ cache_control: cacheControl(breakpoints, part.cache),
532
+ }))
533
+ const messages = yield* lowerMessages(request, breakpoints)
534
+ if (breakpoints.dropped > 0) {
535
+ yield* Effect.logWarning(
536
+ `Anthropic Messages: dropped ${breakpoints.dropped} cache breakpoint(s); the API allows at most ${ANTHROPIC_BREAKPOINT_CAP} per request.`,
537
+ )
538
+ }
539
+ return {
540
+ model: request.model.id,
541
+ system,
542
+ messages,
543
+ tools,
544
+ tool_choice: toolChoice,
545
+ stream: true as const,
546
+ max_tokens: generation?.maxTokens ?? outputLimit,
547
+ temperature: generation?.temperature,
548
+ top_p: generation?.topP,
549
+ top_k: generation?.topK,
550
+ stop_sequences: generation?.stop,
551
+ thinking: yield* lowerThinking(request),
552
+ }
553
+ })
554
+
555
+ // =============================================================================
556
+ // Stream Parsing
557
+ // =============================================================================
558
+ const mapFinishReason = (reason: string | null | undefined): FinishReason => {
559
+ if (reason === "end_turn" || reason === "stop_sequence" || reason === "pause_turn") return "stop"
560
+ if (reason === "max_tokens") return "length"
561
+ if (reason === "tool_use") return "tool-calls"
562
+ if (reason === "refusal") return "content-filter"
563
+ return "unknown"
564
+ }
565
+
566
+ // Anthropic reports the non-overlapping breakdown natively — its
567
+ // `input_tokens` is the *non-cached* count per the Messages API docs, with
568
+ // cache reads and writes as separate fields. We sum them to derive the
569
+ // inclusive `inputTokens` the rest of the contract expects. Extended
570
+ // thinking tokens are *not* broken out by Anthropic — they're billed as
571
+ // part of `output_tokens`, so `reasoningTokens` stays `undefined` and
572
+ // `outputTokens` carries the combined total.
573
+ const mapUsage = (usage: AnthropicUsage | undefined): Usage | undefined => {
574
+ if (!usage) return undefined
575
+ const nonCached = usage.input_tokens
576
+ const cacheRead = usage.cache_read_input_tokens ?? undefined
577
+ const cacheWrite = usage.cache_creation_input_tokens ?? undefined
578
+ const inputTokens = ProviderShared.sumTokens(nonCached, cacheRead, cacheWrite)
579
+ return new Usage({
580
+ inputTokens,
581
+ outputTokens: usage.output_tokens,
582
+ nonCachedInputTokens: nonCached,
583
+ cacheReadInputTokens: cacheRead,
584
+ cacheWriteInputTokens: cacheWrite,
585
+ totalTokens: ProviderShared.totalTokens(inputTokens, usage.output_tokens, undefined),
586
+ providerMetadata: { anthropic: usage },
587
+ })
588
+ }
589
+
590
+ // Anthropic emits usage on `message_start` and again on `message_delta` — the
591
+ // final delta carries the authoritative totals. Right-biased merge: each
592
+ // field prefers `right` when defined, falls back to `left`. `inputTokens` is
593
+ // recomputed from the merged breakdown so the inclusive total stays
594
+ // consistent with `nonCached + cacheRead + cacheWrite`.
595
+ const mergeUsage = (left: Usage | undefined, right: Usage | undefined) => {
596
+ if (!left) return right
597
+ if (!right) return left
598
+ const nonCachedInputTokens = right.nonCachedInputTokens ?? left.nonCachedInputTokens
599
+ const cacheReadInputTokens = right.cacheReadInputTokens ?? left.cacheReadInputTokens
600
+ const cacheWriteInputTokens = right.cacheWriteInputTokens ?? left.cacheWriteInputTokens
601
+ const inputTokens = ProviderShared.sumTokens(nonCachedInputTokens, cacheReadInputTokens, cacheWriteInputTokens)
602
+ const outputTokens = right.outputTokens ?? left.outputTokens
603
+ return new Usage({
604
+ inputTokens,
605
+ outputTokens,
606
+ nonCachedInputTokens,
607
+ cacheReadInputTokens,
608
+ cacheWriteInputTokens,
609
+ totalTokens: ProviderShared.totalTokens(inputTokens, outputTokens, undefined),
610
+ providerMetadata: {
611
+ anthropic: {
612
+ ...left.providerMetadata?.["anthropic"],
613
+ ...right.providerMetadata?.["anthropic"],
614
+ },
615
+ },
616
+ })
617
+ }
618
+
619
+ // Server tool result blocks come whole in `content_block_start` (no streaming
620
+ // delta sequence). We convert the payload to a `tool-result` event with
621
+ // `providerExecuted: true`. The runtime appends it to the assistant message
622
+ // for round-trip; downstream consumers can inspect `result.value` for the
623
+ // structured payload.
624
+ const SERVER_TOOL_RESULT_NAMES: Record<AnthropicServerToolResultType, string> = {
625
+ web_search_tool_result: "web_search",
626
+ code_execution_tool_result: "code_execution",
627
+ web_fetch_tool_result: "web_fetch",
628
+ }
629
+
630
+ const isServerToolResultType = (type: string): type is AnthropicServerToolResultType => type in SERVER_TOOL_RESULT_NAMES
631
+
632
+ const serverToolResultEvent = (block: NonNullable<AnthropicEvent["content_block"]>): LLMEvent | undefined => {
633
+ if (!block.type || !isServerToolResultType(block.type)) return undefined
634
+ const errorPayload =
635
+ typeof block.content === "object" && block.content !== null && "type" in block.content
636
+ ? String((block.content as Record<string, unknown>).type)
637
+ : ""
638
+ const isError = errorPayload.endsWith("_tool_result_error")
639
+ return LLMEvent.toolResult({
640
+ id: block.tool_use_id ?? "",
641
+ name: SERVER_TOOL_RESULT_NAMES[block.type],
642
+ result: isError ? { type: "error", value: block.content } : { type: "json", value: block.content },
643
+ providerExecuted: true,
644
+ providerMetadata: anthropicMetadata({ blockType: block.type }),
645
+ })
646
+ }
647
+
648
+ type StepResult = readonly [ParserState, ReadonlyArray<LLMEvent>]
649
+
650
+ const NO_EVENTS: StepResult["1"] = []
651
+
652
+ const onMessageStart = (state: ParserState, event: AnthropicEvent): StepResult => {
653
+ const usage = mapUsage(event.message?.usage)
654
+ return [usage ? { ...state, usage: mergeUsage(state.usage, usage) } : state, NO_EVENTS]
655
+ }
656
+
657
+ const onContentBlockStart = (state: ParserState, event: AnthropicEvent): StepResult => {
658
+ const block = event.content_block
659
+ if (!block) return [state, NO_EVENTS]
660
+
661
+ if ((block.type === "tool_use" || block.type === "server_tool_use") && event.index !== undefined) {
662
+ const events: LLMEvent[] = []
663
+ const lifecycle = Lifecycle.stepStart(state.lifecycle, events)
664
+ return [
665
+ {
666
+ ...state,
667
+ lifecycle,
668
+ tools: ToolStream.start(state.tools, event.index, {
669
+ id: block.id ?? String(event.index),
670
+ name: block.name ?? "",
671
+ providerExecuted: block.type === "server_tool_use",
672
+ }),
673
+ },
674
+ [...events, LLMEvent.toolInputStart({ id: block.id ?? String(event.index), name: block.name ?? "" })],
675
+ ]
676
+ }
677
+
678
+ if (block.type === "text" && block.text) {
679
+ const events: LLMEvent[] = []
680
+ return [
681
+ { ...state, lifecycle: Lifecycle.textDelta(state.lifecycle, events, `text-${event.index ?? 0}`, block.text) },
682
+ events,
683
+ ]
684
+ }
685
+
686
+ if (block.type === "thinking" && block.thinking) {
687
+ const events: LLMEvent[] = []
688
+ return [
689
+ {
690
+ ...state,
691
+ lifecycle: Lifecycle.reasoningDelta(state.lifecycle, events, `reasoning-${event.index ?? 0}`, block.thinking),
692
+ },
693
+ events,
694
+ ]
695
+ }
696
+
697
+ const result = serverToolResultEvent(block)
698
+ if (!result) return [state, NO_EVENTS]
699
+ const events: LLMEvent[] = []
700
+ return [{ ...state, lifecycle: Lifecycle.stepStart(state.lifecycle, events) }, [...events, result]]
701
+ }
702
+
703
+ const onContentBlockDelta = Effect.fn("AnthropicMessages.onContentBlockDelta")(function* (
704
+ state: ParserState,
705
+ event: AnthropicEvent,
706
+ ) {
707
+ const delta = event.delta
708
+
709
+ if (delta?.type === "text_delta" && delta.text) {
710
+ const events: LLMEvent[] = []
711
+ return [
712
+ { ...state, lifecycle: Lifecycle.textDelta(state.lifecycle, events, `text-${event.index ?? 0}`, delta.text) },
713
+ events,
714
+ ] satisfies StepResult
715
+ }
716
+
717
+ if (delta?.type === "thinking_delta" && delta.thinking) {
718
+ const events: LLMEvent[] = []
719
+ return [
720
+ {
721
+ ...state,
722
+ lifecycle: Lifecycle.reasoningDelta(state.lifecycle, events, `reasoning-${event.index ?? 0}`, delta.thinking),
723
+ },
724
+ events,
725
+ ] satisfies StepResult
726
+ }
727
+
728
+ if (delta?.type === "signature_delta" && delta.signature) {
729
+ const events: LLMEvent[] = []
730
+ return [
731
+ {
732
+ ...state,
733
+ lifecycle: Lifecycle.reasoningEnd(
734
+ state.lifecycle,
735
+ events,
736
+ `reasoning-${event.index ?? 0}`,
737
+ anthropicMetadata({ signature: delta.signature }),
738
+ ),
739
+ },
740
+ events,
741
+ ] satisfies StepResult
742
+ }
743
+
744
+ if (delta?.type === "input_json_delta" && event.index !== undefined) {
745
+ if (!delta.partial_json) return [state, NO_EVENTS] satisfies StepResult
746
+ const result = ToolStream.appendExisting(
747
+ ADAPTER,
748
+ state.tools,
749
+ event.index,
750
+ delta.partial_json,
751
+ "Anthropic Messages tool argument delta is missing its tool call",
752
+ )
753
+ if (ToolStream.isError(result)) return yield* result
754
+ const events: LLMEvent[] = []
755
+ const lifecycle = result.events.length ? Lifecycle.stepStart(state.lifecycle, events) : state.lifecycle
756
+ events.push(...result.events)
757
+ return [{ ...state, lifecycle, tools: result.tools }, events] satisfies StepResult
758
+ }
759
+
760
+ return [state, NO_EVENTS] satisfies StepResult
761
+ })
762
+
763
+ const onContentBlockStop = Effect.fn("AnthropicMessages.onContentBlockStop")(function* (
764
+ state: ParserState,
765
+ event: AnthropicEvent,
766
+ ) {
767
+ if (event.index === undefined) return [state, NO_EVENTS] satisfies StepResult
768
+ const result = yield* ToolStream.finish(ADAPTER, state.tools, event.index)
769
+ const events: LLMEvent[] = []
770
+ const resultEvents = result.events ?? []
771
+ const lifecycle = resultEvents.length
772
+ ? Lifecycle.stepStart(state.lifecycle, events)
773
+ : Lifecycle.reasoningEnd(
774
+ Lifecycle.textEnd(state.lifecycle, events, `text-${event.index}`),
775
+ events,
776
+ `reasoning-${event.index}`,
777
+ )
778
+ events.push(...resultEvents)
779
+ return [{ ...state, lifecycle, tools: result.tools }, events] satisfies StepResult
780
+ })
781
+
782
+ const onMessageDelta = (state: ParserState, event: AnthropicEvent): StepResult => {
783
+ const usage = mergeUsage(state.usage, mapUsage(event.usage))
784
+ const events: LLMEvent[] = []
785
+ const lifecycle = Lifecycle.finish(state.lifecycle, events, {
786
+ reason: mapFinishReason(event.delta?.stop_reason),
787
+ usage,
788
+ providerMetadata: event.delta?.stop_sequence
789
+ ? anthropicMetadata({ stopSequence: event.delta.stop_sequence })
790
+ : undefined,
791
+ })
792
+ return [{ ...state, lifecycle, usage }, events]
793
+ }
794
+
795
+ // Prefix `error.type` so overloads, rate limits, and quota errors are visible
796
+ // even when the provider message is generic or empty.
797
+ const providerErrorMessage = (event: AnthropicEvent): string => {
798
+ const type = event.error?.type
799
+ const message = event.error?.message
800
+ if (type && message) return `${type}: ${message}`
801
+ return message || type || "Anthropic Messages stream error"
802
+ }
803
+
804
+ const onError = (state: ParserState, event: AnthropicEvent): StepResult => [
805
+ state,
806
+ [
807
+ LLMEvent.providerError({
808
+ message: providerErrorMessage(event),
809
+ classification: isContextOverflow(event.error?.message ?? "") ? "context-overflow" : undefined,
810
+ }),
811
+ ],
812
+ ]
813
+
814
+ const step = (state: ParserState, event: AnthropicEvent) => {
815
+ if (event.type === "message_start") return Effect.succeed(onMessageStart(state, event))
816
+ if (event.type === "content_block_start") return Effect.succeed(onContentBlockStart(state, event))
817
+ if (event.type === "content_block_delta") return onContentBlockDelta(state, event)
818
+ if (event.type === "content_block_stop") return onContentBlockStop(state, event)
819
+ if (event.type === "message_delta") return Effect.succeed(onMessageDelta(state, event))
820
+ if (event.type === "error") return Effect.succeed(onError(state, event))
821
+ return Effect.succeed<StepResult>([state, NO_EVENTS])
822
+ }
823
+
824
+ // =============================================================================
825
+ // Protocol And Anthropic Route
826
+ // =============================================================================
827
+ /**
828
+ * The Anthropic Messages protocol — request body construction, body schema,
829
+ * and the streaming-event state machine. Used by native Anthropic Cloud and
830
+ * (once registered) Vertex Anthropic / Bedrock-hosted Anthropic passthrough.
831
+ */
832
+ export const protocol = Protocol.make({
833
+ id: ADAPTER,
834
+ body: {
835
+ schema: AnthropicMessagesBody,
836
+ from: fromRequest,
837
+ },
838
+ stream: {
839
+ event: Protocol.jsonEvent(AnthropicEvent),
840
+ initial: () => ({ tools: ToolStream.empty<number>(), lifecycle: Lifecycle.initial() }),
841
+ step,
842
+ },
843
+ })
844
+
845
+ export const route = Route.make({
846
+ id: ADAPTER,
847
+ provider: "anthropic",
848
+ protocol,
849
+ endpoint: Endpoint.path(PATH, { baseURL: DEFAULT_BASE_URL }),
850
+ auth: Auth.none,
851
+ framing: Framing.sse,
852
+ headers: () => ({ "anthropic-version": "2023-06-01" }),
853
+ })
854
+
855
+ export * as AnthropicMessages from "./anthropic-messages"