@wmaurer/effect-ai-amazon-bedrock 0.3.0 → 0.4.1
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 +99 -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 +223 -25
- package/dist/AmazonBedrockLanguageModel.js.map +1 -1
- package/dist/AmazonBedrockSchema.d.ts +216 -4
- package/dist/AmazonBedrockSchema.d.ts.map +1 -1
- package/dist/AmazonBedrockSchema.js +175 -9
- package/dist/AmazonBedrockSchema.js.map +1 -1
- package/dist/internal/errors.d.ts.map +1 -1
- package/dist/internal/errors.js +27 -18
- 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/package.json +1 -1
- package/src/AmazonBedrockClient.ts +2 -2
- package/src/AmazonBedrockLanguageModel.ts +409 -46
- package/src/AmazonBedrockSchema.ts +193 -9
- package/src/internal/errors.ts +35 -19
- package/src/internal/guardrails.ts +91 -0
|
@@ -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
|
|
@@ -32,6 +36,7 @@ import * as LanguageModel from "effect/ai/LanguageModel"
|
|
|
32
36
|
import * as AiModel from "effect/ai/Model"
|
|
33
37
|
import type * as Prompt from "effect/ai/Prompt"
|
|
34
38
|
import type * as Response from "effect/ai/Response"
|
|
39
|
+
import { addGenAIAnnotations } from "effect/ai/Telemetry"
|
|
35
40
|
import * as Tool from "effect/ai/Tool"
|
|
36
41
|
import * as Context from "effect/Context"
|
|
37
42
|
import * as DateTime from "effect/DateTime"
|
|
@@ -41,9 +46,10 @@ import { dual } from "effect/Function"
|
|
|
41
46
|
import * as Layer from "effect/Layer"
|
|
42
47
|
import * as Predicate from "effect/Predicate"
|
|
43
48
|
import * as Result from "effect/Result"
|
|
44
|
-
import
|
|
49
|
+
import * as Schema from "effect/Schema"
|
|
45
50
|
import * as SchemaAST from "effect/SchemaAST"
|
|
46
51
|
import * as Stream from "effect/Stream"
|
|
52
|
+
import type { Span } from "effect/Tracer"
|
|
47
53
|
import type { Mutable, Simplify } from "effect/Types"
|
|
48
54
|
import { AmazonBedrockClient } from "./AmazonBedrockClient.ts"
|
|
49
55
|
import type {
|
|
@@ -54,17 +60,24 @@ import type {
|
|
|
54
60
|
ConverseRequest,
|
|
55
61
|
ConverseResponse,
|
|
56
62
|
ConverseResponseStreamEvent,
|
|
63
|
+
ConverseStreamRequest,
|
|
57
64
|
DocumentBlock,
|
|
58
65
|
DocumentSource,
|
|
66
|
+
GuardrailConverseContentBlock,
|
|
67
|
+
GuardrailConverseContentQualifier,
|
|
68
|
+
GuardrailConverseImageBlock,
|
|
69
|
+
GuardrailStreamConfiguration,
|
|
59
70
|
ImageBlock,
|
|
60
71
|
MediaSource,
|
|
61
72
|
Message,
|
|
62
73
|
OutputConfig,
|
|
63
74
|
SystemContentBlock,
|
|
75
|
+
TokenUsage,
|
|
64
76
|
Tool as BedrockTool,
|
|
65
77
|
ToolChoice,
|
|
66
78
|
ToolConfiguration
|
|
67
79
|
} from "./AmazonBedrockSchema.ts"
|
|
80
|
+
import * as InternalGuardrails from "./internal/guardrails.ts"
|
|
68
81
|
import * as InternalUtilities from "./internal/utilities.ts"
|
|
69
82
|
|
|
70
83
|
/**
|
|
@@ -149,15 +162,6 @@ interface CachePointOptions {
|
|
|
149
162
|
}
|
|
150
163
|
|
|
151
164
|
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
165
|
/**
|
|
162
166
|
* Amazon Bedrock options for user messages.
|
|
163
167
|
*
|
|
@@ -185,15 +189,6 @@ declare module "effect/ai/Prompt" {
|
|
|
185
189
|
*/
|
|
186
190
|
export interface ToolMessageOptions extends CachePointOptions {}
|
|
187
191
|
|
|
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
192
|
/**
|
|
198
193
|
* Amazon Bedrock options for tool call prompt parts.
|
|
199
194
|
*
|
|
@@ -274,6 +269,13 @@ interface FileOptions {
|
|
|
274
269
|
* Converse cannot cite.
|
|
275
270
|
*/
|
|
276
271
|
readonly citations?: Citations | null
|
|
272
|
+
/**
|
|
273
|
+
* Sends the image as a `guardContent` image block, for the guardrail to
|
|
274
|
+
* evaluate. Only an `image/png` or `image/jpeg` part whose data is bytes
|
|
275
|
+
* or base64 can be guarded. Any other file part with this option fails
|
|
276
|
+
* the request with `InvalidUserInputError`.
|
|
277
|
+
*/
|
|
278
|
+
readonly guardContent?: true | null
|
|
277
279
|
} | null
|
|
278
280
|
}
|
|
279
281
|
|
|
@@ -489,6 +491,190 @@ const reasoningTextInfo = (signature: string | undefined): ReasoningInfo => ({
|
|
|
489
491
|
signature: signature ?? null
|
|
490
492
|
})
|
|
491
493
|
|
|
494
|
+
/**
|
|
495
|
+
* A guardrail to apply to every request, set as `Config.guardrailConfig`.
|
|
496
|
+
*
|
|
497
|
+
* **Details**
|
|
498
|
+
*
|
|
499
|
+
* `generateText` and `generateObject` send it to Converse without
|
|
500
|
+
* `streamProcessingMode`, which Converse does not have. `streamText` sends it
|
|
501
|
+
* to ConverseStream as given.
|
|
502
|
+
*
|
|
503
|
+
* Without `guardContent` on any part, the guardrail evaluates every message
|
|
504
|
+
* and not the system prompt. Set `trace` to `"enabled"` to learn from the
|
|
505
|
+
* finish part whether the guardrail blocked or masked. The trace holds the
|
|
506
|
+
* matched text unmasked.
|
|
507
|
+
*
|
|
508
|
+
* @stability unstable
|
|
509
|
+
* @category models
|
|
510
|
+
* @since 0.4.0
|
|
511
|
+
*/
|
|
512
|
+
export type GuardrailConfig = typeof GuardrailStreamConfiguration.Encoded
|
|
513
|
+
|
|
514
|
+
/**
|
|
515
|
+
* The role a guarded text part plays in a contextual grounding check.
|
|
516
|
+
*
|
|
517
|
+
* @stability unstable
|
|
518
|
+
* @category models
|
|
519
|
+
* @since 0.4.0
|
|
520
|
+
*/
|
|
521
|
+
export type GuardContentQualifier = typeof GuardrailConverseContentQualifier.Type
|
|
522
|
+
|
|
523
|
+
/**
|
|
524
|
+
* Marks a text part or a system message for the guardrail to evaluate.
|
|
525
|
+
*
|
|
526
|
+
* **Details**
|
|
527
|
+
*
|
|
528
|
+
* `true` guards the text. `qualifiers` names its role in a contextual
|
|
529
|
+
* grounding check: the source documents are `grounding_source`, and the
|
|
530
|
+
* question is `query`. Once any part of a request is guarded, the guardrail
|
|
531
|
+
* evaluates only the guarded parts. The model still reads every part.
|
|
532
|
+
*
|
|
533
|
+
* The option stays on the part, so in a chat a guarded turn is evaluated
|
|
534
|
+
* again on every later turn.
|
|
535
|
+
*
|
|
536
|
+
* @stability unstable
|
|
537
|
+
* @category models
|
|
538
|
+
* @since 0.4.0
|
|
539
|
+
*/
|
|
540
|
+
export type GuardContent = true | {
|
|
541
|
+
readonly qualifiers: ReadonlyArray<GuardContentQualifier>
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
interface GuardableOptions {
|
|
545
|
+
readonly amazonBedrock?: {
|
|
546
|
+
/**
|
|
547
|
+
* Marks the end of the reusable prefix of the request. The cache point is
|
|
548
|
+
* emitted after the content this is attached to.
|
|
549
|
+
*/
|
|
550
|
+
readonly cachePoint?: CachePoint | null
|
|
551
|
+
/**
|
|
552
|
+
* Sends the text as a `guardContent` block, for the guardrail to evaluate.
|
|
553
|
+
* Honoured on user text parts and system messages. On an assistant text
|
|
554
|
+
* part it fails the request with `InvalidUserInputError`.
|
|
555
|
+
*/
|
|
556
|
+
readonly guardContent?: GuardContent | null
|
|
557
|
+
} | null
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
declare module "effect/ai/Prompt" {
|
|
561
|
+
/**
|
|
562
|
+
* Amazon Bedrock options for system messages.
|
|
563
|
+
*
|
|
564
|
+
* @stability unstable
|
|
565
|
+
* @category models
|
|
566
|
+
* @since 0.1.0
|
|
567
|
+
*/
|
|
568
|
+
export interface SystemMessageOptions extends GuardableOptions {}
|
|
569
|
+
|
|
570
|
+
/**
|
|
571
|
+
* Amazon Bedrock options for text prompt parts.
|
|
572
|
+
*
|
|
573
|
+
* @stability unstable
|
|
574
|
+
* @category models
|
|
575
|
+
* @since 0.1.0
|
|
576
|
+
*/
|
|
577
|
+
export interface TextPartOptions extends GuardableOptions {}
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
const textBlock = (
|
|
581
|
+
text: string,
|
|
582
|
+
options: GuardableOptions
|
|
583
|
+
): { readonly text: string } | { readonly guardContent: typeof GuardrailConverseContentBlock.Encoded } => {
|
|
584
|
+
const guard = options.amazonBedrock?.guardContent
|
|
585
|
+
if (Predicate.isNullish(guard)) {
|
|
586
|
+
return { text }
|
|
587
|
+
}
|
|
588
|
+
return { guardContent: { text: guard === true ? { text } : { text, qualifiers: guard.qualifiers } } }
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
/**
|
|
592
|
+
* What a guardrail did to one side of the exchange.
|
|
593
|
+
*
|
|
594
|
+
* @stability unstable
|
|
595
|
+
* @category models
|
|
596
|
+
* @since 0.4.0
|
|
597
|
+
*/
|
|
598
|
+
export type GuardrailAction = "blocked" | "masked" | "none"
|
|
599
|
+
|
|
600
|
+
/**
|
|
601
|
+
* What the guardrails did, read from the trace.
|
|
602
|
+
*
|
|
603
|
+
* **Details**
|
|
604
|
+
*
|
|
605
|
+
* `input` and `output` are the strongest action that any guardrail took on
|
|
606
|
+
* that side, `blocked` over `masked` over `none`, across every guardrail in
|
|
607
|
+
* the trace, enforced guardrails included. `policies` lists the trace keys of
|
|
608
|
+
* the policies that blocked or masked, such as `wordPolicy` or
|
|
609
|
+
* `sensitiveInformationPolicy`, each once, in the order the trace names them.
|
|
610
|
+
*
|
|
611
|
+
* @stability unstable
|
|
612
|
+
* @category models
|
|
613
|
+
* @since 0.4.0
|
|
614
|
+
*/
|
|
615
|
+
export type GuardrailOutcome = {
|
|
616
|
+
readonly input: GuardrailAction
|
|
617
|
+
readonly output: GuardrailAction
|
|
618
|
+
// Mutable, because the same type rides on ContentPolicyError, whose
|
|
619
|
+
// metadata must be MutableJson.
|
|
620
|
+
readonly policies: Array<string>
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
/**
|
|
624
|
+
* The Amazon Bedrock metadata on every finish part.
|
|
625
|
+
*
|
|
626
|
+
* **Details**
|
|
627
|
+
*
|
|
628
|
+
* `stopReason` is the raw Converse stop reason. It tells
|
|
629
|
+
* `guardrail_intervened` from `content_filtered`, which both finish with
|
|
630
|
+
* `"content-filter"`, and `max_tokens` from `model_context_window_exceeded`,
|
|
631
|
+
* which both finish with `"length"`. It is `null` only when a stream ended
|
|
632
|
+
* without a `messageStop` event.
|
|
633
|
+
*
|
|
634
|
+
* `guardrail` is `null` when the response carried no guardrail trace, which
|
|
635
|
+
* is the case unless `trace` is enabled in the guardrail config. A trace in
|
|
636
|
+
* which nothing acted gives `{ input: "none", output: "none", policies: [] }`.
|
|
637
|
+
* `trace` is the Converse trace as Bedrock sent it, or `null`. It holds the
|
|
638
|
+
* text that a policy matched, unmasked.
|
|
639
|
+
*
|
|
640
|
+
* @stability unstable
|
|
641
|
+
* @category models
|
|
642
|
+
* @since 0.4.0
|
|
643
|
+
*/
|
|
644
|
+
export type FinishMetadata = {
|
|
645
|
+
readonly stopReason: string | null
|
|
646
|
+
readonly guardrail: GuardrailOutcome | null
|
|
647
|
+
readonly trace: Schema.MutableJson | null
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
declare module "effect/ai/AiError" {
|
|
651
|
+
/**
|
|
652
|
+
* Amazon Bedrock metadata on the `ContentPolicyError` that `generateObject`
|
|
653
|
+
* fails with when a guardrail blocked the exchange. It is the metadata of
|
|
654
|
+
* the response's finish part.
|
|
655
|
+
*
|
|
656
|
+
* @stability unstable
|
|
657
|
+
* @category models
|
|
658
|
+
* @since 0.4.0
|
|
659
|
+
*/
|
|
660
|
+
export interface ContentPolicyErrorMetadata extends ProviderMetadata {
|
|
661
|
+
readonly amazonBedrock?: FinishMetadata | null
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
declare module "effect/ai/Response" {
|
|
666
|
+
/**
|
|
667
|
+
* Amazon Bedrock metadata on the finish part.
|
|
668
|
+
*
|
|
669
|
+
* @stability unstable
|
|
670
|
+
* @category models
|
|
671
|
+
* @since 0.4.0
|
|
672
|
+
*/
|
|
673
|
+
export interface FinishPartMetadata extends ProviderMetadata {
|
|
674
|
+
readonly amazonBedrock?: FinishMetadata | null
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
|
|
492
678
|
// =============================================================================
|
|
493
679
|
// Configuration
|
|
494
680
|
// =============================================================================
|
|
@@ -498,12 +684,17 @@ const reasoningTextInfo = (signature: string | undefined): ReasoningInfo => ({
|
|
|
498
684
|
*
|
|
499
685
|
* **Details**
|
|
500
686
|
*
|
|
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`.
|
|
687
|
+
* Derived from `ConverseStreamRequest`, minus the fields built from the
|
|
688
|
+
* prompt, so every Converse request parameter is settable per request. That
|
|
689
|
+
* includes `additionalModelRequestFields`, which carries inference parameters
|
|
690
|
+
* specific to the target model — among them extended thinking on Anthropic
|
|
691
|
+
* models. The thinking budget is drawn from the same ceiling as the visible
|
|
692
|
+
* answer, so `inferenceConfig.maxTokens` must exceed `budget_tokens`.
|
|
693
|
+
*
|
|
694
|
+
* It also includes `guardrailConfig` (see {@link GuardrailConfig}). An
|
|
695
|
+
* override replaces the whole object, and `guardrailConfig: undefined` turns
|
|
696
|
+
* off a layer's guardrail for one request. `generateText` does not send
|
|
697
|
+
* `streamProcessingMode`.
|
|
507
698
|
*
|
|
508
699
|
* **Example** (Enabling extended thinking for one request)
|
|
509
700
|
*
|
|
@@ -532,7 +723,7 @@ export class Config extends Context.Service<
|
|
|
532
723
|
Simplify<
|
|
533
724
|
& Partial<
|
|
534
725
|
Omit<
|
|
535
|
-
typeof
|
|
726
|
+
typeof ConverseStreamRequest.Encoded,
|
|
536
727
|
"messages" | "system" | "outputConfig"
|
|
537
728
|
>
|
|
538
729
|
>
|
|
@@ -635,7 +826,7 @@ export const make = Effect.fnUntraced(function*({ config: providerConfig, model
|
|
|
635
826
|
function*(
|
|
636
827
|
options: LanguageModel.ProviderOptions
|
|
637
828
|
): Effect.fn.Return<{
|
|
638
|
-
readonly request: typeof
|
|
829
|
+
readonly request: typeof ConverseStreamRequest.Encoded
|
|
639
830
|
readonly nameMapper: Tool.NameMapper<ReadonlyArray<Tool.Any>>
|
|
640
831
|
readonly documents: ReadonlyArray<CitedDocument>
|
|
641
832
|
}, AiError.AiError> {
|
|
@@ -708,7 +899,7 @@ export const make = Effect.fnUntraced(function*({ config: providerConfig, model
|
|
|
708
899
|
}
|
|
709
900
|
}
|
|
710
901
|
|
|
711
|
-
const request: typeof
|
|
902
|
+
const request: typeof ConverseStreamRequest.Encoded = {
|
|
712
903
|
...config,
|
|
713
904
|
modelId,
|
|
714
905
|
system,
|
|
@@ -736,18 +927,35 @@ export const make = Effect.fnUntraced(function*({ config: providerConfig, model
|
|
|
736
927
|
return yield* LanguageModel.make({
|
|
737
928
|
codecTransformer: toCodecAnthropic,
|
|
738
929
|
generateText: Effect.fnUntraced(function*(options) {
|
|
739
|
-
const { documents, nameMapper, request } = yield* makeRequest(options)
|
|
930
|
+
const { documents, nameMapper, request: streamRequest } = yield* makeRequest(options)
|
|
931
|
+
const request = toConverseRequest(streamRequest)
|
|
932
|
+
annotateRequest(options.span, request)
|
|
740
933
|
const rawResponse = yield* client.converse({ payload: request })
|
|
934
|
+
// Before `makeResponse`, which fails a guardrail-blocked object with a
|
|
935
|
+
// `ContentPolicyError` after the call has already spent its tokens.
|
|
936
|
+
annotateResponse(options.span, request.modelId, rawResponse.stopReason, rawResponse.usage)
|
|
741
937
|
return yield* makeResponse(request, rawResponse, options, nameMapper, documents)
|
|
742
938
|
}),
|
|
743
939
|
streamText: Effect.fnUntraced(function*(options) {
|
|
744
940
|
const { documents, nameMapper, request } = yield* makeRequest(options)
|
|
941
|
+
annotateRequest(options.span, request)
|
|
745
942
|
const stream = client.converseStream({ payload: request })
|
|
746
|
-
return yield* makeStreamResponse(request, stream, nameMapper, documents)
|
|
943
|
+
return yield* makeStreamResponse(request, stream, options.tools, nameMapper, documents, options.span)
|
|
747
944
|
}, (effect, _options) => effect.pipe(Stream.unwrap))
|
|
748
945
|
})
|
|
749
946
|
})
|
|
750
947
|
|
|
948
|
+
const toConverseRequest = (
|
|
949
|
+
request: typeof ConverseStreamRequest.Encoded
|
|
950
|
+
): typeof ConverseRequest.Encoded => {
|
|
951
|
+
const { guardrailConfig, ...rest } = request
|
|
952
|
+
if (Predicate.isUndefined(guardrailConfig)) {
|
|
953
|
+
return rest
|
|
954
|
+
}
|
|
955
|
+
const { streamProcessingMode: _, ...converseGuardrail } = guardrailConfig
|
|
956
|
+
return { ...rest, guardrailConfig: converseGuardrail }
|
|
957
|
+
}
|
|
958
|
+
|
|
751
959
|
/**
|
|
752
960
|
* Creates a layer for the Amazon Bedrock language model.
|
|
753
961
|
*
|
|
@@ -818,18 +1026,40 @@ const documentFormats: Record<string, typeof DocumentBlock.Encoded["format"]> =
|
|
|
818
1026
|
/** Matches any absolute url, so a non-s3 one is rejected instead of being treated as base64 data. */
|
|
819
1027
|
const urlPattern = /^[a-z][a-z0-9+.-]*:\/\//i
|
|
820
1028
|
|
|
1029
|
+
/** Matches the header of a `data:` url, up to the comma before its payload. */
|
|
1030
|
+
const dataUrlPattern = /^data:[^,]*?(;base64)?,/i
|
|
1031
|
+
|
|
821
1032
|
/**
|
|
822
1033
|
* Resolves file part data into a Converse media source.
|
|
823
1034
|
*
|
|
824
1035
|
* **Details**
|
|
825
1036
|
*
|
|
826
1037
|
* String data is already base64 per `Prompt.FilePart`, so it is passed through
|
|
827
|
-
* untouched.
|
|
1038
|
+
* untouched. A `data:` url has no `://`, so it is matched on its own, and only
|
|
1039
|
+
* its base64 payload is sent. The only remote source Converse accepts is an S3
|
|
1040
|
+
* location.
|
|
828
1041
|
*/
|
|
829
1042
|
const fileSource: (
|
|
830
1043
|
data: typeof Prompt.FilePart.Type["data"]
|
|
831
1044
|
) => Effect.Effect<typeof MediaSource.Encoded, AiError.AiError> = Effect.fnUntraced(function*(data) {
|
|
832
|
-
if (data instanceof
|
|
1045
|
+
if (data instanceof Uint8Array) {
|
|
1046
|
+
return { bytes: Base64.encode(data) }
|
|
1047
|
+
}
|
|
1048
|
+
const string = data instanceof URL ? data.href : data
|
|
1049
|
+
const dataUrl = dataUrlPattern.exec(string)
|
|
1050
|
+
if (dataUrl !== null) {
|
|
1051
|
+
if (Predicate.isUndefined(dataUrl[1])) {
|
|
1052
|
+
return yield* AiError.make({
|
|
1053
|
+
module: "AmazonBedrockLanguageModel",
|
|
1054
|
+
method: "prepareMessages",
|
|
1055
|
+
reason: new AiError.InvalidUserInputError({
|
|
1056
|
+
description: `Unsupported file url '${dataUrl[0]}...' - a data url must have a base64 payload`
|
|
1057
|
+
})
|
|
1058
|
+
})
|
|
1059
|
+
}
|
|
1060
|
+
return { bytes: string.slice(dataUrl[0].length) }
|
|
1061
|
+
}
|
|
1062
|
+
if (data instanceof URL || urlPattern.test(data)) {
|
|
833
1063
|
const url = data instanceof URL ? data : new URL(data)
|
|
834
1064
|
if (url.protocol !== "s3:") {
|
|
835
1065
|
return yield* AiError.make({
|
|
@@ -842,7 +1072,7 @@ const fileSource: (
|
|
|
842
1072
|
}
|
|
843
1073
|
return { s3Location: { uri: url.toString() } }
|
|
844
1074
|
}
|
|
845
|
-
return { bytes:
|
|
1075
|
+
return { bytes: data }
|
|
846
1076
|
})
|
|
847
1077
|
|
|
848
1078
|
/**
|
|
@@ -871,7 +1101,7 @@ const documentSource: (
|
|
|
871
1101
|
if (data instanceof Uint8Array) {
|
|
872
1102
|
return { text: new TextDecoder().decode(data) }
|
|
873
1103
|
}
|
|
874
|
-
const decoded = Base64.decodeString(
|
|
1104
|
+
const decoded = Base64.decodeString(source.bytes)
|
|
875
1105
|
if (Result.isFailure(decoded)) {
|
|
876
1106
|
return yield* AiError.make({
|
|
877
1107
|
module: "AmazonBedrockLanguageModel",
|
|
@@ -902,6 +1132,45 @@ const documentName = (fileName: string | undefined, position: number): string =>
|
|
|
902
1132
|
return `document ${position}`
|
|
903
1133
|
}
|
|
904
1134
|
|
|
1135
|
+
const guardedImageFormats: Record<string, typeof GuardrailConverseImageBlock.Encoded["format"]> = {
|
|
1136
|
+
"image/jpeg": "jpeg",
|
|
1137
|
+
"image/png": "png"
|
|
1138
|
+
}
|
|
1139
|
+
|
|
1140
|
+
const guardedImage: (
|
|
1141
|
+
part: Prompt.FilePart
|
|
1142
|
+
) => Effect.Effect<typeof GuardrailConverseImageBlock.Encoded, AiError.AiError> = Effect.fnUntraced(
|
|
1143
|
+
function*(part) {
|
|
1144
|
+
const format = guardedImageFormats[part.mediaType]
|
|
1145
|
+
if (Predicate.isUndefined(format)) {
|
|
1146
|
+
const reason = part.mediaType === "image/*"
|
|
1147
|
+
? "its format is unknown, and a guarded image must be image/png or image/jpeg"
|
|
1148
|
+
: part.mediaType.startsWith("image/")
|
|
1149
|
+
? "a guarded image must be image/png or image/jpeg"
|
|
1150
|
+
: "only text parts and png or jpeg images can be guarded"
|
|
1151
|
+
return yield* AiError.make({
|
|
1152
|
+
module: "AmazonBedrockLanguageModel",
|
|
1153
|
+
method: "prepareMessages",
|
|
1154
|
+
reason: new AiError.InvalidUserInputError({
|
|
1155
|
+
description: `Unsupported guardContent on a file part of media type '${part.mediaType}' - ${reason}`
|
|
1156
|
+
})
|
|
1157
|
+
})
|
|
1158
|
+
}
|
|
1159
|
+
const source = yield* fileSource(part.data)
|
|
1160
|
+
if (Predicate.isUndefined(source.bytes)) {
|
|
1161
|
+
return yield* AiError.make({
|
|
1162
|
+
module: "AmazonBedrockLanguageModel",
|
|
1163
|
+
method: "prepareMessages",
|
|
1164
|
+
reason: new AiError.InvalidUserInputError({
|
|
1165
|
+
description: `Unsupported guardContent on an image at '${source.s3Location?.uri}' - a guarded image ` +
|
|
1166
|
+
"must be sent as bytes or base64, not as an s3:// location"
|
|
1167
|
+
})
|
|
1168
|
+
})
|
|
1169
|
+
}
|
|
1170
|
+
return { format, source: { bytes: source.bytes } }
|
|
1171
|
+
}
|
|
1172
|
+
)
|
|
1173
|
+
|
|
905
1174
|
const prepareMessages: (options: LanguageModel.ProviderOptions) => Effect.Effect<{
|
|
906
1175
|
readonly system: ReadonlyArray<typeof SystemContentBlock.Encoded>
|
|
907
1176
|
readonly messages: ReadonlyArray<typeof Message.Encoded>
|
|
@@ -933,7 +1202,7 @@ const prepareMessages: (options: LanguageModel.ProviderOptions) => Effect.Effect
|
|
|
933
1202
|
})
|
|
934
1203
|
}
|
|
935
1204
|
for (const message of group.messages) {
|
|
936
|
-
system.push(
|
|
1205
|
+
system.push(textBlock(message.content, message.options))
|
|
937
1206
|
const cachePoint = getCachePoint(message)
|
|
938
1207
|
if (Predicate.isNotNull(cachePoint)) {
|
|
939
1208
|
system.push({ cachePoint })
|
|
@@ -950,9 +1219,14 @@ const prepareMessages: (options: LanguageModel.ProviderOptions) => Effect.Effect
|
|
|
950
1219
|
case "user": {
|
|
951
1220
|
for (const part of message.content) {
|
|
952
1221
|
if (part.type === "text") {
|
|
953
|
-
content.push(
|
|
1222
|
+
content.push(textBlock(part.text, part.options))
|
|
954
1223
|
pushCachePoint(content, part)
|
|
955
1224
|
} else if (part.type === "file") {
|
|
1225
|
+
if (part.options.amazonBedrock?.guardContent === true) {
|
|
1226
|
+
content.push({ guardContent: { image: yield* guardedImage(part) } })
|
|
1227
|
+
pushCachePoint(content, part)
|
|
1228
|
+
continue
|
|
1229
|
+
}
|
|
956
1230
|
const imageFormat = imageFormats[part.mediaType]
|
|
957
1231
|
if (Predicate.isNotUndefined(imageFormat)) {
|
|
958
1232
|
content.push({ image: { format: imageFormat, source: yield* fileSource(part.data) } })
|
|
@@ -1048,6 +1322,16 @@ const prepareMessages: (options: LanguageModel.ProviderOptions) => Effect.Effect
|
|
|
1048
1322
|
const isLastPart = k === message.content.length - 1
|
|
1049
1323
|
|
|
1050
1324
|
if (part.type === "text") {
|
|
1325
|
+
if (!Predicate.isNullish(part.options.amazonBedrock?.guardContent)) {
|
|
1326
|
+
return yield* AiError.make({
|
|
1327
|
+
module: "AmazonBedrockLanguageModel",
|
|
1328
|
+
method: "prepareMessages",
|
|
1329
|
+
reason: new AiError.InvalidUserInputError({
|
|
1330
|
+
description: "Unsupported guardContent on an assistant text part - this provider guards only " +
|
|
1331
|
+
"user text parts, user png and jpeg images, and system messages"
|
|
1332
|
+
})
|
|
1333
|
+
})
|
|
1334
|
+
}
|
|
1051
1335
|
// Skip empty text blocks
|
|
1052
1336
|
if (part.text.trim().length === 0) {
|
|
1053
1337
|
continue
|
|
@@ -1129,6 +1413,26 @@ const tryToolJsonSchema = (tool: Tool.Any, method: string) =>
|
|
|
1129
1413
|
catch: (error) => unsupportedSchemaError(error, method)
|
|
1130
1414
|
})
|
|
1131
1415
|
|
|
1416
|
+
// Bedrock is sent each tool's codec schema, which differs from the tool's own
|
|
1417
|
+
// schema wherever the codec rewrites a parameter. Params that follow the codec
|
|
1418
|
+
// are re-encoded with the tool's schema for the Toolkit to decode. Any others
|
|
1419
|
+
// are left as they are, so that the Toolkit reports the error.
|
|
1420
|
+
const decodeToolParams = (
|
|
1421
|
+
tools: ReadonlyArray<Tool.Any>,
|
|
1422
|
+
toolName: string,
|
|
1423
|
+
params: unknown
|
|
1424
|
+
): Effect.Effect<unknown> => {
|
|
1425
|
+
const tool = tools.find((tool) => tool.name === toolName)
|
|
1426
|
+
if (Predicate.isUndefined(tool)) {
|
|
1427
|
+
return Effect.succeed(params)
|
|
1428
|
+
}
|
|
1429
|
+
return Effect.try(() => toCodecAnthropic(tool.parametersSchema).codec).pipe(
|
|
1430
|
+
Effect.flatMap((codec) => Schema.decodeUnknownEffect(codec)(params)),
|
|
1431
|
+
Effect.flatMap((decoded) => Schema.encodeUnknownEffect(tool.parametersSchema)(decoded)),
|
|
1432
|
+
Effect.orElseSucceed(() => params)
|
|
1433
|
+
) as Effect.Effect<unknown>
|
|
1434
|
+
}
|
|
1435
|
+
|
|
1132
1436
|
interface ModelCapabilities {
|
|
1133
1437
|
readonly supportsStructuredOutput: boolean
|
|
1134
1438
|
readonly supportsForcedToolChoice: boolean
|
|
@@ -1326,7 +1630,7 @@ const makeResponse: (
|
|
|
1326
1630
|
documents: ReadonlyArray<CitedDocument>
|
|
1327
1631
|
) => Effect.Effect<
|
|
1328
1632
|
Array<Response.PartEncoded>,
|
|
1329
|
-
|
|
1633
|
+
AiError.AiError,
|
|
1330
1634
|
IdGenerator.IdGenerator
|
|
1331
1635
|
> = Effect.fnUntraced(function*(request, response, options, nameMapper, documents) {
|
|
1332
1636
|
const idGenerator = yield* IdGenerator.IdGenerator
|
|
@@ -1380,11 +1684,12 @@ const makeResponse: (
|
|
|
1380
1684
|
}
|
|
1381
1685
|
continue
|
|
1382
1686
|
}
|
|
1687
|
+
const name = nameMapper.getCustomName(part.toolUse.name)
|
|
1383
1688
|
parts.push({
|
|
1384
1689
|
type: "tool-call",
|
|
1385
1690
|
id: part.toolUse.toolUseId,
|
|
1386
|
-
name
|
|
1387
|
-
params: part.toolUse.input,
|
|
1691
|
+
name,
|
|
1692
|
+
params: yield* decodeToolParams(options.tools, name, part.toolUse.input),
|
|
1388
1693
|
providerExecuted: false
|
|
1389
1694
|
})
|
|
1390
1695
|
} else if (Predicate.isNotUndefined(part.reasoningContent)) {
|
|
@@ -1434,6 +1739,22 @@ const makeResponse: (
|
|
|
1434
1739
|
: InternalUtilities.resolveFinishReason(response.stopReason)
|
|
1435
1740
|
const cacheReadTokens = response.usage.cacheReadInputTokens ?? 0
|
|
1436
1741
|
const cacheWriteTokens = response.usage.cacheWriteInputTokens ?? 0
|
|
1742
|
+
const metadata = InternalGuardrails.finishMetadata(response.stopReason, response.trace)
|
|
1743
|
+
|
|
1744
|
+
if (options.responseFormat.type === "json") {
|
|
1745
|
+
const text = parts.flatMap((part) => part.type === "text" ? [part.text] : []).join("")
|
|
1746
|
+
if (InternalGuardrails.blocksObject(response.stopReason, text)) {
|
|
1747
|
+
return yield* AiError.make({
|
|
1748
|
+
module: "AmazonBedrockLanguageModel",
|
|
1749
|
+
method: "generateObject",
|
|
1750
|
+
reason: new AiError.ContentPolicyError({
|
|
1751
|
+
description: "An Amazon Bedrock guardrail intervened, and the response holds no JSON object. " +
|
|
1752
|
+
`Bedrock answered: ${truncate(text, 200)}`,
|
|
1753
|
+
metadata: { amazonBedrock: metadata }
|
|
1754
|
+
})
|
|
1755
|
+
})
|
|
1756
|
+
}
|
|
1757
|
+
}
|
|
1437
1758
|
|
|
1438
1759
|
parts.push({
|
|
1439
1760
|
type: "finish",
|
|
@@ -1453,23 +1774,26 @@ const makeResponse: (
|
|
|
1453
1774
|
},
|
|
1454
1775
|
// `Response.FinishPart` models `response` with `Schema.UndefinedOr`, which
|
|
1455
1776
|
// requires the key; HTTP response details are not surfaced by the client.
|
|
1456
|
-
response: undefined
|
|
1777
|
+
response: undefined,
|
|
1778
|
+
metadata: { amazonBedrock: metadata }
|
|
1457
1779
|
})
|
|
1458
1780
|
|
|
1459
1781
|
return parts
|
|
1460
1782
|
})
|
|
1461
1783
|
|
|
1462
1784
|
const makeStreamResponse: (
|
|
1463
|
-
request: typeof
|
|
1785
|
+
request: typeof ConverseStreamRequest.Encoded,
|
|
1464
1786
|
stream: Stream.Stream<ConverseResponseStreamEvent, AiError.AiError>,
|
|
1787
|
+
tools: ReadonlyArray<Tool.Any>,
|
|
1465
1788
|
nameMapper: Tool.NameMapper<ReadonlyArray<Tool.Any>>,
|
|
1466
|
-
documents: ReadonlyArray<CitedDocument
|
|
1789
|
+
documents: ReadonlyArray<CitedDocument>,
|
|
1790
|
+
span: Span
|
|
1467
1791
|
) => Effect.Effect<
|
|
1468
1792
|
Stream.Stream<Response.StreamPartEncoded, AiError.AiError>,
|
|
1469
1793
|
never,
|
|
1470
1794
|
IdGenerator.IdGenerator
|
|
1471
1795
|
> = Effect.fnUntraced(
|
|
1472
|
-
function*(request, stream, nameMapper, documents) {
|
|
1796
|
+
function*(request, stream, tools, nameMapper, documents, span) {
|
|
1473
1797
|
// Acquired up front: the stream itself must not carry the requirement.
|
|
1474
1798
|
const idGenerator = yield* IdGenerator.IdGenerator
|
|
1475
1799
|
|
|
@@ -1503,6 +1827,7 @@ const makeStreamResponse: (
|
|
|
1503
1827
|
// (which AWS sends AFTER messageStop and which carries the usage), so finish
|
|
1504
1828
|
// reports the real token counts rather than the pre-metadata zeros.
|
|
1505
1829
|
let finishReason: Response.FinishReason = "unknown"
|
|
1830
|
+
let stopReason: string | null = null
|
|
1506
1831
|
|
|
1507
1832
|
return stream.pipe(
|
|
1508
1833
|
Stream.mapEffect(Effect.fnUntraced(function*(event) {
|
|
@@ -1671,7 +1996,7 @@ const makeStreamResponse: (
|
|
|
1671
1996
|
type: "tool-call",
|
|
1672
1997
|
id: block.id,
|
|
1673
1998
|
name: block.name,
|
|
1674
|
-
params,
|
|
1999
|
+
params: yield* decodeToolParams(tools, block.name, params),
|
|
1675
2000
|
providerExecuted: false
|
|
1676
2001
|
})
|
|
1677
2002
|
}
|
|
@@ -1682,6 +2007,7 @@ const makeStreamResponse: (
|
|
|
1682
2007
|
// Defer the `finish` part to the trailing `metadata` event (usage
|
|
1683
2008
|
// is still zero here — metadata has not arrived yet).
|
|
1684
2009
|
finishReason = InternalUtilities.resolveFinishReason(event.messageStop.stopReason)
|
|
2010
|
+
stopReason = event.messageStop.stopReason
|
|
1685
2011
|
break
|
|
1686
2012
|
}
|
|
1687
2013
|
|
|
@@ -1692,6 +2018,7 @@ const makeStreamResponse: (
|
|
|
1692
2018
|
if (Predicate.isNotUndefined(event.metadata.usage.cacheWriteInputTokens)) {
|
|
1693
2019
|
usage.cacheWriteInputTokens = event.metadata.usage.cacheWriteInputTokens
|
|
1694
2020
|
}
|
|
2021
|
+
annotateResponse(span, request.modelId, stopReason, event.metadata.usage)
|
|
1695
2022
|
// Terminal success event: usage is now populated — emit `finish`.
|
|
1696
2023
|
parts.push({
|
|
1697
2024
|
type: "finish",
|
|
@@ -1710,7 +2037,8 @@ const makeStreamResponse: (
|
|
|
1710
2037
|
}
|
|
1711
2038
|
},
|
|
1712
2039
|
// See `makeResponse`: the key is required (`Schema.UndefinedOr`).
|
|
1713
|
-
response: undefined
|
|
2040
|
+
response: undefined,
|
|
2041
|
+
metadata: { amazonBedrock: InternalGuardrails.finishMetadata(stopReason, event.metadata.trace) }
|
|
1714
2042
|
})
|
|
1715
2043
|
break
|
|
1716
2044
|
}
|
|
@@ -1748,6 +2076,41 @@ const makeStreamResponse: (
|
|
|
1748
2076
|
}
|
|
1749
2077
|
)
|
|
1750
2078
|
|
|
2079
|
+
// =============================================================================
|
|
2080
|
+
// Telemetry
|
|
2081
|
+
// =============================================================================
|
|
2082
|
+
|
|
2083
|
+
const annotateRequest = (span: Span, request: typeof ConverseStreamRequest.Encoded): void => {
|
|
2084
|
+
addGenAIAnnotations(span, {
|
|
2085
|
+
provider: { name: "aws.bedrock" },
|
|
2086
|
+
operation: { name: "chat" },
|
|
2087
|
+
request: {
|
|
2088
|
+
model: request.modelId,
|
|
2089
|
+
maxTokens: request.inferenceConfig?.maxTokens,
|
|
2090
|
+
temperature: request.inferenceConfig?.temperature,
|
|
2091
|
+
topP: request.inferenceConfig?.topP,
|
|
2092
|
+
stopSequences: request.inferenceConfig?.stopSequences
|
|
2093
|
+
}
|
|
2094
|
+
})
|
|
2095
|
+
}
|
|
2096
|
+
|
|
2097
|
+
const annotateResponse = (span: Span, modelId: string, stopReason: string | null, usage: TokenUsage): void => {
|
|
2098
|
+
const cacheRead = usage.cacheReadInputTokens ?? 0
|
|
2099
|
+
const cacheWrite = usage.cacheWriteInputTokens ?? 0
|
|
2100
|
+
addGenAIAnnotations(span, {
|
|
2101
|
+
response: {
|
|
2102
|
+
model: modelId,
|
|
2103
|
+
finishReasons: stopReason === null ? undefined : [stopReason]
|
|
2104
|
+
},
|
|
2105
|
+
usage: {
|
|
2106
|
+
inputTokens: usage.inputTokens + cacheRead + cacheWrite,
|
|
2107
|
+
outputTokens: usage.outputTokens
|
|
2108
|
+
}
|
|
2109
|
+
})
|
|
2110
|
+
span.attribute("gen_ai.usage.cache_read.input_tokens", cacheRead)
|
|
2111
|
+
span.attribute("gen_ai.usage.cache_write.input_tokens", cacheWrite)
|
|
2112
|
+
}
|
|
2113
|
+
|
|
1751
2114
|
// =============================================================================
|
|
1752
2115
|
// Utilities
|
|
1753
2116
|
// =============================================================================
|