@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,674 @@
1
+ import { Effect, Schema } from "effect"
2
+ import { Route } from "../route/client"
3
+ import { Endpoint } from "../route/endpoint"
4
+ import { Protocol } from "../route/protocol"
5
+ import {
6
+ LLMEvent,
7
+ Usage,
8
+ type CacheHint,
9
+ type FinishReason,
10
+ type JsonSchema,
11
+ type LLMRequest,
12
+ type ModelToolSchemaCompatibility,
13
+ type ProviderMetadata,
14
+ type ReasoningPart,
15
+ type ToolCallPart,
16
+ type ToolDefinition,
17
+ type ToolResultPart,
18
+ } from "../schema"
19
+ import { BedrockEventStream } from "./bedrock-event-stream"
20
+ import { isContextOverflow } from "../provider-error"
21
+ import { JsonObject, optionalArray, ProviderShared } from "./shared"
22
+ import { BedrockAuth } from "./utils/bedrock-auth"
23
+ import { BedrockCache } from "./utils/bedrock-cache"
24
+ import { BedrockMedia } from "./utils/bedrock-media"
25
+ import { Lifecycle } from "./utils/lifecycle"
26
+ import { ToolSchemaProjection } from "./utils/tool-schema"
27
+ import { ToolStream } from "./utils/tool-stream"
28
+
29
+ const ADAPTER = "bedrock-converse"
30
+
31
+ export type { Credentials as BedrockCredentials } from "./utils/bedrock-auth"
32
+
33
+ // =============================================================================
34
+ // Request Body Schema
35
+ // =============================================================================
36
+ const BedrockTextBlock = Schema.Struct({
37
+ text: Schema.String,
38
+ })
39
+ type BedrockTextBlock = Schema.Schema.Type<typeof BedrockTextBlock>
40
+
41
+ const BedrockToolUseBlock = Schema.Struct({
42
+ toolUse: Schema.Struct({
43
+ toolUseId: Schema.String,
44
+ name: Schema.String,
45
+ input: Schema.Unknown,
46
+ }),
47
+ })
48
+ type BedrockToolUseBlock = Schema.Schema.Type<typeof BedrockToolUseBlock>
49
+
50
+ const BedrockToolResultContentItem = Schema.Union([
51
+ Schema.Struct({ text: Schema.String }),
52
+ Schema.Struct({ json: Schema.Unknown }),
53
+ BedrockMedia.ImageBlock,
54
+ ])
55
+
56
+ const BedrockToolResultBlock = Schema.Struct({
57
+ toolResult: Schema.Struct({
58
+ toolUseId: Schema.String,
59
+ content: Schema.Array(BedrockToolResultContentItem),
60
+ status: Schema.optional(Schema.Literals(["success", "error"])),
61
+ }),
62
+ })
63
+ type BedrockToolResultBlock = Schema.Schema.Type<typeof BedrockToolResultBlock>
64
+
65
+ const BedrockReasoningBlock = Schema.Struct({
66
+ reasoningContent: Schema.Struct({
67
+ reasoningText: Schema.optional(
68
+ Schema.Struct({
69
+ text: Schema.String,
70
+ signature: Schema.optional(Schema.String),
71
+ }),
72
+ ),
73
+ }),
74
+ })
75
+
76
+ const BedrockUserBlock = Schema.Union([
77
+ BedrockTextBlock,
78
+ BedrockMedia.ImageBlock,
79
+ BedrockMedia.DocumentBlock,
80
+ BedrockToolResultBlock,
81
+ BedrockCache.CachePointBlock,
82
+ ])
83
+ type BedrockUserBlock = Schema.Schema.Type<typeof BedrockUserBlock>
84
+
85
+ const BedrockAssistantBlock = Schema.Union([
86
+ BedrockTextBlock,
87
+ BedrockReasoningBlock,
88
+ BedrockToolUseBlock,
89
+ BedrockCache.CachePointBlock,
90
+ ])
91
+ type BedrockAssistantBlock = Schema.Schema.Type<typeof BedrockAssistantBlock>
92
+
93
+ const BedrockMessage = Schema.Union([
94
+ Schema.Struct({ role: Schema.Literal("user"), content: Schema.Array(BedrockUserBlock) }),
95
+ Schema.Struct({ role: Schema.Literal("assistant"), content: Schema.Array(BedrockAssistantBlock) }),
96
+ ]).pipe(Schema.toTaggedUnion("role"))
97
+ type BedrockMessage = Schema.Schema.Type<typeof BedrockMessage>
98
+
99
+ const BedrockSystemBlock = Schema.Union([BedrockTextBlock, BedrockCache.CachePointBlock])
100
+ type BedrockSystemBlock = Schema.Schema.Type<typeof BedrockSystemBlock>
101
+
102
+ const BedrockToolSpec = Schema.Struct({
103
+ toolSpec: Schema.Struct({
104
+ name: Schema.String,
105
+ description: Schema.String,
106
+ inputSchema: Schema.Struct({
107
+ json: JsonObject,
108
+ }),
109
+ }),
110
+ })
111
+ type BedrockToolSpec = Schema.Schema.Type<typeof BedrockToolSpec>
112
+
113
+ const BedrockTool = Schema.Union([BedrockToolSpec, BedrockCache.CachePointBlock])
114
+ type BedrockTool = Schema.Schema.Type<typeof BedrockTool>
115
+
116
+ const BedrockToolChoice = Schema.Union([
117
+ Schema.Struct({ auto: Schema.Struct({}) }),
118
+ Schema.Struct({ any: Schema.Struct({}) }),
119
+ Schema.Struct({ tool: Schema.Struct({ name: Schema.String }) }),
120
+ ])
121
+
122
+ const BedrockBodyFields = {
123
+ modelId: Schema.String,
124
+ messages: Schema.Array(BedrockMessage),
125
+ system: optionalArray(BedrockSystemBlock),
126
+ inferenceConfig: Schema.optional(
127
+ Schema.Struct({
128
+ maxTokens: Schema.optional(Schema.Number),
129
+ temperature: Schema.optional(Schema.Number),
130
+ topP: Schema.optional(Schema.Number),
131
+ stopSequences: optionalArray(Schema.String),
132
+ }),
133
+ ),
134
+ toolConfig: Schema.optional(
135
+ Schema.Struct({
136
+ tools: Schema.Array(BedrockTool),
137
+ toolChoice: Schema.optional(BedrockToolChoice),
138
+ }),
139
+ ),
140
+ additionalModelRequestFields: Schema.optional(JsonObject),
141
+ }
142
+ const BedrockConverseBody = Schema.Struct(BedrockBodyFields)
143
+ export type BedrockConverseBody = Schema.Schema.Type<typeof BedrockConverseBody>
144
+
145
+ const BedrockUsageSchema = Schema.Struct({
146
+ inputTokens: Schema.optional(Schema.Number),
147
+ outputTokens: Schema.optional(Schema.Number),
148
+ totalTokens: Schema.optional(Schema.Number),
149
+ cacheReadInputTokens: Schema.optional(Schema.Number),
150
+ cacheWriteInputTokens: Schema.optional(Schema.Number),
151
+ })
152
+ type BedrockUsageSchema = Schema.Schema.Type<typeof BedrockUsageSchema>
153
+
154
+ // Streaming event shape — the AWS event stream wraps each JSON payload by its
155
+ // `:event-type` header (e.g. `messageStart`, `contentBlockDelta`). We
156
+ // reconstruct that wrapping in `decodeFrames` below so the event schema can
157
+ // stay a plain discriminated record.
158
+ const BedrockEvent = Schema.Struct({
159
+ messageStart: Schema.optional(Schema.Struct({ role: Schema.String })),
160
+ contentBlockStart: Schema.optional(
161
+ Schema.Struct({
162
+ contentBlockIndex: Schema.Number,
163
+ start: Schema.optional(
164
+ Schema.Struct({
165
+ toolUse: Schema.optional(Schema.Struct({ toolUseId: Schema.String, name: Schema.String })),
166
+ }),
167
+ ),
168
+ }),
169
+ ),
170
+ contentBlockDelta: Schema.optional(
171
+ Schema.Struct({
172
+ contentBlockIndex: Schema.Number,
173
+ delta: Schema.optional(
174
+ Schema.Struct({
175
+ text: Schema.optional(Schema.String),
176
+ toolUse: Schema.optional(Schema.Struct({ input: Schema.String })),
177
+ reasoningContent: Schema.optional(
178
+ Schema.Struct({
179
+ text: Schema.optional(Schema.String),
180
+ signature: Schema.optional(Schema.String),
181
+ }),
182
+ ),
183
+ }),
184
+ ),
185
+ }),
186
+ ),
187
+ contentBlockStop: Schema.optional(Schema.Struct({ contentBlockIndex: Schema.Number })),
188
+ messageStop: Schema.optional(
189
+ Schema.Struct({
190
+ stopReason: Schema.String,
191
+ additionalModelResponseFields: Schema.optional(Schema.Unknown),
192
+ }),
193
+ ),
194
+ metadata: Schema.optional(
195
+ Schema.Struct({
196
+ usage: Schema.optional(BedrockUsageSchema),
197
+ metrics: Schema.optional(Schema.Unknown),
198
+ }),
199
+ ),
200
+ internalServerException: Schema.optional(Schema.Struct({ message: Schema.String })),
201
+ modelStreamErrorException: Schema.optional(Schema.Struct({ message: Schema.String })),
202
+ validationException: Schema.optional(Schema.Struct({ message: Schema.String })),
203
+ throttlingException: Schema.optional(Schema.Struct({ message: Schema.String })),
204
+ serviceUnavailableException: Schema.optional(Schema.Struct({ message: Schema.String })),
205
+ })
206
+ type BedrockEvent = Schema.Schema.Type<typeof BedrockEvent>
207
+
208
+ // =============================================================================
209
+ // Request Lowering
210
+ // =============================================================================
211
+ const lowerToolSpec = (tool: ToolDefinition, inputSchema: JsonSchema): BedrockToolSpec => ({
212
+ toolSpec: {
213
+ name: tool.name,
214
+ description: tool.description,
215
+ inputSchema: { json: inputSchema },
216
+ },
217
+ })
218
+
219
+ const lowerTools = (
220
+ compatibility: ModelToolSchemaCompatibility | undefined,
221
+ breakpoints: BedrockCache.Breakpoints,
222
+ tools: ReadonlyArray<ToolDefinition>,
223
+ ): BedrockTool[] => {
224
+ const result: BedrockTool[] = []
225
+ for (const tool of tools) {
226
+ result.push(lowerToolSpec(tool, ToolSchemaProjection.modelCompatibility(tool.inputSchema, compatibility)))
227
+ const cachePoint = BedrockCache.block(breakpoints, tool.cache)
228
+ if (cachePoint) result.push(cachePoint)
229
+ }
230
+ return result
231
+ }
232
+
233
+ const textWithCache = (
234
+ breakpoints: BedrockCache.Breakpoints,
235
+ text: string,
236
+ cache: CacheHint | undefined,
237
+ ): Array<BedrockTextBlock | BedrockCache.CachePointBlock> => {
238
+ const cachePoint = BedrockCache.block(breakpoints, cache)
239
+ return cachePoint ? [{ text }, cachePoint] : [{ text }]
240
+ }
241
+
242
+ const lowerToolChoice = (toolChoice: NonNullable<LLMRequest["toolChoice"]>) =>
243
+ ProviderShared.matchToolChoice("Bedrock Converse", toolChoice, {
244
+ auto: () => ({ auto: {} }) as const,
245
+ none: () => undefined,
246
+ required: () => ({ any: {} }) as const,
247
+ tool: (name) => ({ tool: { name } }) as const,
248
+ })
249
+
250
+ const bedrockMetadata = (metadata: Record<string, unknown>): ProviderMetadata => ({ bedrock: metadata })
251
+
252
+ const reasoningSignature = (part: ReasoningPart) => {
253
+ const bedrock = part.providerMetadata?.bedrock
254
+ return (
255
+ part.encrypted ??
256
+ (ProviderShared.isRecord(bedrock) && typeof bedrock.signature === "string" ? bedrock.signature : undefined)
257
+ )
258
+ }
259
+
260
+ const lowerToolCall = (part: ToolCallPart): BedrockToolUseBlock => ({
261
+ toolUse: {
262
+ toolUseId: part.id,
263
+ name: part.name,
264
+ input: part.input,
265
+ },
266
+ })
267
+
268
+ const lowerToolResultContent = Effect.fn("BedrockConverse.lowerToolResultContent")(function* (part: ToolResultPart) {
269
+ if (part.result.type === "text" || part.result.type === "error")
270
+ return [{ text: ProviderShared.toolResultText(part) }]
271
+ if (part.result.type === "json") return [{ json: part.result.value }]
272
+
273
+ const content: Array<Schema.Schema.Type<typeof BedrockToolResultContentItem>> = []
274
+ for (const item of part.result.value) {
275
+ if (item.type === "text") {
276
+ content.push({ text: item.text })
277
+ continue
278
+ }
279
+ const media = yield* BedrockMedia.lower({
280
+ type: "media",
281
+ mediaType: item.mime,
282
+ data: item.uri,
283
+ filename: item.name,
284
+ })
285
+ if (!("image" in media))
286
+ return yield* ProviderShared.invalidRequest("Bedrock Converse only supports image media in tool results")
287
+ content.push(media)
288
+ }
289
+ return content
290
+ })
291
+
292
+ const lowerToolResult = Effect.fn("BedrockConverse.lowerToolResult")(function* (part: ToolResultPart) {
293
+ return {
294
+ toolResult: {
295
+ toolUseId: part.id,
296
+ content: yield* lowerToolResultContent(part),
297
+ status: part.result.type === "error" ? "error" : "success",
298
+ },
299
+ } satisfies BedrockToolResultBlock
300
+ })
301
+
302
+ const lowerMessages = Effect.fn("BedrockConverse.lowerMessages")(function* (
303
+ request: LLMRequest,
304
+ breakpoints: BedrockCache.Breakpoints,
305
+ ) {
306
+ const messages: BedrockMessage[] = []
307
+
308
+ for (const message of request.messages) {
309
+ if (message.role === "system") {
310
+ const part = yield* ProviderShared.wrappedSystemUpdate("Bedrock Converse", message)
311
+ const content = textWithCache(breakpoints, part.text, part.cache)
312
+ const previous = messages.at(-1)
313
+ if (previous?.role === "user")
314
+ messages[messages.length - 1] = { role: "user", content: [...previous.content, ...content] }
315
+ else messages.push({ role: "user", content })
316
+ continue
317
+ }
318
+
319
+ if (message.role === "user") {
320
+ const content: BedrockUserBlock[] = []
321
+ for (const part of message.content) {
322
+ if (!ProviderShared.supportsContent(part, ["text", "media"]))
323
+ return yield* ProviderShared.unsupportedContent("Bedrock Converse", "user", ["text", "media"])
324
+ if (part.type === "text") {
325
+ content.push(...textWithCache(breakpoints, part.text, part.cache))
326
+ continue
327
+ }
328
+ if (part.type === "media") {
329
+ content.push(yield* BedrockMedia.lower(part))
330
+ continue
331
+ }
332
+ }
333
+ messages.push({ role: "user", content })
334
+ continue
335
+ }
336
+
337
+ if (message.role === "assistant") {
338
+ const content: BedrockAssistantBlock[] = []
339
+ for (const part of message.content) {
340
+ if (!ProviderShared.supportsContent(part, ["text", "reasoning", "tool-call"]))
341
+ return yield* ProviderShared.unsupportedContent("Bedrock Converse", "assistant", [
342
+ "text",
343
+ "reasoning",
344
+ "tool-call",
345
+ ])
346
+ if (part.type === "text") {
347
+ content.push(...textWithCache(breakpoints, part.text, part.cache))
348
+ continue
349
+ }
350
+ if (part.type === "reasoning") {
351
+ content.push({
352
+ reasoningContent: {
353
+ reasoningText: { text: part.text, signature: reasoningSignature(part) },
354
+ },
355
+ })
356
+ continue
357
+ }
358
+ if (part.type === "tool-call") {
359
+ content.push(lowerToolCall(part))
360
+ continue
361
+ }
362
+ }
363
+ messages.push({ role: "assistant", content })
364
+ continue
365
+ }
366
+
367
+ const content: BedrockUserBlock[] = []
368
+ for (const part of message.content) {
369
+ if (!ProviderShared.supportsContent(part, ["tool-result"]))
370
+ return yield* ProviderShared.unsupportedContent("Bedrock Converse", "tool", ["tool-result"])
371
+ content.push(yield* lowerToolResult(part))
372
+ const cachePoint = BedrockCache.block(breakpoints, part.cache)
373
+ if (cachePoint) content.push(cachePoint)
374
+ }
375
+ messages.push({ role: "user", content })
376
+ }
377
+
378
+ return messages
379
+ })
380
+
381
+ // System prompts share the cache-point convention: emit the text block, then
382
+ // optionally a positional `cachePoint` marker.
383
+ const lowerSystem = (
384
+ breakpoints: BedrockCache.Breakpoints,
385
+ system: ReadonlyArray<LLMRequest["system"][number]>,
386
+ ): BedrockSystemBlock[] => system.flatMap((part) => textWithCache(breakpoints, part.text, part.cache))
387
+
388
+ const fromRequest = Effect.fn("BedrockConverse.fromRequest")(function* (request: LLMRequest) {
389
+ const toolChoice = request.toolChoice ? yield* lowerToolChoice(request.toolChoice) : undefined
390
+ const generation = request.generation
391
+ // Bedrock-Claude shares Anthropic's 4-breakpoint cap. Spend the budget in
392
+ // tools → system → messages order to favour the highest-impact prefixes.
393
+ const breakpoints = BedrockCache.breakpoints()
394
+ const toolConfig =
395
+ request.tools.length > 0 && request.toolChoice?.type !== "none"
396
+ ? { tools: lowerTools(request.model.compatibility?.toolSchema, breakpoints, request.tools), toolChoice }
397
+ : undefined
398
+ const system = request.system.length === 0 ? undefined : lowerSystem(breakpoints, request.system)
399
+ const messages = yield* lowerMessages(request, breakpoints)
400
+ if (breakpoints.dropped > 0) {
401
+ yield* Effect.logWarning(
402
+ `Bedrock Converse: dropped ${breakpoints.dropped} cache breakpoint(s); the API allows at most ${BedrockCache.BEDROCK_BREAKPOINT_CAP} per request.`,
403
+ )
404
+ }
405
+ return {
406
+ modelId: request.model.id,
407
+ messages,
408
+ system,
409
+ inferenceConfig:
410
+ generation?.maxTokens === undefined &&
411
+ generation?.temperature === undefined &&
412
+ generation?.topP === undefined &&
413
+ (generation?.stop === undefined || generation.stop.length === 0)
414
+ ? undefined
415
+ : {
416
+ maxTokens: generation?.maxTokens,
417
+ temperature: generation?.temperature,
418
+ topP: generation?.topP,
419
+ stopSequences: generation?.stop,
420
+ },
421
+ toolConfig,
422
+ // Converse's base inferenceConfig has no topK; Anthropic/Nova accept it
423
+ // as a model-specific field, so it goes through additionalModelRequestFields.
424
+ additionalModelRequestFields: generation?.topK === undefined ? undefined : { top_k: generation.topK },
425
+ }
426
+ })
427
+
428
+ // =============================================================================
429
+ // Stream Parsing
430
+ // =============================================================================
431
+ const mapFinishReason = (reason: string): FinishReason => {
432
+ if (reason === "end_turn" || reason === "stop_sequence") return "stop"
433
+ if (reason === "max_tokens") return "length"
434
+ if (reason === "tool_use") return "tool-calls"
435
+ if (reason === "content_filtered" || reason === "guardrail_intervened") return "content-filter"
436
+ return "unknown"
437
+ }
438
+
439
+ // AWS Bedrock Converse reports `inputTokens` (inclusive total) with
440
+ // `cacheReadInputTokens` and `cacheWriteInputTokens` as subsets. Pass
441
+ // the total through and derive the non-cached breakdown. Bedrock does
442
+ // not break reasoning out of `outputTokens` for any current model.
443
+ const mapUsage = (usage: BedrockUsageSchema | undefined): Usage | undefined => {
444
+ if (!usage) return undefined
445
+ const cacheTotal = (usage.cacheReadInputTokens ?? 0) + (usage.cacheWriteInputTokens ?? 0)
446
+ const nonCached = ProviderShared.subtractTokens(usage.inputTokens, cacheTotal)
447
+ return new Usage({
448
+ inputTokens: usage.inputTokens,
449
+ outputTokens: usage.outputTokens,
450
+ nonCachedInputTokens: nonCached,
451
+ cacheReadInputTokens: usage.cacheReadInputTokens,
452
+ cacheWriteInputTokens: usage.cacheWriteInputTokens,
453
+ totalTokens: ProviderShared.totalTokens(usage.inputTokens, usage.outputTokens, usage.totalTokens),
454
+ providerMetadata: { bedrock: usage },
455
+ })
456
+ }
457
+
458
+ interface ParserState {
459
+ readonly tools: ToolStream.State<number>
460
+ // Bedrock splits the finish into `messageStop` (carries `stopReason`) and
461
+ // `metadata` (carries usage). Hold the terminal event in state so `onHalt`
462
+ // can emit exactly one finish after both chunks have had a chance to arrive.
463
+ readonly pendingFinish: { readonly reason: FinishReason; readonly usage?: Usage } | undefined
464
+ readonly hasToolCalls: boolean
465
+ readonly lifecycle: Lifecycle.State
466
+ readonly reasoningSignatures: Readonly<Record<number, string>>
467
+ }
468
+
469
+ const step = (state: ParserState, event: BedrockEvent) =>
470
+ Effect.gen(function* () {
471
+ if (event.contentBlockStart?.start?.toolUse) {
472
+ const index = event.contentBlockStart.contentBlockIndex
473
+ const events: LLMEvent[] = []
474
+ const lifecycle = Lifecycle.stepStart(state.lifecycle, events)
475
+ return [
476
+ {
477
+ ...state,
478
+ lifecycle,
479
+ tools: ToolStream.start(state.tools, index, {
480
+ id: event.contentBlockStart.start.toolUse.toolUseId,
481
+ name: event.contentBlockStart.start.toolUse.name,
482
+ }),
483
+ },
484
+ [
485
+ ...events,
486
+ LLMEvent.toolInputStart({
487
+ id: event.contentBlockStart.start.toolUse.toolUseId,
488
+ name: event.contentBlockStart.start.toolUse.name,
489
+ }),
490
+ ],
491
+ ] as const
492
+ }
493
+
494
+ if (event.contentBlockDelta?.delta?.text) {
495
+ const events: LLMEvent[] = []
496
+ return [
497
+ {
498
+ ...state,
499
+ lifecycle: Lifecycle.textDelta(
500
+ state.lifecycle,
501
+ events,
502
+ `text-${event.contentBlockDelta.contentBlockIndex}`,
503
+ event.contentBlockDelta.delta.text,
504
+ ),
505
+ },
506
+ events,
507
+ ] as const
508
+ }
509
+
510
+ if (event.contentBlockDelta?.delta?.reasoningContent) {
511
+ const index = event.contentBlockDelta.contentBlockIndex
512
+ const reasoning = event.contentBlockDelta.delta.reasoningContent
513
+ const events: LLMEvent[] = []
514
+ return [
515
+ {
516
+ ...state,
517
+ lifecycle: reasoning.text
518
+ ? Lifecycle.reasoningDelta(state.lifecycle, events, `reasoning-${index}`, reasoning.text)
519
+ : state.lifecycle,
520
+ reasoningSignatures: reasoning.signature
521
+ ? { ...state.reasoningSignatures, [index]: reasoning.signature }
522
+ : state.reasoningSignatures,
523
+ },
524
+ events,
525
+ ] as const
526
+ }
527
+
528
+ if (event.contentBlockDelta?.delta?.toolUse) {
529
+ const index = event.contentBlockDelta.contentBlockIndex
530
+ const result = ToolStream.appendExisting(
531
+ ADAPTER,
532
+ state.tools,
533
+ index,
534
+ event.contentBlockDelta.delta.toolUse.input,
535
+ "Bedrock Converse tool delta is missing its tool call",
536
+ )
537
+ if (ToolStream.isError(result)) return yield* result
538
+ const events: LLMEvent[] = []
539
+ const lifecycle = result.events.length ? Lifecycle.stepStart(state.lifecycle, events) : state.lifecycle
540
+ events.push(...result.events)
541
+ return [{ ...state, lifecycle, tools: result.tools }, events] as const
542
+ }
543
+
544
+ if (event.contentBlockStop) {
545
+ const index = event.contentBlockStop.contentBlockIndex
546
+ const result = yield* ToolStream.finish(ADAPTER, state.tools, index)
547
+ const events: LLMEvent[] = []
548
+ const resultEvents = result.events ?? []
549
+ const lifecycle = resultEvents.length
550
+ ? Lifecycle.stepStart(state.lifecycle, events)
551
+ : Lifecycle.reasoningEnd(
552
+ Lifecycle.textEnd(state.lifecycle, events, `text-${index}`),
553
+ events,
554
+ `reasoning-${index}`,
555
+ state.reasoningSignatures[index]
556
+ ? bedrockMetadata({ signature: state.reasoningSignatures[index] })
557
+ : undefined,
558
+ )
559
+ events.push(...resultEvents)
560
+ return [
561
+ {
562
+ ...state,
563
+ hasToolCalls: resultEvents.some(LLMEvent.is.toolCall) ? true : state.hasToolCalls,
564
+ lifecycle,
565
+ tools: result.tools,
566
+ reasoningSignatures: Object.fromEntries(
567
+ Object.entries(state.reasoningSignatures).filter(([key]) => key !== String(index)),
568
+ ),
569
+ },
570
+ events,
571
+ ] as const
572
+ }
573
+
574
+ if (event.messageStop) {
575
+ return [
576
+ {
577
+ ...state,
578
+ pendingFinish: { reason: mapFinishReason(event.messageStop.stopReason), usage: state.pendingFinish?.usage },
579
+ },
580
+ [],
581
+ ] as const
582
+ }
583
+
584
+ if (event.metadata) {
585
+ const usage = mapUsage(event.metadata.usage)
586
+ return [{ ...state, pendingFinish: { reason: state.pendingFinish?.reason ?? "stop", usage } }, []] as const
587
+ }
588
+
589
+ if (event.internalServerException || event.modelStreamErrorException || event.serviceUnavailableException) {
590
+ const message =
591
+ event.internalServerException?.message ??
592
+ event.modelStreamErrorException?.message ??
593
+ event.serviceUnavailableException?.message ??
594
+ "Bedrock Converse stream error"
595
+ return [state, [LLMEvent.providerError({ message, retryable: true })]] as const
596
+ }
597
+
598
+ if (event.validationException || event.throttlingException) {
599
+ const message =
600
+ event.validationException?.message ?? event.throttlingException?.message ?? "Bedrock Converse error"
601
+ return [
602
+ state,
603
+ [
604
+ LLMEvent.providerError({
605
+ message,
606
+ classification: event.validationException && isContextOverflow(message) ? "context-overflow" : undefined,
607
+ retryable: event.throttlingException !== undefined,
608
+ }),
609
+ ],
610
+ ] as const
611
+ }
612
+
613
+ return [state, []] as const
614
+ })
615
+
616
+ const framing = BedrockEventStream.framing(ADAPTER)
617
+
618
+ const onHalt = (state: ParserState): ReadonlyArray<LLMEvent> =>
619
+ state.pendingFinish
620
+ ? (() => {
621
+ const events: LLMEvent[] = []
622
+ Lifecycle.finish(state.lifecycle, events, {
623
+ reason:
624
+ state.pendingFinish.reason === "stop" && state.hasToolCalls ? "tool-calls" : state.pendingFinish.reason,
625
+ usage: state.pendingFinish.usage,
626
+ })
627
+ return events
628
+ })()
629
+ : []
630
+
631
+ // =============================================================================
632
+ // Protocol And Bedrock Route
633
+ // =============================================================================
634
+ /**
635
+ * The Bedrock Converse protocol — request body construction, body schema, and
636
+ * the streaming-event state machine.
637
+ */
638
+ export const protocol = Protocol.make({
639
+ id: ADAPTER,
640
+ body: {
641
+ schema: BedrockConverseBody,
642
+ from: fromRequest,
643
+ },
644
+ stream: {
645
+ event: BedrockEvent,
646
+ initial: () => ({
647
+ tools: ToolStream.empty<number>(),
648
+ pendingFinish: undefined,
649
+ hasToolCalls: false,
650
+ lifecycle: Lifecycle.initial(),
651
+ reasoningSignatures: {},
652
+ }),
653
+ step,
654
+ onHalt,
655
+ },
656
+ })
657
+
658
+ export const route = Route.make({
659
+ id: ADAPTER,
660
+ provider: "bedrock",
661
+ protocol,
662
+ // Bedrock's URL embeds the region in the route endpoint host and the
663
+ // validated modelId in the path. We read the validated body so the URL
664
+ // matches the body that gets signed.
665
+ endpoint: Endpoint.path<BedrockConverseBody>(
666
+ ({ body }) => `/model/${encodeURIComponent(body.modelId)}/converse-stream`,
667
+ ),
668
+ auth: BedrockAuth.auth,
669
+ framing,
670
+ })
671
+
672
+ export const sigV4Auth = BedrockAuth.sigV4
673
+
674
+ export * as BedrockConverse from "./bedrock-converse"