@wmaurer/effect-ai-amazon-bedrock 0.2.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +21 -0
- package/README.md +98 -9
- package/dist/AmazonBedrockClient.d.ts +2 -2
- package/dist/AmazonBedrockClient.d.ts.map +1 -1
- package/dist/AmazonBedrockLanguageModel.d.ts +188 -25
- package/dist/AmazonBedrockLanguageModel.d.ts.map +1 -1
- package/dist/AmazonBedrockLanguageModel.js +203 -37
- package/dist/AmazonBedrockLanguageModel.js.map +1 -1
- package/dist/AmazonBedrockSchema.d.ts +240 -7
- package/dist/AmazonBedrockSchema.d.ts.map +1 -1
- package/dist/AmazonBedrockSchema.js +189 -10
- package/dist/AmazonBedrockSchema.js.map +1 -1
- package/dist/internal/errors.d.ts.map +1 -1
- package/dist/internal/errors.js +8 -0
- package/dist/internal/errors.js.map +1 -1
- package/dist/internal/guardrails.d.ts +15 -0
- package/dist/internal/guardrails.d.ts.map +1 -0
- package/dist/internal/guardrails.js +77 -0
- package/dist/internal/guardrails.js.map +1 -0
- package/dist/internal/utilities.d.ts +1 -1
- package/dist/internal/utilities.d.ts.map +1 -1
- package/dist/internal/utilities.js +4 -5
- package/dist/internal/utilities.js.map +1 -1
- package/package.json +1 -1
- package/src/AmazonBedrockClient.ts +2 -2
- package/src/AmazonBedrockLanguageModel.ts +422 -74
- package/src/AmazonBedrockSchema.ts +219 -12
- package/src/internal/errors.ts +14 -0
- package/src/internal/guardrails.ts +91 -0
- package/src/internal/utilities.ts +7 -7
|
@@ -20,7 +20,11 @@
|
|
|
20
20
|
* loudly with `AiError.InvalidUserInputError` rather than silently dropping
|
|
21
21
|
* content. Prompt caching is opt-in per message or per part via provider
|
|
22
22
|
* options. Citations are opt-in per document, and the citation blocks and
|
|
23
|
-
* deltas Converse returns are surfaced as document source parts.
|
|
23
|
+
* deltas Converse returns are surfaced as document source parts. A guardrail
|
|
24
|
+
* applies through `Config.guardrailConfig`, and `guardContent` options on text
|
|
25
|
+
* parts, png and jpeg file parts, and system messages choose what it
|
|
26
|
+
* evaluates. Every finish part reports the raw stop reason and, when the trace
|
|
27
|
+
* is enabled, what the guardrail did.
|
|
24
28
|
*
|
|
25
29
|
* @stability unstable
|
|
26
30
|
* @since 0.1.0
|
|
@@ -54,8 +58,13 @@ import type {
|
|
|
54
58
|
ConverseRequest,
|
|
55
59
|
ConverseResponse,
|
|
56
60
|
ConverseResponseStreamEvent,
|
|
61
|
+
ConverseStreamRequest,
|
|
57
62
|
DocumentBlock,
|
|
58
63
|
DocumentSource,
|
|
64
|
+
GuardrailConverseContentBlock,
|
|
65
|
+
GuardrailConverseContentQualifier,
|
|
66
|
+
GuardrailConverseImageBlock,
|
|
67
|
+
GuardrailStreamConfiguration,
|
|
59
68
|
ImageBlock,
|
|
60
69
|
MediaSource,
|
|
61
70
|
Message,
|
|
@@ -65,6 +74,7 @@ import type {
|
|
|
65
74
|
ToolChoice,
|
|
66
75
|
ToolConfiguration
|
|
67
76
|
} from "./AmazonBedrockSchema.ts"
|
|
77
|
+
import * as InternalGuardrails from "./internal/guardrails.ts"
|
|
68
78
|
import * as InternalUtilities from "./internal/utilities.ts"
|
|
69
79
|
|
|
70
80
|
/**
|
|
@@ -149,15 +159,6 @@ interface CachePointOptions {
|
|
|
149
159
|
}
|
|
150
160
|
|
|
151
161
|
declare module "effect/ai/Prompt" {
|
|
152
|
-
/**
|
|
153
|
-
* Amazon Bedrock options for system messages.
|
|
154
|
-
*
|
|
155
|
-
* @stability unstable
|
|
156
|
-
* @category models
|
|
157
|
-
* @since 0.1.0
|
|
158
|
-
*/
|
|
159
|
-
export interface SystemMessageOptions extends CachePointOptions {}
|
|
160
|
-
|
|
161
162
|
/**
|
|
162
163
|
* Amazon Bedrock options for user messages.
|
|
163
164
|
*
|
|
@@ -185,15 +186,6 @@ declare module "effect/ai/Prompt" {
|
|
|
185
186
|
*/
|
|
186
187
|
export interface ToolMessageOptions extends CachePointOptions {}
|
|
187
188
|
|
|
188
|
-
/**
|
|
189
|
-
* Amazon Bedrock options for text prompt parts.
|
|
190
|
-
*
|
|
191
|
-
* @stability unstable
|
|
192
|
-
* @category models
|
|
193
|
-
* @since 0.1.0
|
|
194
|
-
*/
|
|
195
|
-
export interface TextPartOptions extends CachePointOptions {}
|
|
196
|
-
|
|
197
189
|
/**
|
|
198
190
|
* Amazon Bedrock options for tool call prompt parts.
|
|
199
191
|
*
|
|
@@ -274,6 +266,13 @@ interface FileOptions {
|
|
|
274
266
|
* Converse cannot cite.
|
|
275
267
|
*/
|
|
276
268
|
readonly citations?: Citations | null
|
|
269
|
+
/**
|
|
270
|
+
* Sends the image as a `guardContent` image block, for the guardrail to
|
|
271
|
+
* evaluate. Only an `image/png` or `image/jpeg` part whose data is bytes
|
|
272
|
+
* or base64 can be guarded. Any other file part with this option fails
|
|
273
|
+
* the request with `InvalidUserInputError`.
|
|
274
|
+
*/
|
|
275
|
+
readonly guardContent?: true | null
|
|
277
276
|
} | null
|
|
278
277
|
}
|
|
279
278
|
|
|
@@ -489,6 +488,190 @@ const reasoningTextInfo = (signature: string | undefined): ReasoningInfo => ({
|
|
|
489
488
|
signature: signature ?? null
|
|
490
489
|
})
|
|
491
490
|
|
|
491
|
+
/**
|
|
492
|
+
* A guardrail to apply to every request, set as `Config.guardrailConfig`.
|
|
493
|
+
*
|
|
494
|
+
* **Details**
|
|
495
|
+
*
|
|
496
|
+
* `generateText` and `generateObject` send it to Converse without
|
|
497
|
+
* `streamProcessingMode`, which Converse does not have. `streamText` sends it
|
|
498
|
+
* to ConverseStream as given.
|
|
499
|
+
*
|
|
500
|
+
* Without `guardContent` on any part, the guardrail evaluates every message
|
|
501
|
+
* and not the system prompt. Set `trace` to `"enabled"` to learn from the
|
|
502
|
+
* finish part whether the guardrail blocked or masked. The trace holds the
|
|
503
|
+
* matched text unmasked.
|
|
504
|
+
*
|
|
505
|
+
* @stability unstable
|
|
506
|
+
* @category models
|
|
507
|
+
* @since 0.4.0
|
|
508
|
+
*/
|
|
509
|
+
export type GuardrailConfig = typeof GuardrailStreamConfiguration.Encoded
|
|
510
|
+
|
|
511
|
+
/**
|
|
512
|
+
* The role a guarded text part plays in a contextual grounding check.
|
|
513
|
+
*
|
|
514
|
+
* @stability unstable
|
|
515
|
+
* @category models
|
|
516
|
+
* @since 0.4.0
|
|
517
|
+
*/
|
|
518
|
+
export type GuardContentQualifier = typeof GuardrailConverseContentQualifier.Type
|
|
519
|
+
|
|
520
|
+
/**
|
|
521
|
+
* Marks a text part or a system message for the guardrail to evaluate.
|
|
522
|
+
*
|
|
523
|
+
* **Details**
|
|
524
|
+
*
|
|
525
|
+
* `true` guards the text. `qualifiers` names its role in a contextual
|
|
526
|
+
* grounding check: the source documents are `grounding_source`, and the
|
|
527
|
+
* question is `query`. Once any part of a request is guarded, the guardrail
|
|
528
|
+
* evaluates only the guarded parts. The model still reads every part.
|
|
529
|
+
*
|
|
530
|
+
* The option stays on the part, so in a chat a guarded turn is evaluated
|
|
531
|
+
* again on every later turn.
|
|
532
|
+
*
|
|
533
|
+
* @stability unstable
|
|
534
|
+
* @category models
|
|
535
|
+
* @since 0.4.0
|
|
536
|
+
*/
|
|
537
|
+
export type GuardContent = true | {
|
|
538
|
+
readonly qualifiers: ReadonlyArray<GuardContentQualifier>
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
interface GuardableOptions {
|
|
542
|
+
readonly amazonBedrock?: {
|
|
543
|
+
/**
|
|
544
|
+
* Marks the end of the reusable prefix of the request. The cache point is
|
|
545
|
+
* emitted after the content this is attached to.
|
|
546
|
+
*/
|
|
547
|
+
readonly cachePoint?: CachePoint | null
|
|
548
|
+
/**
|
|
549
|
+
* Sends the text as a `guardContent` block, for the guardrail to evaluate.
|
|
550
|
+
* Honoured on user text parts and system messages. On an assistant text
|
|
551
|
+
* part it fails the request with `InvalidUserInputError`.
|
|
552
|
+
*/
|
|
553
|
+
readonly guardContent?: GuardContent | null
|
|
554
|
+
} | null
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
declare module "effect/ai/Prompt" {
|
|
558
|
+
/**
|
|
559
|
+
* Amazon Bedrock options for system messages.
|
|
560
|
+
*
|
|
561
|
+
* @stability unstable
|
|
562
|
+
* @category models
|
|
563
|
+
* @since 0.1.0
|
|
564
|
+
*/
|
|
565
|
+
export interface SystemMessageOptions extends GuardableOptions {}
|
|
566
|
+
|
|
567
|
+
/**
|
|
568
|
+
* Amazon Bedrock options for text prompt parts.
|
|
569
|
+
*
|
|
570
|
+
* @stability unstable
|
|
571
|
+
* @category models
|
|
572
|
+
* @since 0.1.0
|
|
573
|
+
*/
|
|
574
|
+
export interface TextPartOptions extends GuardableOptions {}
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
const textBlock = (
|
|
578
|
+
text: string,
|
|
579
|
+
options: GuardableOptions
|
|
580
|
+
): { readonly text: string } | { readonly guardContent: typeof GuardrailConverseContentBlock.Encoded } => {
|
|
581
|
+
const guard = options.amazonBedrock?.guardContent
|
|
582
|
+
if (Predicate.isNullish(guard)) {
|
|
583
|
+
return { text }
|
|
584
|
+
}
|
|
585
|
+
return { guardContent: { text: guard === true ? { text } : { text, qualifiers: guard.qualifiers } } }
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
/**
|
|
589
|
+
* What a guardrail did to one side of the exchange.
|
|
590
|
+
*
|
|
591
|
+
* @stability unstable
|
|
592
|
+
* @category models
|
|
593
|
+
* @since 0.4.0
|
|
594
|
+
*/
|
|
595
|
+
export type GuardrailAction = "blocked" | "masked" | "none"
|
|
596
|
+
|
|
597
|
+
/**
|
|
598
|
+
* What the guardrails did, read from the trace.
|
|
599
|
+
*
|
|
600
|
+
* **Details**
|
|
601
|
+
*
|
|
602
|
+
* `input` and `output` are the strongest action that any guardrail took on
|
|
603
|
+
* that side, `blocked` over `masked` over `none`, across every guardrail in
|
|
604
|
+
* the trace, enforced guardrails included. `policies` lists the trace keys of
|
|
605
|
+
* the policies that blocked or masked, such as `wordPolicy` or
|
|
606
|
+
* `sensitiveInformationPolicy`, each once, in the order the trace names them.
|
|
607
|
+
*
|
|
608
|
+
* @stability unstable
|
|
609
|
+
* @category models
|
|
610
|
+
* @since 0.4.0
|
|
611
|
+
*/
|
|
612
|
+
export type GuardrailOutcome = {
|
|
613
|
+
readonly input: GuardrailAction
|
|
614
|
+
readonly output: GuardrailAction
|
|
615
|
+
// Mutable, because the same type rides on ContentPolicyError, whose
|
|
616
|
+
// metadata must be MutableJson.
|
|
617
|
+
readonly policies: Array<string>
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
/**
|
|
621
|
+
* The Amazon Bedrock metadata on every finish part.
|
|
622
|
+
*
|
|
623
|
+
* **Details**
|
|
624
|
+
*
|
|
625
|
+
* `stopReason` is the raw Converse stop reason. It tells
|
|
626
|
+
* `guardrail_intervened` from `content_filtered`, which both finish with
|
|
627
|
+
* `"content-filter"`, and `max_tokens` from `model_context_window_exceeded`,
|
|
628
|
+
* which both finish with `"length"`. It is `null` only when a stream ended
|
|
629
|
+
* without a `messageStop` event.
|
|
630
|
+
*
|
|
631
|
+
* `guardrail` is `null` when the response carried no guardrail trace, which
|
|
632
|
+
* is the case unless `trace` is enabled in the guardrail config. A trace in
|
|
633
|
+
* which nothing acted gives `{ input: "none", output: "none", policies: [] }`.
|
|
634
|
+
* `trace` is the Converse trace as Bedrock sent it, or `null`. It holds the
|
|
635
|
+
* text that a policy matched, unmasked.
|
|
636
|
+
*
|
|
637
|
+
* @stability unstable
|
|
638
|
+
* @category models
|
|
639
|
+
* @since 0.4.0
|
|
640
|
+
*/
|
|
641
|
+
export type FinishMetadata = {
|
|
642
|
+
readonly stopReason: string | null
|
|
643
|
+
readonly guardrail: GuardrailOutcome | null
|
|
644
|
+
readonly trace: Schema.MutableJson | null
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
declare module "effect/ai/AiError" {
|
|
648
|
+
/**
|
|
649
|
+
* Amazon Bedrock metadata on the `ContentPolicyError` that `generateObject`
|
|
650
|
+
* fails with when a guardrail blocked the exchange. It is the metadata of
|
|
651
|
+
* the response's finish part.
|
|
652
|
+
*
|
|
653
|
+
* @stability unstable
|
|
654
|
+
* @category models
|
|
655
|
+
* @since 0.4.0
|
|
656
|
+
*/
|
|
657
|
+
export interface ContentPolicyErrorMetadata extends ProviderMetadata {
|
|
658
|
+
readonly amazonBedrock?: FinishMetadata | null
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
declare module "effect/ai/Response" {
|
|
663
|
+
/**
|
|
664
|
+
* Amazon Bedrock metadata on the finish part.
|
|
665
|
+
*
|
|
666
|
+
* @stability unstable
|
|
667
|
+
* @category models
|
|
668
|
+
* @since 0.4.0
|
|
669
|
+
*/
|
|
670
|
+
export interface FinishPartMetadata extends ProviderMetadata {
|
|
671
|
+
readonly amazonBedrock?: FinishMetadata | null
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
|
|
492
675
|
// =============================================================================
|
|
493
676
|
// Configuration
|
|
494
677
|
// =============================================================================
|
|
@@ -498,12 +681,17 @@ const reasoningTextInfo = (signature: string | undefined): ReasoningInfo => ({
|
|
|
498
681
|
*
|
|
499
682
|
* **Details**
|
|
500
683
|
*
|
|
501
|
-
* Derived from `
|
|
502
|
-
* every Converse request parameter is settable per request. That
|
|
503
|
-
* `additionalModelRequestFields`, which carries inference parameters
|
|
504
|
-
* to the target model — among them extended thinking on Anthropic
|
|
505
|
-
* thinking budget is drawn from the same ceiling as the visible
|
|
506
|
-
* `inferenceConfig.maxTokens` must exceed `budget_tokens`.
|
|
684
|
+
* Derived from `ConverseStreamRequest`, minus the fields built from the
|
|
685
|
+
* prompt, so every Converse request parameter is settable per request. That
|
|
686
|
+
* includes `additionalModelRequestFields`, which carries inference parameters
|
|
687
|
+
* specific to the target model — among them extended thinking on Anthropic
|
|
688
|
+
* models. The thinking budget is drawn from the same ceiling as the visible
|
|
689
|
+
* answer, so `inferenceConfig.maxTokens` must exceed `budget_tokens`.
|
|
690
|
+
*
|
|
691
|
+
* It also includes `guardrailConfig` (see {@link GuardrailConfig}). An
|
|
692
|
+
* override replaces the whole object, and `guardrailConfig: undefined` turns
|
|
693
|
+
* off a layer's guardrail for one request. `generateText` does not send
|
|
694
|
+
* `streamProcessingMode`.
|
|
507
695
|
*
|
|
508
696
|
* **Example** (Enabling extended thinking for one request)
|
|
509
697
|
*
|
|
@@ -532,7 +720,7 @@ export class Config extends Context.Service<
|
|
|
532
720
|
Simplify<
|
|
533
721
|
& Partial<
|
|
534
722
|
Omit<
|
|
535
|
-
typeof
|
|
723
|
+
typeof ConverseStreamRequest.Encoded,
|
|
536
724
|
"messages" | "system" | "outputConfig"
|
|
537
725
|
>
|
|
538
726
|
>
|
|
@@ -554,11 +742,15 @@ export class Config extends Context.Service<
|
|
|
554
742
|
*
|
|
555
743
|
* **Details**
|
|
556
744
|
*
|
|
557
|
-
* Overrides
|
|
558
|
-
*
|
|
559
|
-
*
|
|
560
|
-
*
|
|
561
|
-
*
|
|
745
|
+
* Overrides the default for the provider that the model identifier
|
|
746
|
+
* names. By default only Claude models, and ARNs that do not name their
|
|
747
|
+
* model, get native structured output. In probes on 2026-10-17 in
|
|
748
|
+
* eu-west-1 and us-east-1, Nova, Mistral Large 2402, Pixtral Large,
|
|
749
|
+
* Llama, and Palmyra answered `outputConfig` with a 400. When `false`,
|
|
750
|
+
* `generateObject` asks for the object through a tool instead, which is
|
|
751
|
+
* forced on models that accept a forced `toolChoice` and left unforced,
|
|
752
|
+
* with an instruction to call it, on models that do not. Never sent to
|
|
753
|
+
* Bedrock.
|
|
562
754
|
*
|
|
563
755
|
* When the prompt holds tool calls or tool results and the request
|
|
564
756
|
* offers no tools, `generateObject` uses the tool even when this is
|
|
@@ -571,12 +763,19 @@ export class Config extends Context.Service<
|
|
|
571
763
|
*
|
|
572
764
|
* **Details**
|
|
573
765
|
*
|
|
574
|
-
* Overrides
|
|
575
|
-
*
|
|
576
|
-
* without native structured output is forced
|
|
577
|
-
* `structuredOutputs: false` it selects between
|
|
578
|
-
* unforced tool, for example for an application
|
|
579
|
-
* which does not name its model. Never sent to
|
|
766
|
+
* Overrides the default for the provider that the model identifier
|
|
767
|
+
* names. It decides whether the tool that `generateObject` uses
|
|
768
|
+
* without native structured output is forced with `toolChoice`
|
|
769
|
+
* `{ any: {} }`, so with `structuredOutputs: false` it selects between
|
|
770
|
+
* the forced and the unforced tool, for example for an application
|
|
771
|
+
* inference profile ARN, which does not name its model. Never sent to
|
|
772
|
+
* Bedrock.
|
|
773
|
+
*
|
|
774
|
+
* By default Meta and Writer models, and Claude Opus 5.5, Sonnet 5.5,
|
|
775
|
+
* Fable 5.1, and Mythos 5.1, get the unforced tool. In probes on
|
|
776
|
+
* 2026-10-17 in us-east-1, Llama 3.1, 3.3, and 4 and Palmyra X4 and X5
|
|
777
|
+
* answered `any` with a 400, and in eu-west-1 Opus 5.5 and Sonnet 5.5
|
|
778
|
+
* rejected both `any` and a named `tool`.
|
|
580
779
|
*
|
|
581
780
|
* It steers only that tool. A `toolChoice` passed with the caller's own
|
|
582
781
|
* tools, such as `"required"` or a named tool, goes to Bedrock
|
|
@@ -624,7 +823,7 @@ export const make = Effect.fnUntraced(function*({ config: providerConfig, model
|
|
|
624
823
|
function*(
|
|
625
824
|
options: LanguageModel.ProviderOptions
|
|
626
825
|
): Effect.fn.Return<{
|
|
627
|
-
readonly request: typeof
|
|
826
|
+
readonly request: typeof ConverseStreamRequest.Encoded
|
|
628
827
|
readonly nameMapper: Tool.NameMapper<ReadonlyArray<Tool.Any>>
|
|
629
828
|
readonly documents: ReadonlyArray<CitedDocument>
|
|
630
829
|
}, AiError.AiError> {
|
|
@@ -674,9 +873,9 @@ export const make = Effect.fnUntraced(function*({ config: providerConfig, model
|
|
|
674
873
|
}
|
|
675
874
|
}
|
|
676
875
|
} else {
|
|
677
|
-
// The object is
|
|
678
|
-
//
|
|
679
|
-
//
|
|
876
|
+
// The object tool is the only tool, so `any` forces it, and
|
|
877
|
+
// Mistral Large 2402 and Pixtral Large accept `any` but reject a
|
|
878
|
+
// named `tool`.
|
|
680
879
|
const forced = forcedToolChoice ?? capabilities.supportsForcedToolChoice
|
|
681
880
|
nameMapper = new Tool.NameMapper([])
|
|
682
881
|
toolConfig = {
|
|
@@ -687,7 +886,7 @@ export const make = Effect.fnUntraced(function*({ config: providerConfig, model
|
|
|
687
886
|
inputSchema: { json: json as Record<string, unknown> }
|
|
688
887
|
}
|
|
689
888
|
}],
|
|
690
|
-
toolChoice: forced ? {
|
|
889
|
+
toolChoice: forced ? { any: {} } : { auto: {} }
|
|
691
890
|
}
|
|
692
891
|
if (!forced) {
|
|
693
892
|
system = [...(system ?? []), {
|
|
@@ -697,7 +896,7 @@ export const make = Effect.fnUntraced(function*({ config: providerConfig, model
|
|
|
697
896
|
}
|
|
698
897
|
}
|
|
699
898
|
|
|
700
|
-
const request: typeof
|
|
899
|
+
const request: typeof ConverseStreamRequest.Encoded = {
|
|
701
900
|
...config,
|
|
702
901
|
modelId,
|
|
703
902
|
system,
|
|
@@ -725,7 +924,8 @@ export const make = Effect.fnUntraced(function*({ config: providerConfig, model
|
|
|
725
924
|
return yield* LanguageModel.make({
|
|
726
925
|
codecTransformer: toCodecAnthropic,
|
|
727
926
|
generateText: Effect.fnUntraced(function*(options) {
|
|
728
|
-
const { documents, nameMapper, request } = yield* makeRequest(options)
|
|
927
|
+
const { documents, nameMapper, request: streamRequest } = yield* makeRequest(options)
|
|
928
|
+
const request = toConverseRequest(streamRequest)
|
|
729
929
|
const rawResponse = yield* client.converse({ payload: request })
|
|
730
930
|
return yield* makeResponse(request, rawResponse, options, nameMapper, documents)
|
|
731
931
|
}),
|
|
@@ -737,6 +937,17 @@ export const make = Effect.fnUntraced(function*({ config: providerConfig, model
|
|
|
737
937
|
})
|
|
738
938
|
})
|
|
739
939
|
|
|
940
|
+
const toConverseRequest = (
|
|
941
|
+
request: typeof ConverseStreamRequest.Encoded
|
|
942
|
+
): typeof ConverseRequest.Encoded => {
|
|
943
|
+
const { guardrailConfig, ...rest } = request
|
|
944
|
+
if (Predicate.isUndefined(guardrailConfig)) {
|
|
945
|
+
return rest
|
|
946
|
+
}
|
|
947
|
+
const { streamProcessingMode: _, ...converseGuardrail } = guardrailConfig
|
|
948
|
+
return { ...rest, guardrailConfig: converseGuardrail }
|
|
949
|
+
}
|
|
950
|
+
|
|
740
951
|
/**
|
|
741
952
|
* Creates a layer for the Amazon Bedrock language model.
|
|
742
953
|
*
|
|
@@ -891,6 +1102,45 @@ const documentName = (fileName: string | undefined, position: number): string =>
|
|
|
891
1102
|
return `document ${position}`
|
|
892
1103
|
}
|
|
893
1104
|
|
|
1105
|
+
const guardedImageFormats: Record<string, typeof GuardrailConverseImageBlock.Encoded["format"]> = {
|
|
1106
|
+
"image/jpeg": "jpeg",
|
|
1107
|
+
"image/png": "png"
|
|
1108
|
+
}
|
|
1109
|
+
|
|
1110
|
+
const guardedImage: (
|
|
1111
|
+
part: Prompt.FilePart
|
|
1112
|
+
) => Effect.Effect<typeof GuardrailConverseImageBlock.Encoded, AiError.AiError> = Effect.fnUntraced(
|
|
1113
|
+
function*(part) {
|
|
1114
|
+
const format = guardedImageFormats[part.mediaType]
|
|
1115
|
+
if (Predicate.isUndefined(format)) {
|
|
1116
|
+
const reason = part.mediaType === "image/*"
|
|
1117
|
+
? "its format is unknown, and a guarded image must be image/png or image/jpeg"
|
|
1118
|
+
: part.mediaType.startsWith("image/")
|
|
1119
|
+
? "a guarded image must be image/png or image/jpeg"
|
|
1120
|
+
: "only text parts and png or jpeg images can be guarded"
|
|
1121
|
+
return yield* AiError.make({
|
|
1122
|
+
module: "AmazonBedrockLanguageModel",
|
|
1123
|
+
method: "prepareMessages",
|
|
1124
|
+
reason: new AiError.InvalidUserInputError({
|
|
1125
|
+
description: `Unsupported guardContent on a file part of media type '${part.mediaType}' - ${reason}`
|
|
1126
|
+
})
|
|
1127
|
+
})
|
|
1128
|
+
}
|
|
1129
|
+
const source = yield* fileSource(part.data)
|
|
1130
|
+
if (Predicate.isUndefined(source.bytes)) {
|
|
1131
|
+
return yield* AiError.make({
|
|
1132
|
+
module: "AmazonBedrockLanguageModel",
|
|
1133
|
+
method: "prepareMessages",
|
|
1134
|
+
reason: new AiError.InvalidUserInputError({
|
|
1135
|
+
description: `Unsupported guardContent on an image at '${source.s3Location?.uri}' - a guarded image ` +
|
|
1136
|
+
"must be sent as bytes or base64, not as an s3:// location"
|
|
1137
|
+
})
|
|
1138
|
+
})
|
|
1139
|
+
}
|
|
1140
|
+
return { format, source: { bytes: source.bytes } }
|
|
1141
|
+
}
|
|
1142
|
+
)
|
|
1143
|
+
|
|
894
1144
|
const prepareMessages: (options: LanguageModel.ProviderOptions) => Effect.Effect<{
|
|
895
1145
|
readonly system: ReadonlyArray<typeof SystemContentBlock.Encoded>
|
|
896
1146
|
readonly messages: ReadonlyArray<typeof Message.Encoded>
|
|
@@ -922,7 +1172,7 @@ const prepareMessages: (options: LanguageModel.ProviderOptions) => Effect.Effect
|
|
|
922
1172
|
})
|
|
923
1173
|
}
|
|
924
1174
|
for (const message of group.messages) {
|
|
925
|
-
system.push(
|
|
1175
|
+
system.push(textBlock(message.content, message.options))
|
|
926
1176
|
const cachePoint = getCachePoint(message)
|
|
927
1177
|
if (Predicate.isNotNull(cachePoint)) {
|
|
928
1178
|
system.push({ cachePoint })
|
|
@@ -939,9 +1189,14 @@ const prepareMessages: (options: LanguageModel.ProviderOptions) => Effect.Effect
|
|
|
939
1189
|
case "user": {
|
|
940
1190
|
for (const part of message.content) {
|
|
941
1191
|
if (part.type === "text") {
|
|
942
|
-
content.push(
|
|
1192
|
+
content.push(textBlock(part.text, part.options))
|
|
943
1193
|
pushCachePoint(content, part)
|
|
944
1194
|
} else if (part.type === "file") {
|
|
1195
|
+
if (part.options.amazonBedrock?.guardContent === true) {
|
|
1196
|
+
content.push({ guardContent: { image: yield* guardedImage(part) } })
|
|
1197
|
+
pushCachePoint(content, part)
|
|
1198
|
+
continue
|
|
1199
|
+
}
|
|
945
1200
|
const imageFormat = imageFormats[part.mediaType]
|
|
946
1201
|
if (Predicate.isNotUndefined(imageFormat)) {
|
|
947
1202
|
content.push({ image: { format: imageFormat, source: yield* fileSource(part.data) } })
|
|
@@ -1037,6 +1292,16 @@ const prepareMessages: (options: LanguageModel.ProviderOptions) => Effect.Effect
|
|
|
1037
1292
|
const isLastPart = k === message.content.length - 1
|
|
1038
1293
|
|
|
1039
1294
|
if (part.type === "text") {
|
|
1295
|
+
if (!Predicate.isNullish(part.options.amazonBedrock?.guardContent)) {
|
|
1296
|
+
return yield* AiError.make({
|
|
1297
|
+
module: "AmazonBedrockLanguageModel",
|
|
1298
|
+
method: "prepareMessages",
|
|
1299
|
+
reason: new AiError.InvalidUserInputError({
|
|
1300
|
+
description: "Unsupported guardContent on an assistant text part - this provider guards only " +
|
|
1301
|
+
"user text parts, user png and jpeg images, and system messages"
|
|
1302
|
+
})
|
|
1303
|
+
})
|
|
1304
|
+
}
|
|
1040
1305
|
// Skip empty text blocks
|
|
1041
1306
|
if (part.text.trim().length === 0) {
|
|
1042
1307
|
continue
|
|
@@ -1124,16 +1389,36 @@ interface ModelCapabilities {
|
|
|
1124
1389
|
}
|
|
1125
1390
|
|
|
1126
1391
|
/**
|
|
1127
|
-
* Returns what a model accepts for structured output,
|
|
1128
|
-
*
|
|
1129
|
-
*
|
|
1392
|
+
* Returns what a model accepts for structured output, looked up by the
|
|
1393
|
+
* provider that the model ID names: `anthropic` in
|
|
1394
|
+
* `eu.anthropic.claude-opus-4-8`, `amazon` in `amazon.nova-pro-v1:0`. A
|
|
1395
|
+
* foundation model or inference profile ARN names its model at the end. Other
|
|
1396
|
+
* ARNs, such as an application inference profile or a provisioned model, do
|
|
1397
|
+
* not. They get the Claude rules, which give them the native structured output
|
|
1398
|
+
* and forced tool of 0.2.0, on the unmeasured assumption that most of them
|
|
1399
|
+
* wrap a Claude model.
|
|
1400
|
+
*
|
|
1401
|
+
* Probes on 2026-10-17, one run per model and call unless noted:
|
|
1130
1402
|
*
|
|
1403
|
+
* - Only Claude gets native structured output. Nova, Mistral Large 2402 and
|
|
1404
|
+
* Pixtral Large in eu-west-1, and Llama, Palmyra, DeepSeek R1, Mistral 7B
|
|
1405
|
+
* and Mixtral in us-east-1, answer `outputConfig` with a 400. gpt-oss-120b
|
|
1406
|
+
* accepts it but wrote text before the JSON in 3 of 4 runs.
|
|
1407
|
+
* - The forced object tool, sent as `{ any: {} }`, returned the object on 15
|
|
1408
|
+
* of 16 models in eu-west-1, Claude Haiku 4.5 and Opus 4.8 included, and
|
|
1409
|
+
* on all 17 models in us-east-1 that accept a forced tool. Magistral Small
|
|
1410
|
+
* fails every object path.
|
|
1411
|
+
* - Llama 3.1, 3.3 and 4 and Palmyra X4 and X5 (`meta`, `writer`) answer
|
|
1412
|
+
* `any` with a 400 in us-east-1, and so does each of them that the probe
|
|
1413
|
+
* sent a named `tool`, all but Llama 3.1 8B. They get the unforced tool. It
|
|
1414
|
+
* returned the object on Llama 3.1 8B, Llama 4 Scout and Maverick and
|
|
1415
|
+
* Palmyra X5, and a wrong object on Llama 3.1 70B and 3.3 70B.
|
|
1131
1416
|
* - Claude models before Opus 4.1 / Sonnet 4.5 predate structured output, as
|
|
1132
1417
|
* in the Anthropic provider. This is not checked on Bedrock: a probe of 17
|
|
1133
|
-
* Claude inference profiles in eu-west-1
|
|
1134
|
-
*
|
|
1135
|
-
* 3
|
|
1136
|
-
* - Every Claude profile that served
|
|
1418
|
+
* Claude inference profiles in eu-west-1 found Claude 3 Haiku, Claude 3
|
|
1419
|
+
* Sonnet and Sonnet 4 at end of life there, and did not try Claude 3 Opus,
|
|
1420
|
+
* 3.5 Haiku, 3.5 Sonnet, 3.7 Sonnet, Opus 4 or Opus 4.1.
|
|
1421
|
+
* - Every Claude profile that served that probe's requests (12 of 17)
|
|
1137
1422
|
* accepted `outputConfig.textFormat`: Haiku 4.5 and 5.5, Opus 4.5 to 4.8, 5
|
|
1138
1423
|
* and 5.5, and Sonnet 4.5, 4.6, 5 and 5.5. Fable 5 and 5.1 rejected every
|
|
1139
1424
|
* request, with or without `textFormat`, with a data-retention
|
|
@@ -1146,23 +1431,51 @@ interface ModelCapabilities {
|
|
|
1146
1431
|
* @see https://platform.claude.com/docs/en/build-with-claude/structured-outputs
|
|
1147
1432
|
*/
|
|
1148
1433
|
const getModelCapabilities = (modelId: string): ModelCapabilities => {
|
|
1149
|
-
const
|
|
1150
|
-
|
|
1151
|
-
modelId
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
)
|
|
1155
|
-
const
|
|
1156
|
-
|
|
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 }
|
|
1434
|
+
const named = modelId.startsWith("arn:") ? namingArn.exec(modelId)?.[1] : modelId
|
|
1435
|
+
if (Predicate.isUndefined(named)) {
|
|
1436
|
+
return claudeCapabilities(modelId)
|
|
1437
|
+
}
|
|
1438
|
+
// `provider.model`, or `geo.provider.model` for an inference profile.
|
|
1439
|
+
const [first = "", second = ""] = named.split(".")
|
|
1440
|
+
const byProvider = capabilitiesByProvider.get(first) ?? capabilitiesByProvider.get(second)
|
|
1441
|
+
return Predicate.isUndefined(byProvider) ? otherProvider : byProvider(named)
|
|
1164
1442
|
}
|
|
1165
1443
|
|
|
1444
|
+
// Of the ARNs Converse accepts, only these two end in the ID of the model
|
|
1445
|
+
// they invoke.
|
|
1446
|
+
const namingArn = /^arn:[^:]+:bedrock:[^:]*:[^:]*:(?:foundation-model|inference-profile)\/(.+)$/
|
|
1447
|
+
|
|
1448
|
+
const otherProvider: ModelCapabilities = { supportsStructuredOutput: false, supportsForcedToolChoice: true }
|
|
1449
|
+
|
|
1450
|
+
const unforcedTool: ModelCapabilities = { supportsStructuredOutput: false, supportsForcedToolChoice: false }
|
|
1451
|
+
|
|
1452
|
+
const claudeWithoutStructuredOutput = [
|
|
1453
|
+
"claude-sonnet-4-0",
|
|
1454
|
+
"claude-sonnet-4-20250514",
|
|
1455
|
+
"claude-3-7-sonnet",
|
|
1456
|
+
"claude-opus-4-0",
|
|
1457
|
+
"claude-opus-4-20250514",
|
|
1458
|
+
"claude-3-"
|
|
1459
|
+
]
|
|
1460
|
+
|
|
1461
|
+
const claudeWithoutForcedToolChoice = [
|
|
1462
|
+
"claude-opus-5-5",
|
|
1463
|
+
"claude-sonnet-5-5",
|
|
1464
|
+
"claude-fable-5-1",
|
|
1465
|
+
"claude-mythos-5-1"
|
|
1466
|
+
]
|
|
1467
|
+
|
|
1468
|
+
const claudeCapabilities = (modelId: string): ModelCapabilities => ({
|
|
1469
|
+
supportsStructuredOutput: !claudeWithoutStructuredOutput.some((id) => modelId.includes(id)),
|
|
1470
|
+
supportsForcedToolChoice: !claudeWithoutForcedToolChoice.some((id) => modelId.includes(id))
|
|
1471
|
+
})
|
|
1472
|
+
|
|
1473
|
+
const capabilitiesByProvider = new Map<string, (modelId: string) => ModelCapabilities>([
|
|
1474
|
+
["anthropic", claudeCapabilities],
|
|
1475
|
+
["meta", () => unforcedTool],
|
|
1476
|
+
["writer", () => unforcedTool]
|
|
1477
|
+
])
|
|
1478
|
+
|
|
1166
1479
|
const tryJsonSchema = (schema: Schema.Top, method: string) =>
|
|
1167
1480
|
Effect.try({
|
|
1168
1481
|
try: () => Tool.getJsonSchemaFromSchema(schema, { transformer: toCodecAnthropic }),
|
|
@@ -1267,7 +1580,7 @@ const makeResponse: (
|
|
|
1267
1580
|
documents: ReadonlyArray<CitedDocument>
|
|
1268
1581
|
) => Effect.Effect<
|
|
1269
1582
|
Array<Response.PartEncoded>,
|
|
1270
|
-
|
|
1583
|
+
AiError.AiError,
|
|
1271
1584
|
IdGenerator.IdGenerator
|
|
1272
1585
|
> = Effect.fnUntraced(function*(request, response, options, nameMapper, documents) {
|
|
1273
1586
|
const idGenerator = yield* IdGenerator.IdGenerator
|
|
@@ -1375,6 +1688,22 @@ const makeResponse: (
|
|
|
1375
1688
|
: InternalUtilities.resolveFinishReason(response.stopReason)
|
|
1376
1689
|
const cacheReadTokens = response.usage.cacheReadInputTokens ?? 0
|
|
1377
1690
|
const cacheWriteTokens = response.usage.cacheWriteInputTokens ?? 0
|
|
1691
|
+
const metadata = InternalGuardrails.finishMetadata(response.stopReason, response.trace)
|
|
1692
|
+
|
|
1693
|
+
if (options.responseFormat.type === "json") {
|
|
1694
|
+
const text = parts.flatMap((part) => part.type === "text" ? [part.text] : []).join("")
|
|
1695
|
+
if (InternalGuardrails.blocksObject(response.stopReason, text)) {
|
|
1696
|
+
return yield* AiError.make({
|
|
1697
|
+
module: "AmazonBedrockLanguageModel",
|
|
1698
|
+
method: "generateObject",
|
|
1699
|
+
reason: new AiError.ContentPolicyError({
|
|
1700
|
+
description: "An Amazon Bedrock guardrail intervened, and the response holds no JSON object. " +
|
|
1701
|
+
`Bedrock answered: ${truncate(text, 200)}`,
|
|
1702
|
+
metadata: { amazonBedrock: metadata }
|
|
1703
|
+
})
|
|
1704
|
+
})
|
|
1705
|
+
}
|
|
1706
|
+
}
|
|
1378
1707
|
|
|
1379
1708
|
parts.push({
|
|
1380
1709
|
type: "finish",
|
|
@@ -1394,14 +1723,15 @@ const makeResponse: (
|
|
|
1394
1723
|
},
|
|
1395
1724
|
// `Response.FinishPart` models `response` with `Schema.UndefinedOr`, which
|
|
1396
1725
|
// requires the key; HTTP response details are not surfaced by the client.
|
|
1397
|
-
response: undefined
|
|
1726
|
+
response: undefined,
|
|
1727
|
+
metadata: { amazonBedrock: metadata }
|
|
1398
1728
|
})
|
|
1399
1729
|
|
|
1400
1730
|
return parts
|
|
1401
1731
|
})
|
|
1402
1732
|
|
|
1403
1733
|
const makeStreamResponse: (
|
|
1404
|
-
request: typeof
|
|
1734
|
+
request: typeof ConverseStreamRequest.Encoded,
|
|
1405
1735
|
stream: Stream.Stream<ConverseResponseStreamEvent, AiError.AiError>,
|
|
1406
1736
|
nameMapper: Tool.NameMapper<ReadonlyArray<Tool.Any>>,
|
|
1407
1737
|
documents: ReadonlyArray<CitedDocument>
|
|
@@ -1444,6 +1774,7 @@ const makeStreamResponse: (
|
|
|
1444
1774
|
// (which AWS sends AFTER messageStop and which carries the usage), so finish
|
|
1445
1775
|
// reports the real token counts rather than the pre-metadata zeros.
|
|
1446
1776
|
let finishReason: Response.FinishReason = "unknown"
|
|
1777
|
+
let stopReason: string | null = null
|
|
1447
1778
|
|
|
1448
1779
|
return stream.pipe(
|
|
1449
1780
|
Stream.mapEffect(Effect.fnUntraced(function*(event) {
|
|
@@ -1591,6 +1922,19 @@ const makeStreamResponse: (
|
|
|
1591
1922
|
const block = toolBlocks.get(index)
|
|
1592
1923
|
if (Predicate.isNotUndefined(block)) {
|
|
1593
1924
|
toolBlocks.delete(index)
|
|
1925
|
+
const params = yield* Effect.try({
|
|
1926
|
+
// A tool taking no arguments streams no deltas at all.
|
|
1927
|
+
try: () => Tool.unsafeSecureJsonParse(block.params.length === 0 ? "{}" : block.params),
|
|
1928
|
+
catch: () =>
|
|
1929
|
+
AiError.make({
|
|
1930
|
+
module: "AmazonBedrockLanguageModel",
|
|
1931
|
+
method: "makeStreamResponse",
|
|
1932
|
+
reason: new AiError.InvalidOutputError({
|
|
1933
|
+
description: `Bedrock streamed input for the \`${block.name}\` tool that is not valid JSON: ` +
|
|
1934
|
+
truncate(block.params, 500)
|
|
1935
|
+
})
|
|
1936
|
+
})
|
|
1937
|
+
})
|
|
1594
1938
|
parts.push({
|
|
1595
1939
|
type: "tool-params-end",
|
|
1596
1940
|
id: block.id
|
|
@@ -1599,8 +1943,7 @@ const makeStreamResponse: (
|
|
|
1599
1943
|
type: "tool-call",
|
|
1600
1944
|
id: block.id,
|
|
1601
1945
|
name: block.name,
|
|
1602
|
-
|
|
1603
|
-
params: Tool.unsafeSecureJsonParse(block.params.length === 0 ? "{}" : block.params),
|
|
1946
|
+
params,
|
|
1604
1947
|
providerExecuted: false
|
|
1605
1948
|
})
|
|
1606
1949
|
}
|
|
@@ -1611,6 +1954,7 @@ const makeStreamResponse: (
|
|
|
1611
1954
|
// Defer the `finish` part to the trailing `metadata` event (usage
|
|
1612
1955
|
// is still zero here — metadata has not arrived yet).
|
|
1613
1956
|
finishReason = InternalUtilities.resolveFinishReason(event.messageStop.stopReason)
|
|
1957
|
+
stopReason = event.messageStop.stopReason
|
|
1614
1958
|
break
|
|
1615
1959
|
}
|
|
1616
1960
|
|
|
@@ -1639,7 +1983,8 @@ const makeStreamResponse: (
|
|
|
1639
1983
|
}
|
|
1640
1984
|
},
|
|
1641
1985
|
// See `makeResponse`: the key is required (`Schema.UndefinedOr`).
|
|
1642
|
-
response: undefined
|
|
1986
|
+
response: undefined,
|
|
1987
|
+
metadata: { amazonBedrock: InternalGuardrails.finishMetadata(stopReason, event.metadata.trace) }
|
|
1643
1988
|
})
|
|
1644
1989
|
break
|
|
1645
1990
|
}
|
|
@@ -1744,3 +2089,6 @@ const trimIfLast = (
|
|
|
1744
2089
|
isLastPart: boolean,
|
|
1745
2090
|
text: string
|
|
1746
2091
|
) => isLastGroup && isLastMessage && isLastPart ? text.trim() : text
|
|
2092
|
+
|
|
2093
|
+
const truncate = (text: string, maxLength: number): string =>
|
|
2094
|
+
text.length > maxLength ? `${text.slice(0, maxLength)}...` : text
|