@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.
@@ -29,13 +29,13 @@ const makeIdentifier = (name: string) => `${prefix}/${name}`
29
29
  export const IntZeroOrGreater = Schema.Int.check(Schema.isGreaterThanOrEqualTo(0))
30
30
 
31
31
  /**
32
- * The reason the model stopped generating a response.
32
+ * The stop reasons that the Smithy model of the Converse API names.
33
33
  *
34
34
  * @stability unstable
35
35
  * @category schemas
36
- * @since 0.1.0
36
+ * @since 0.3.0
37
37
  */
38
- export const StopReason = Schema.Literals([
38
+ export const KnownStopReason = Schema.Literals([
39
39
  "end_turn",
40
40
  "tool_use",
41
41
  "max_tokens",
@@ -47,6 +47,29 @@ export const StopReason = Schema.Literals([
47
47
  "model_context_window_exceeded"
48
48
  ])
49
49
 
50
+ /**
51
+ * The type of {@link KnownStopReason}.
52
+ *
53
+ * @stability unstable
54
+ * @category schemas
55
+ * @since 0.3.0
56
+ */
57
+ export type KnownStopReason = typeof KnownStopReason.Type
58
+
59
+ /**
60
+ * The reason the model stopped generating a response.
61
+ *
62
+ * **Details**
63
+ *
64
+ * Any string decodes, so a reason that Bedrock adds later, or that a model
65
+ * returns outside the Smithy model, does not fail the response.
66
+ *
67
+ * @stability unstable
68
+ * @category schemas
69
+ * @since 0.1.0
70
+ */
71
+ export const StopReason = Schema.Union([KnownStopReason, Schema.StringForLiteralAutocomplete])
72
+
50
73
  /**
51
74
  * The type of {@link StopReason}.
52
75
  *
@@ -354,6 +377,79 @@ export class CitationsContentBlock extends Schema.Class<CitationsContentBlock>(
354
377
  citations: Schema.optional(Schema.Array(Citation))
355
378
  }) {}
356
379
 
380
+ /**
381
+ * The role a guarded text block plays in a contextual grounding check.
382
+ *
383
+ * **Details**
384
+ *
385
+ * `grounding_source` marks the documents an answer must be grounded in,
386
+ * `query` marks the question, and `guard_content` marks text the guardrail
387
+ * evaluates without a grounding role.
388
+ *
389
+ * @stability unstable
390
+ * @category schemas
391
+ * @since 0.4.0
392
+ */
393
+ export const GuardrailConverseContentQualifier = Schema.Literals(["grounding_source", "query", "guard_content"])
394
+
395
+ /**
396
+ * Text that the guardrail evaluates.
397
+ *
398
+ * **Details**
399
+ *
400
+ * `qualifiers` uses `optionalKey` for the same reason as
401
+ * `CachePointBlock.ttl`: the block is built from `Prompt` provider options,
402
+ * whose values must satisfy `Schema.Json`.
403
+ *
404
+ * @stability unstable
405
+ * @category schemas
406
+ * @since 0.4.0
407
+ */
408
+ export class GuardrailConverseTextBlock extends Schema.Class<GuardrailConverseTextBlock>(
409
+ makeIdentifier("GuardrailConverseTextBlock")
410
+ )({
411
+ text: Schema.String,
412
+ qualifiers: Schema.optionalKey(Schema.Array(GuardrailConverseContentQualifier))
413
+ }) {}
414
+
415
+ /**
416
+ * An image that the guardrail evaluates.
417
+ *
418
+ * **Details**
419
+ *
420
+ * The Smithy model allows only png and jpeg, and only `bytes` as the source.
421
+ * `bytes` is a Smithy blob, which the JSON protocol carries as a base64
422
+ * string.
423
+ *
424
+ * @stability unstable
425
+ * @category schemas
426
+ * @since 0.4.0
427
+ */
428
+ export class GuardrailConverseImageBlock extends Schema.Class<GuardrailConverseImageBlock>(
429
+ makeIdentifier("GuardrailConverseImageBlock")
430
+ )({
431
+ format: Schema.Literals(["png", "jpeg"]),
432
+ source: Schema.Struct({ bytes: Schema.String })
433
+ }) {}
434
+
435
+ /**
436
+ * Content that the guardrail evaluates, in a message or in the system prompt.
437
+ *
438
+ * **Details**
439
+ *
440
+ * AWS models `GuardrailConverseContentBlock` as a UNION, so both members are
441
+ * optional here. Once a request holds one `guardContent` block, the guardrail
442
+ * evaluates only the guarded blocks. The model still reads every block.
443
+ *
444
+ * @stability unstable
445
+ * @category schemas
446
+ * @since 0.4.0
447
+ */
448
+ export const GuardrailConverseContentBlock = Schema.Struct({
449
+ text: Schema.optional(GuardrailConverseTextBlock),
450
+ image: Schema.optional(GuardrailConverseImageBlock)
451
+ })
452
+
357
453
  /**
358
454
  * A text content block within a Converse message.
359
455
  *
@@ -378,7 +474,8 @@ export const ContentBlock = Schema.Struct({
378
474
  image: Schema.optional(ImageBlock),
379
475
  document: Schema.optional(DocumentBlock),
380
476
  cachePoint: Schema.optional(CachePointBlock),
381
- citationsContent: Schema.optional(CitationsContentBlock)
477
+ citationsContent: Schema.optional(CitationsContentBlock),
478
+ guardContent: Schema.optional(GuardrailConverseContentBlock)
382
479
  })
383
480
 
384
481
  /**
@@ -398,8 +495,9 @@ export class Message extends Schema.Class<Message>(makeIdentifier("Message"))({
398
495
  *
399
496
  * **Details**
400
497
  *
401
- * AWS models `SystemContentBlock` as a UNION, so both members are optional; a
402
- * block carries either the system text or a cache point.
498
+ * AWS models `SystemContentBlock` as a UNION, so every member is optional; a
499
+ * block carries the system text, guarded system text, or a cache point. The
500
+ * guardrail evaluates the system prompt only where it is guarded.
403
501
  *
404
502
  * @stability unstable
405
503
  * @category schemas
@@ -407,7 +505,8 @@ export class Message extends Schema.Class<Message>(makeIdentifier("Message"))({
407
505
  */
408
506
  export const SystemContentBlock = Schema.Struct({
409
507
  text: Schema.optional(Schema.String),
410
- cachePoint: Schema.optional(CachePointBlock)
508
+ cachePoint: Schema.optional(CachePointBlock),
509
+ guardContent: Schema.optional(GuardrailConverseContentBlock)
411
510
  })
412
511
 
413
512
  /**
@@ -555,7 +654,80 @@ export const OutputConfig = Schema.Struct({
555
654
  })
556
655
 
557
656
  /**
558
- * The request payload for the Converse and ConverseStream operations.
657
+ * Whether Bedrock returns the guardrail trace.
658
+ *
659
+ * **Details**
660
+ *
661
+ * Bedrock's default is `disabled`. The trace holds the text that a policy
662
+ * matched, unmasked. `enabled_full` is in the Smithy model, and AWS does not
663
+ * document it.
664
+ *
665
+ * @stability unstable
666
+ * @category schemas
667
+ * @since 0.4.0
668
+ */
669
+ export const GuardrailTrace = Schema.Literals(["enabled", "disabled", "enabled_full"])
670
+
671
+ /**
672
+ * How ConverseStream applies a guardrail to the streamed output.
673
+ *
674
+ * **Details**
675
+ *
676
+ * `sync`, Bedrock's default, holds back each chunk until the guardrail has
677
+ * evaluated it. `async` sends chunks at once, so text that the guardrail
678
+ * later blocks can reach the caller, and masking does not apply.
679
+ *
680
+ * @stability unstable
681
+ * @category schemas
682
+ * @since 0.4.0
683
+ */
684
+ export const GuardrailStreamProcessingMode = Schema.Literals(["sync", "async"])
685
+
686
+ // Smithy gives the identifier and the version `default: ""`, and Bedrock cannot
687
+ // apply a guardrail without them, so both are required here.
688
+ const guardrailFields = {
689
+ guardrailIdentifier: Schema.String,
690
+ guardrailVersion: Schema.String,
691
+ trace: Schema.optionalKey(GuardrailTrace)
692
+ }
693
+
694
+ /**
695
+ * The guardrail of a Converse request.
696
+ *
697
+ * **Details**
698
+ *
699
+ * `guardrailIdentifier` is the guardrail's ID or ARN, and `guardrailVersion`
700
+ * is a version number or `DRAFT`. Converse has no `streamProcessingMode`, so
701
+ * the member is typed `never`: a ConverseStream request does not fit Converse
702
+ * until the mode is removed.
703
+ *
704
+ * @stability unstable
705
+ * @category schemas
706
+ * @since 0.4.0
707
+ */
708
+ export class GuardrailConfiguration extends Schema.Class<GuardrailConfiguration>(
709
+ makeIdentifier("GuardrailConfiguration")
710
+ )({
711
+ ...guardrailFields,
712
+ streamProcessingMode: Schema.optionalKey(Schema.Never)
713
+ }) {}
714
+
715
+ /**
716
+ * The guardrail of a ConverseStream request.
717
+ *
718
+ * @stability unstable
719
+ * @category schemas
720
+ * @since 0.4.0
721
+ */
722
+ export class GuardrailStreamConfiguration extends Schema.Class<GuardrailStreamConfiguration>(
723
+ makeIdentifier("GuardrailStreamConfiguration")
724
+ )({
725
+ ...guardrailFields,
726
+ streamProcessingMode: Schema.optionalKey(GuardrailStreamProcessingMode)
727
+ }) {}
728
+
729
+ /**
730
+ * The request payload for the Converse operation.
559
731
  *
560
732
  * **Details**
561
733
  *
@@ -579,9 +751,30 @@ export class ConverseRequest extends Schema.Class<ConverseRequest>(makeIdentifie
579
751
  toolConfig: Schema.optionalKey(ToolConfiguration),
580
752
  inferenceConfig: Schema.optional(InferenceConfiguration),
581
753
  outputConfig: Schema.optionalKey(OutputConfig),
582
- additionalModelRequestFields: Schema.optional(Schema.Record(Schema.String, Schema.Unknown))
754
+ additionalModelRequestFields: Schema.optional(Schema.Record(Schema.String, Schema.Unknown)),
755
+ guardrailConfig: Schema.optional(GuardrailConfiguration)
583
756
  }) {}
584
757
 
758
+ /**
759
+ * The request payload for the ConverseStream operation.
760
+ *
761
+ * **Details**
762
+ *
763
+ * The members of `ConverseRequest`, with the guardrail of a stream, which
764
+ * adds `streamProcessingMode`. That member is the only difference between the
765
+ * two operations.
766
+ *
767
+ * @stability unstable
768
+ * @category schemas
769
+ * @since 0.4.0
770
+ */
771
+ export class ConverseStreamRequest
772
+ extends Schema.Class<ConverseStreamRequest>(makeIdentifier("ConverseStreamRequest"))({
773
+ ...ConverseRequest.fields,
774
+ guardrailConfig: Schema.optional(GuardrailStreamConfiguration)
775
+ })
776
+ {}
777
+
585
778
  // =============================================================================
586
779
  // Response
587
780
  // =============================================================================
@@ -611,6 +804,12 @@ export class ConverseMetrics extends Schema.Class<ConverseMetrics>(makeIdentifie
611
804
  /**
612
805
  * The response from a successful Converse call.
613
806
  *
807
+ * **Details**
808
+ *
809
+ * `trace` is kept as the JSON Bedrock sent. Its Smithy tree has about 90
810
+ * shapes that AWS still extends, and a struct would drop every member added
811
+ * later.
812
+ *
614
813
  * @stability unstable
615
814
  * @category schemas
616
815
  * @since 0.1.0
@@ -619,7 +818,8 @@ export class ConverseResponse extends Schema.Class<ConverseResponse>(makeIdentif
619
818
  output: ConverseOutput,
620
819
  usage: TokenUsage,
621
820
  stopReason: StopReason,
622
- metrics: Schema.optional(ConverseMetrics)
821
+ metrics: Schema.optional(ConverseMetrics),
822
+ trace: Schema.optional(Schema.MutableJson)
623
823
  }) {}
624
824
 
625
825
  // =============================================================================
@@ -800,7 +1000,13 @@ export class MessageStopEvent extends Schema.Class<MessageStopEvent>(
800
1000
  }) {}
801
1001
 
802
1002
  /**
803
- * The trailing metadata event for a Converse stream, carrying usage and metrics.
1003
+ * The trailing metadata event for a Converse stream, carrying usage, metrics,
1004
+ * and the trace.
1005
+ *
1006
+ * **Details**
1007
+ *
1008
+ * This is the only stream event that carries the trace. It is kept as JSON,
1009
+ * as in `ConverseResponse`.
804
1010
  *
805
1011
  * @stability unstable
806
1012
  * @category schemas
@@ -810,7 +1016,8 @@ export class ConverseStreamMetadataEvent extends Schema.Class<ConverseStreamMeta
810
1016
  makeIdentifier("ConverseStreamMetadataEvent")
811
1017
  )({
812
1018
  usage: TokenUsage,
813
- metrics: Schema.optional(ConverseMetrics)
1019
+ metrics: Schema.optional(ConverseMetrics),
1020
+ trace: Schema.optional(Schema.MutableJson)
814
1021
  }) {}
815
1022
 
816
1023
  const ExceptionBody = Schema.Struct({
@@ -220,9 +220,23 @@ const buildInvalidRequestDescription = (params: {
220
220
  parts.push(`Response: ${truncated}`)
221
221
  }
222
222
 
223
+ if (params.message && dataRetentionRejection.test(params.message)) {
224
+ parts.push(dataRetentionHint)
225
+ }
226
+
223
227
  return parts.join(" ")
224
228
  }
225
229
 
230
+ // Converse has no request field for the data retention mode, so the AWS message
231
+ // alone leaves the reader looking for one. Only an account-level setting in each
232
+ // region, which the control plane sets, makes such a model callable.
233
+ const dataRetentionRejection = /data retention mode '[^']*' is not available for this model/
234
+
235
+ const dataRetentionHint = "The account's data retention mode in this region does not allow this model." +
236
+ " Claude Fable 5 and 5.1 need `aws_review`." +
237
+ " Set the mode with the Bedrock PutAccountDataRetention operation:" +
238
+ " https://docs.aws.amazon.com/bedrock/latest/userguide/data-retention.html"
239
+
226
240
  /**
227
241
  * AWS sends the exception shape as `Name:http://internal.amazon.com/coral/...`.
228
242
  * Only the part before the colon identifies it.
@@ -0,0 +1,91 @@
1
+ import type * as Schema from "effect/Schema"
2
+ import type { FinishMetadata, GuardrailAction, GuardrailOutcome } from "../AmazonBedrockLanguageModel.ts"
3
+
4
+ const strength: Record<GuardrailAction, number> = { none: 0, masked: 1, blocked: 2 }
5
+
6
+ const stronger = (a: GuardrailAction, b: GuardrailAction): GuardrailAction => strength[b] > strength[a] ? b : a
7
+
8
+ const isObject = (value: unknown): value is Schema.MutableJsonObject =>
9
+ typeof value === "object" && value !== null && !Array.isArray(value)
10
+
11
+ const actionOf = (value: string): GuardrailAction =>
12
+ value === "BLOCKED" ? "blocked" : value === "ANONYMIZED" ? "masked" : "none"
13
+
14
+ const strongestAction = (value: Schema.MutableJson): GuardrailAction => {
15
+ let action: GuardrailAction = "none"
16
+ if (Array.isArray(value)) {
17
+ for (const item of value) {
18
+ action = stronger(action, strongestAction(item))
19
+ }
20
+ } else if (isObject(value)) {
21
+ for (const [key, member] of Object.entries(value)) {
22
+ action = stronger(
23
+ action,
24
+ key === "action" && typeof member === "string" ? actionOf(member) : strongestAction(member)
25
+ )
26
+ }
27
+ }
28
+ return action
29
+ }
30
+
31
+ const assessSide = (assessments: ReadonlyArray<Schema.MutableJson>, policies: Array<string>): GuardrailAction => {
32
+ let side: GuardrailAction = "none"
33
+ for (const assessment of assessments) {
34
+ if (!isObject(assessment)) {
35
+ continue
36
+ }
37
+ for (const [policy, member] of Object.entries(assessment)) {
38
+ const action = strongestAction(member)
39
+ if (action !== "none" && !policies.includes(policy)) {
40
+ policies.push(policy)
41
+ }
42
+ side = stronger(side, action)
43
+ }
44
+ }
45
+ return side
46
+ }
47
+
48
+ /** @internal */
49
+ export const guardrailOutcome = (trace: Schema.MutableJson | undefined): GuardrailOutcome | null => {
50
+ const guardrail = isObject(trace) ? trace.guardrail : undefined
51
+ if (!isObject(guardrail)) {
52
+ return null
53
+ }
54
+ const inputAssessment = guardrail.inputAssessment
55
+ const outputAssessments = guardrail.outputAssessments
56
+ const policies: Array<string> = []
57
+ const input = assessSide(isObject(inputAssessment) ? Object.values(inputAssessment) : [], policies)
58
+ const output = assessSide(
59
+ isObject(outputAssessments)
60
+ ? Object.values(outputAssessments).flatMap((chunks) => Array.isArray(chunks) ? chunks : [])
61
+ : [],
62
+ policies
63
+ )
64
+ return { input, output, policies }
65
+ }
66
+
67
+ /** @internal */
68
+ export const finishMetadata = (stopReason: string | null, trace: Schema.MutableJson | undefined): FinishMetadata => ({
69
+ stopReason,
70
+ guardrail: guardrailOutcome(trace),
71
+ trace: trace ?? null
72
+ })
73
+
74
+ const isJson = (text: string): boolean => {
75
+ try {
76
+ JSON.parse(text)
77
+ return true
78
+ } catch {
79
+ return false
80
+ }
81
+ }
82
+
83
+ /**
84
+ * Bedrock stops with `guardrail_intervened` both when a guardrail blocks,
85
+ * replacing the text with the guardrail's blocked message, and when it masks,
86
+ * leaving the object JSON.
87
+ *
88
+ * @internal
89
+ */
90
+ export const blocksObject = (stopReason: string, text: string): boolean =>
91
+ stopReason === "guardrail_intervened" && !isJson(text)
@@ -1,8 +1,10 @@
1
1
  import type * as Response from "effect/ai/Response"
2
- import * as Predicate from "effect/Predicate"
3
- import type { StopReason } from "../AmazonBedrockSchema.ts"
2
+ import * as Schema from "effect/Schema"
3
+ import { KnownStopReason, type StopReason } from "../AmazonBedrockSchema.ts"
4
4
 
5
- const finishReasonMap: Record<StopReason, Response.FinishReason> = {
5
+ const isKnownStopReason = Schema.is(KnownStopReason)
6
+
7
+ const finishReasonMap: Record<KnownStopReason, Response.FinishReason> = {
6
8
  content_filtered: "content-filter",
7
9
  end_turn: "stop",
8
10
  guardrail_intervened: "content-filter",
@@ -15,7 +17,5 @@ const finishReasonMap: Record<StopReason, Response.FinishReason> = {
15
17
  }
16
18
 
17
19
  /** @internal */
18
- export const resolveFinishReason = (stopReason: StopReason): Response.FinishReason => {
19
- const reason = finishReasonMap[stopReason]
20
- return Predicate.isUndefined(reason) ? "unknown" : reason
21
- }
20
+ export const resolveFinishReason = (stopReason: StopReason): Response.FinishReason =>
21
+ isKnownStopReason(stopReason) ? finishReasonMap[stopReason] : "other"