@codilore/llm 1.15.13

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