@wmaurer/effect-ai-amazon-bedrock 0.1.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/README.md +97 -16
  3. package/dist/AmazonBedrockClient.d.ts +135 -23
  4. package/dist/AmazonBedrockClient.d.ts.map +1 -1
  5. package/dist/AmazonBedrockClient.js +36 -92
  6. package/dist/AmazonBedrockClient.js.map +1 -1
  7. package/dist/AmazonBedrockConfig.d.ts +6 -6
  8. package/dist/AmazonBedrockConfig.js +4 -4
  9. package/dist/AmazonBedrockEventStream.d.ts +2 -2
  10. package/dist/AmazonBedrockEventStream.js +3 -3
  11. package/dist/AmazonBedrockLanguageModel.d.ts +22 -22
  12. package/dist/AmazonBedrockLanguageModel.d.ts.map +1 -1
  13. package/dist/AmazonBedrockLanguageModel.js +77 -28
  14. package/dist/AmazonBedrockLanguageModel.js.map +1 -1
  15. package/dist/AmazonBedrockSchema.d.ts +76 -55
  16. package/dist/AmazonBedrockSchema.d.ts.map +1 -1
  17. package/dist/AmazonBedrockSchema.js +63 -50
  18. package/dist/AmazonBedrockSchema.js.map +1 -1
  19. package/dist/index.d.ts +6 -6
  20. package/dist/index.js +6 -6
  21. package/dist/internal/credentials.d.ts +79 -0
  22. package/dist/internal/credentials.d.ts.map +1 -0
  23. package/dist/internal/credentials.js +241 -0
  24. package/dist/internal/credentials.js.map +1 -0
  25. package/dist/internal/errors.d.ts +2 -0
  26. package/dist/internal/errors.d.ts.map +1 -1
  27. package/dist/internal/errors.js +6 -0
  28. package/dist/internal/errors.js.map +1 -1
  29. package/dist/internal/utilities.d.ts +1 -1
  30. package/dist/internal/utilities.d.ts.map +1 -1
  31. package/dist/internal/utilities.js +4 -5
  32. package/dist/internal/utilities.js.map +1 -1
  33. package/package.json +1 -1
  34. package/src/AmazonBedrockClient.ts +184 -95
  35. package/src/AmazonBedrockConfig.ts +6 -6
  36. package/src/AmazonBedrockEventStream.ts +3 -3
  37. package/src/AmazonBedrockLanguageModel.ts +136 -62
  38. package/src/AmazonBedrockSchema.ts +77 -54
  39. package/src/index.ts +6 -6
  40. package/src/internal/credentials.ts +319 -0
  41. package/src/internal/errors.ts +5 -0
  42. package/src/internal/utilities.ts +7 -7
@@ -23,7 +23,7 @@
23
23
  * deltas Converse returns are surfaced as document source parts.
24
24
  *
25
25
  * @stability unstable
26
- * @since 4.0.0
26
+ * @since 0.1.0
27
27
  */
28
28
  import * as AiError from "effect/ai/AiError"
29
29
  import { toCodecAnthropic } from "effect/ai/AnthropicStructuredOutput"
@@ -73,7 +73,7 @@ import * as InternalUtilities from "./internal/utilities.ts"
73
73
  *
74
74
  * @stability unstable
75
75
  * @category models
76
- * @since 4.0.0
76
+ * @since 0.1.0
77
77
  */
78
78
  export type Model = string & {}
79
79
 
@@ -92,7 +92,7 @@ export type Model = string & {}
92
92
  *
93
93
  * @stability unstable
94
94
  * @category models
95
- * @since 4.0.0
95
+ * @since 0.1.0
96
96
  */
97
97
  export type CachePoint = typeof CachePointBlock.Encoded
98
98
 
@@ -123,7 +123,7 @@ export type CachePoint = typeof CachePointBlock.Encoded
123
123
  *
124
124
  * @stability unstable
125
125
  * @category services
126
- * @since 4.0.0
126
+ * @since 0.1.0
127
127
  */
128
128
  export const ToolCachePoint = Context.Reference<CachePoint | undefined>(
129
129
  "@wmaurer/effect-ai-amazon-bedrock/AmazonBedrockLanguageModel/ToolCachePoint",
@@ -154,7 +154,7 @@ declare module "effect/ai/Prompt" {
154
154
  *
155
155
  * @stability unstable
156
156
  * @category models
157
- * @since 4.0.0
157
+ * @since 0.1.0
158
158
  */
159
159
  export interface SystemMessageOptions extends CachePointOptions {}
160
160
 
@@ -163,7 +163,7 @@ declare module "effect/ai/Prompt" {
163
163
  *
164
164
  * @stability unstable
165
165
  * @category models
166
- * @since 4.0.0
166
+ * @since 0.1.0
167
167
  */
168
168
  export interface UserMessageOptions extends CachePointOptions {}
169
169
 
@@ -172,7 +172,7 @@ declare module "effect/ai/Prompt" {
172
172
  *
173
173
  * @stability unstable
174
174
  * @category models
175
- * @since 4.0.0
175
+ * @since 0.1.0
176
176
  */
177
177
  export interface AssistantMessageOptions extends CachePointOptions {}
178
178
 
@@ -181,7 +181,7 @@ declare module "effect/ai/Prompt" {
181
181
  *
182
182
  * @stability unstable
183
183
  * @category models
184
- * @since 4.0.0
184
+ * @since 0.1.0
185
185
  */
186
186
  export interface ToolMessageOptions extends CachePointOptions {}
187
187
 
@@ -190,7 +190,7 @@ declare module "effect/ai/Prompt" {
190
190
  *
191
191
  * @stability unstable
192
192
  * @category models
193
- * @since 4.0.0
193
+ * @since 0.1.0
194
194
  */
195
195
  export interface TextPartOptions extends CachePointOptions {}
196
196
 
@@ -199,7 +199,7 @@ declare module "effect/ai/Prompt" {
199
199
  *
200
200
  * @stability unstable
201
201
  * @category models
202
- * @since 4.0.0
202
+ * @since 0.1.0
203
203
  */
204
204
  export interface ToolCallPartOptions extends CachePointOptions {}
205
205
 
@@ -208,7 +208,7 @@ declare module "effect/ai/Prompt" {
208
208
  *
209
209
  * @stability unstable
210
210
  * @category models
211
- * @since 4.0.0
211
+ * @since 0.1.0
212
212
  */
213
213
  export interface ToolResultPartOptions extends CachePointOptions {}
214
214
  }
@@ -242,7 +242,7 @@ const pushCachePoint = (
242
242
  *
243
243
  * @stability unstable
244
244
  * @category models
245
- * @since 4.0.0
245
+ * @since 0.1.0
246
246
  */
247
247
  export type Citations = typeof CitationsConfig.Encoded
248
248
 
@@ -283,7 +283,7 @@ declare module "effect/ai/Prompt" {
283
283
  *
284
284
  * @stability unstable
285
285
  * @category models
286
- * @since 4.0.0
286
+ * @since 0.1.0
287
287
  */
288
288
  export interface FilePartOptions extends FileOptions {}
289
289
  }
@@ -299,7 +299,7 @@ declare module "effect/ai/Response" {
299
299
  *
300
300
  * @stability unstable
301
301
  * @category models
302
- * @since 4.0.0
302
+ * @since 0.1.0
303
303
  */
304
304
  export interface DocumentSourcePartMetadata extends ProviderMetadata {
305
305
  readonly amazonBedrock?: {
@@ -403,7 +403,7 @@ const citationSource: (
403
403
  *
404
404
  * @stability unstable
405
405
  * @category models
406
- * @since 4.0.0
406
+ * @since 0.1.0
407
407
  */
408
408
  export type ReasoningInfo = {
409
409
  readonly type: "reasoningText"
@@ -433,7 +433,7 @@ declare module "effect/ai/Prompt" {
433
433
  *
434
434
  * @stability unstable
435
435
  * @category models
436
- * @since 4.0.0
436
+ * @since 0.1.0
437
437
  */
438
438
  export interface ReasoningPartOptions extends ProviderOptions {
439
439
  readonly amazonBedrock?: {
@@ -448,7 +448,7 @@ declare module "effect/ai/Response" {
448
448
  *
449
449
  * @stability unstable
450
450
  * @category models
451
- * @since 4.0.0
451
+ * @since 0.1.0
452
452
  */
453
453
  export interface ReasoningPartMetadata extends ProviderMetadata {
454
454
  readonly amazonBedrock?: {
@@ -467,7 +467,7 @@ declare module "effect/ai/Response" {
467
467
  *
468
468
  * @stability unstable
469
469
  * @category models
470
- * @since 4.0.0
470
+ * @since 0.1.0
471
471
  */
472
472
  export interface ReasoningDeltaPartMetadata extends ProviderMetadata {
473
473
  readonly amazonBedrock?: {
@@ -525,7 +525,7 @@ const reasoningTextInfo = (signature: string | undefined): ReasoningInfo => ({
525
525
  *
526
526
  * @stability unstable
527
527
  * @category configuration
528
- * @since 4.0.0
528
+ * @since 0.1.0
529
529
  */
530
530
  export class Config extends Context.Service<
531
531
  Config,
@@ -554,11 +554,15 @@ export class Config extends Context.Service<
554
554
  *
555
555
  * **Details**
556
556
  *
557
- * Overrides automatic capability detection based on the model
558
- * identifier. When `false`, `generateObject` asks for the object through
559
- * a tool instead, which is forced on models that accept a forced
560
- * `toolChoice` and left unforced, with an instruction to call it, on
561
- * models that do not. Never sent to Bedrock.
557
+ * Overrides the default for the provider that the model identifier
558
+ * names. By default only Claude models, and ARNs that do not name their
559
+ * model, get native structured output. In probes on 2026-10-17 in
560
+ * eu-west-1 and us-east-1, Nova, Mistral Large 2402, Pixtral Large,
561
+ * Llama, and Palmyra answered `outputConfig` with a 400. When `false`,
562
+ * `generateObject` asks for the object through a tool instead, which is
563
+ * forced on models that accept a forced `toolChoice` and left unforced,
564
+ * with an instruction to call it, on models that do not. Never sent to
565
+ * Bedrock.
562
566
  *
563
567
  * When the prompt holds tool calls or tool results and the request
564
568
  * offers no tools, `generateObject` uses the tool even when this is
@@ -571,12 +575,19 @@ export class Config extends Context.Service<
571
575
  *
572
576
  * **Details**
573
577
  *
574
- * Overrides automatic capability detection based on the model
575
- * identifier. It decides whether the tool that `generateObject` uses
576
- * without native structured output is forced, so with
577
- * `structuredOutputs: false` it selects between the forced and the
578
- * unforced tool, for example for an application inference profile ARN,
579
- * which does not name its model. Never sent to Bedrock.
578
+ * Overrides the default for the provider that the model identifier
579
+ * names. It decides whether the tool that `generateObject` uses
580
+ * without native structured output is forced with `toolChoice`
581
+ * `{ any: {} }`, so with `structuredOutputs: false` it selects between
582
+ * the forced and the unforced tool, for example for an application
583
+ * inference profile ARN, which does not name its model. Never sent to
584
+ * Bedrock.
585
+ *
586
+ * By default Meta and Writer models, and Claude Opus 5.5, Sonnet 5.5,
587
+ * Fable 5.1, and Mythos 5.1, get the unforced tool. In probes on
588
+ * 2026-10-17 in us-east-1, Llama 3.1, 3.3, and 4 and Palmyra X4 and X5
589
+ * answered `any` with a 400, and in eu-west-1 Opus 5.5 and Sonnet 5.5
590
+ * rejected both `any` and a named `tool`.
580
591
  *
581
592
  * It steers only that tool. A `toolChoice` passed with the caller's own
582
593
  * tools, such as `"required"` or a named tool, goes to Bedrock
@@ -598,7 +609,7 @@ export class Config extends Context.Service<
598
609
  *
599
610
  * @stability unstable
600
611
  * @category constructors
601
- * @since 4.0.0
612
+ * @since 0.1.0
602
613
  */
603
614
  export const model = (
604
615
  model: (string & {}) | Model,
@@ -612,7 +623,7 @@ export const model = (
612
623
  *
613
624
  * @stability unstable
614
625
  * @category constructors
615
- * @since 4.0.0
626
+ * @since 0.1.0
616
627
  */
617
628
  export const make = Effect.fnUntraced(function*({ config: providerConfig, model }: {
618
629
  readonly model: (string & {}) | Model
@@ -674,9 +685,9 @@ export const make = Effect.fnUntraced(function*({ config: providerConfig, model
674
685
  }
675
686
  }
676
687
  } else {
677
- // The object is asked for through a tool, which replaces the
678
- // caller's tools. A model that rejects a forced `toolChoice` gets
679
- // the tool unforced, and the request says to call it.
688
+ // The object tool is the only tool, so `any` forces it, and
689
+ // Mistral Large 2402 and Pixtral Large accept `any` but reject a
690
+ // named `tool`.
680
691
  const forced = forcedToolChoice ?? capabilities.supportsForcedToolChoice
681
692
  nameMapper = new Tool.NameMapper([])
682
693
  toolConfig = {
@@ -687,7 +698,7 @@ export const make = Effect.fnUntraced(function*({ config: providerConfig, model
687
698
  inputSchema: { json: json as Record<string, unknown> }
688
699
  }
689
700
  }],
690
- toolChoice: forced ? { tool: { name: responseFormat.objectName } } : { auto: {} }
701
+ toolChoice: forced ? { any: {} } : { auto: {} }
691
702
  }
692
703
  if (!forced) {
693
704
  system = [...(system ?? []), {
@@ -742,7 +753,7 @@ export const make = Effect.fnUntraced(function*({ config: providerConfig, model
742
753
  *
743
754
  * @stability unstable
744
755
  * @category layers
745
- * @since 4.0.0
756
+ * @since 0.1.0
746
757
  */
747
758
  export const layer = (options: {
748
759
  readonly model: (string & {}) | Model
@@ -755,7 +766,7 @@ export const layer = (options: {
755
766
  *
756
767
  * @stability unstable
757
768
  * @category configuration
758
- * @since 4.0.0
769
+ * @since 0.1.0
759
770
  */
760
771
  export const withConfigOverride: {
761
772
  (overrides: typeof Config.Service): <A, E, R>(self: Effect.Effect<A, E, R>) => Effect.Effect<A, E, Exclude<R, Config>>
@@ -1124,16 +1135,36 @@ interface ModelCapabilities {
1124
1135
  }
1125
1136
 
1126
1137
  /**
1127
- * Returns what a model accepts for structured output, optimistically assuming
1128
- * modern capabilities for unknown IDs. Matched by substring, so foundation
1129
- * model ids and inference profile ids (`eu.anthropic.…`) both resolve.
1138
+ * Returns what a model accepts for structured output, looked up by the
1139
+ * provider that the model ID names: `anthropic` in
1140
+ * `eu.anthropic.claude-opus-4-8`, `amazon` in `amazon.nova-pro-v1:0`. A
1141
+ * foundation model or inference profile ARN names its model at the end. Other
1142
+ * ARNs, such as an application inference profile or a provisioned model, do
1143
+ * not. They get the Claude rules, which give them the native structured output
1144
+ * and forced tool of 0.2.0, on the unmeasured assumption that most of them
1145
+ * wrap a Claude model.
1146
+ *
1147
+ * Probes on 2026-10-17, one run per model and call unless noted:
1130
1148
  *
1149
+ * - Only Claude gets native structured output. Nova, Mistral Large 2402 and
1150
+ * Pixtral Large in eu-west-1, and Llama, Palmyra, DeepSeek R1, Mistral 7B
1151
+ * and Mixtral in us-east-1, answer `outputConfig` with a 400. gpt-oss-120b
1152
+ * accepts it but wrote text before the JSON in 3 of 4 runs.
1153
+ * - The forced object tool, sent as `{ any: {} }`, returned the object on 15
1154
+ * of 16 models in eu-west-1, Claude Haiku 4.5 and Opus 4.8 included, and
1155
+ * on all 17 models in us-east-1 that accept a forced tool. Magistral Small
1156
+ * fails every object path.
1157
+ * - Llama 3.1, 3.3 and 4 and Palmyra X4 and X5 (`meta`, `writer`) answer
1158
+ * `any` with a 400 in us-east-1, and so does each of them that the probe
1159
+ * sent a named `tool`, all but Llama 3.1 8B. They get the unforced tool. It
1160
+ * returned the object on Llama 3.1 8B, Llama 4 Scout and Maverick and
1161
+ * Palmyra X5, and a wrong object on Llama 3.1 70B and 3.3 70B.
1131
1162
  * - Claude models before Opus 4.1 / Sonnet 4.5 predate structured output, as
1132
1163
  * in the Anthropic provider. This is not checked on Bedrock: a probe of 17
1133
- * Claude inference profiles in eu-west-1 on 2026-10-17 found Claude 3 Haiku,
1134
- * Claude 3 Sonnet and Sonnet 4 at end of life there, and did not try Claude
1135
- * 3 Opus, 3.5 Haiku, 3.5 Sonnet, 3.7 Sonnet, Opus 4 or Opus 4.1.
1136
- * - Every Claude profile that served the probe's requests (12 of 17)
1164
+ * Claude inference profiles in eu-west-1 found Claude 3 Haiku, Claude 3
1165
+ * Sonnet and Sonnet 4 at end of life there, and did not try Claude 3 Opus,
1166
+ * 3.5 Haiku, 3.5 Sonnet, 3.7 Sonnet, Opus 4 or Opus 4.1.
1167
+ * - Every Claude profile that served that probe's requests (12 of 17)
1137
1168
  * accepted `outputConfig.textFormat`: Haiku 4.5 and 5.5, Opus 4.5 to 4.8, 5
1138
1169
  * and 5.5, and Sonnet 4.5, 4.6, 5 and 5.5. Fable 5 and 5.1 rejected every
1139
1170
  * request, with or without `textFormat`, with a data-retention
@@ -1146,23 +1177,51 @@ interface ModelCapabilities {
1146
1177
  * @see https://platform.claude.com/docs/en/build-with-claude/structured-outputs
1147
1178
  */
1148
1179
  const getModelCapabilities = (modelId: string): ModelCapabilities => {
1149
- const supportsForcedToolChoice = !(
1150
- modelId.includes("claude-opus-5-5") ||
1151
- modelId.includes("claude-sonnet-5-5") ||
1152
- modelId.includes("claude-fable-5-1") ||
1153
- modelId.includes("claude-mythos-5-1")
1154
- )
1155
- const supportsStructuredOutput = !(
1156
- modelId.includes("claude-sonnet-4-0") ||
1157
- modelId.includes("claude-sonnet-4-20250514") ||
1158
- modelId.includes("claude-3-7-sonnet") ||
1159
- modelId.includes("claude-opus-4-0") ||
1160
- modelId.includes("claude-opus-4-20250514") ||
1161
- modelId.includes("claude-3-")
1162
- )
1163
- return { supportsStructuredOutput, supportsForcedToolChoice }
1180
+ const named = modelId.startsWith("arn:") ? namingArn.exec(modelId)?.[1] : modelId
1181
+ if (Predicate.isUndefined(named)) {
1182
+ return claudeCapabilities(modelId)
1183
+ }
1184
+ // `provider.model`, or `geo.provider.model` for an inference profile.
1185
+ const [first = "", second = ""] = named.split(".")
1186
+ const byProvider = capabilitiesByProvider.get(first) ?? capabilitiesByProvider.get(second)
1187
+ return Predicate.isUndefined(byProvider) ? otherProvider : byProvider(named)
1164
1188
  }
1165
1189
 
1190
+ // Of the ARNs Converse accepts, only these two end in the ID of the model
1191
+ // they invoke.
1192
+ const namingArn = /^arn:[^:]+:bedrock:[^:]*:[^:]*:(?:foundation-model|inference-profile)\/(.+)$/
1193
+
1194
+ const otherProvider: ModelCapabilities = { supportsStructuredOutput: false, supportsForcedToolChoice: true }
1195
+
1196
+ const unforcedTool: ModelCapabilities = { supportsStructuredOutput: false, supportsForcedToolChoice: false }
1197
+
1198
+ const claudeWithoutStructuredOutput = [
1199
+ "claude-sonnet-4-0",
1200
+ "claude-sonnet-4-20250514",
1201
+ "claude-3-7-sonnet",
1202
+ "claude-opus-4-0",
1203
+ "claude-opus-4-20250514",
1204
+ "claude-3-"
1205
+ ]
1206
+
1207
+ const claudeWithoutForcedToolChoice = [
1208
+ "claude-opus-5-5",
1209
+ "claude-sonnet-5-5",
1210
+ "claude-fable-5-1",
1211
+ "claude-mythos-5-1"
1212
+ ]
1213
+
1214
+ const claudeCapabilities = (modelId: string): ModelCapabilities => ({
1215
+ supportsStructuredOutput: !claudeWithoutStructuredOutput.some((id) => modelId.includes(id)),
1216
+ supportsForcedToolChoice: !claudeWithoutForcedToolChoice.some((id) => modelId.includes(id))
1217
+ })
1218
+
1219
+ const capabilitiesByProvider = new Map<string, (modelId: string) => ModelCapabilities>([
1220
+ ["anthropic", claudeCapabilities],
1221
+ ["meta", () => unforcedTool],
1222
+ ["writer", () => unforcedTool]
1223
+ ])
1224
+
1166
1225
  const tryJsonSchema = (schema: Schema.Top, method: string) =>
1167
1226
  Effect.try({
1168
1227
  try: () => Tool.getJsonSchemaFromSchema(schema, { transformer: toCodecAnthropic }),
@@ -1591,6 +1650,19 @@ const makeStreamResponse: (
1591
1650
  const block = toolBlocks.get(index)
1592
1651
  if (Predicate.isNotUndefined(block)) {
1593
1652
  toolBlocks.delete(index)
1653
+ const params = yield* Effect.try({
1654
+ // A tool taking no arguments streams no deltas at all.
1655
+ try: () => Tool.unsafeSecureJsonParse(block.params.length === 0 ? "{}" : block.params),
1656
+ catch: () =>
1657
+ AiError.make({
1658
+ module: "AmazonBedrockLanguageModel",
1659
+ method: "makeStreamResponse",
1660
+ reason: new AiError.InvalidOutputError({
1661
+ description: `Bedrock streamed input for the \`${block.name}\` tool that is not valid JSON: ` +
1662
+ truncate(block.params, 500)
1663
+ })
1664
+ })
1665
+ })
1594
1666
  parts.push({
1595
1667
  type: "tool-params-end",
1596
1668
  id: block.id
@@ -1599,8 +1671,7 @@ const makeStreamResponse: (
1599
1671
  type: "tool-call",
1600
1672
  id: block.id,
1601
1673
  name: block.name,
1602
- // A tool taking no arguments streams no deltas at all.
1603
- params: Tool.unsafeSecureJsonParse(block.params.length === 0 ? "{}" : block.params),
1674
+ params,
1604
1675
  providerExecuted: false
1605
1676
  })
1606
1677
  }
@@ -1744,3 +1815,6 @@ const trimIfLast = (
1744
1815
  isLastPart: boolean,
1745
1816
  text: string
1746
1817
  ) => isLastGroup && isLastMessage && isLastPart ? text.trim() : text
1818
+
1819
+ const truncate = (text: string, maxLength: number): string =>
1820
+ text.length > maxLength ? `${text.slice(0, maxLength)}...` : text