@wmaurer/effect-ai-amazon-bedrock 0.0.0-stage → 0.2.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 +34 -0
- package/LICENSE +22 -0
- package/README.md +429 -2
- package/dist/AmazonBedrockClient.d.ts +216 -0
- package/dist/AmazonBedrockClient.d.ts.map +1 -0
- package/dist/AmazonBedrockClient.js +129 -0
- package/dist/AmazonBedrockClient.js.map +1 -0
- package/dist/AmazonBedrockConfig.d.ts +59 -0
- package/dist/AmazonBedrockConfig.d.ts.map +1 -0
- package/dist/AmazonBedrockConfig.js +40 -0
- package/dist/AmazonBedrockConfig.js.map +1 -0
- package/dist/AmazonBedrockEventStream.d.ts +29 -0
- package/dist/AmazonBedrockEventStream.d.ts.map +1 -0
- package/dist/AmazonBedrockEventStream.js +197 -0
- package/dist/AmazonBedrockEventStream.js.map +1 -0
- package/dist/AmazonBedrockLanguageModel.d.ts +418 -0
- package/dist/AmazonBedrockLanguageModel.d.ts.map +1 -0
- package/dist/AmazonBedrockLanguageModel.js +1303 -0
- package/dist/AmazonBedrockLanguageModel.js.map +1 -0
- package/dist/AmazonBedrockSchema.d.ts +957 -0
- package/dist/AmazonBedrockSchema.d.ts.map +1 -0
- package/dist/AmazonBedrockSchema.js +775 -0
- package/dist/AmazonBedrockSchema.js.map +1 -0
- package/dist/index.d.ts +30 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +31 -0
- package/dist/index.js.map +1 -0
- package/dist/internal/credentials.d.ts +79 -0
- package/dist/internal/credentials.d.ts.map +1 -0
- package/dist/internal/credentials.js +241 -0
- package/dist/internal/credentials.js.map +1 -0
- package/dist/internal/errors.d.ts +32 -0
- package/dist/internal/errors.d.ts.map +1 -0
- package/dist/internal/errors.js +267 -0
- package/dist/internal/errors.js.map +1 -0
- package/dist/internal/utilities.d.ts +5 -0
- package/dist/internal/utilities.d.ts.map +1 -0
- package/dist/internal/utilities.js +18 -0
- package/dist/internal/utilities.js.map +1 -0
- package/package.json +66 -3
- package/src/AmazonBedrockClient.ts +386 -0
- package/src/AmazonBedrockConfig.ts +73 -0
- package/src/AmazonBedrockEventStream.ts +229 -0
- package/src/AmazonBedrockLanguageModel.ts +1746 -0
- package/src/AmazonBedrockSchema.ts +887 -0
- package/src/index.ts +36 -0
- package/src/internal/credentials.ts +319 -0
- package/src/internal/errors.ts +310 -0
- package/src/internal/utilities.ts +21 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# @wmaurer/effect-ai-amazon-bedrock
|
|
2
|
+
|
|
3
|
+
## 0.2.0
|
|
4
|
+
|
|
5
|
+
`AmazonBedrockClient.layer`, `layerConfig`, and `make` take one of three ways to authenticate: the fixed keys of 0.1.0, a `credentialProvider`, or a Bedrock `apiKey`. Options that mix two ways, or give none, do not compile. Every 0.1.0 call compiles and runs unchanged.
|
|
6
|
+
|
|
7
|
+
- Add the `credentialProvider` option. It takes a function in the shape of the AWS SDK's credential providers, such as `fromNodeProviderChain()` from `@aws-sdk/credential-providers`, or an `Effect`. The package does not depend on the AWS SDK. The client runs the provider when a request needs credentials, runs it once for concurrent requests, and keeps the credentials until five minutes before their `expiration`, or for at least 10 seconds.
|
|
8
|
+
- Keep the last credentials while the provider fails and they have not expired. Ask the provider again after 30 seconds, or when the last credentials expire if that is sooner.
|
|
9
|
+
- Refresh credentials that Bedrock rejects. After an `AuthenticationError` of kind `ExpiredKey`, `InvalidKey`, or `MissingKey`, the next request gets new credentials, and a function provider gets `{ forceRefresh: true }`. The rejected request fails and is not sent again.
|
|
10
|
+
- Fail a request with an `AuthenticationError` of kind `Unknown` when the promise of a function provider rejects, and of kind `MissingKey` when a provider returns an empty key.
|
|
11
|
+
- An `Effect` provider fails with an `AiError`, which the request fails with unchanged, or with an `AiError` reason, which the client puts in an `AiError`. The services that it requires become requirements of the layer through a new type parameter. It runs with the services of the request first, and the services that the layer was built with fill in the rest, so a span that it opens is a child of the request's span.
|
|
12
|
+
- Add the `apiKey` option for Bedrock API keys. The client sends the key as `Authorization: Bearer <key>` in place of a SigV4 signature, and an `AiError` shows the header as `<redacted>`.
|
|
13
|
+
- AWS rejects an API key that it does not accept with `AccessDeniedException`, as it rejects a missing IAM permission, so the request fails with an `AuthenticationError` of kind `InsufficientPermissions`.
|
|
14
|
+
- Add the `Credentials`, `CredentialProvider`, and `ConfigOptions` types. `Options`, `ConfigOptions`, `CredentialProvider`, `make`, `layer`, and `layerConfig` take a type parameter `R` for the services of an `Effect` provider. It defaults to `never`.
|
|
15
|
+
- A retry policy in `transformClient` or `AmazonBedrockConfig.withClientTransform` signs each attempt again with the credentials of the request, and does not retry a provider that fails.
|
|
16
|
+
|
|
17
|
+
## 0.1.0
|
|
18
|
+
|
|
19
|
+
The first release. It adds an Amazon Bedrock provider for `effect/ai`. The provider calls the Converse and ConverseStream APIs, signs requests with SigV4 through `aws4fetch`, and decodes the AWS event-stream framing without another dependency. `AmazonBedrockClient.layer` and `AmazonBedrockClient.layerConfig` take an access key and a secret key, and the optional `sessionToken`, `region`, `apiUrl`, and `transformClient`. The region defaults to `us-east-1`. `AmazonBedrockLanguageModel.Config` takes the Converse request fields other than `messages` and `system`.
|
|
20
|
+
|
|
21
|
+
- Add tool calling to `generateText`. The provider sends `toolConfig` with the tools and the `toolChoice`, decodes `toolUse` blocks into `tool-call` parts, and sends tool results back as `toolResult` blocks.
|
|
22
|
+
- Add tool calling to `streamText`. Streamed tool calls arrive as `tool-params-start`, `tool-params-delta`, `tool-params-end`, and a final `tool-call` part.
|
|
23
|
+
- Add structured output. `generateObject` sends the schema in the Converse `outputConfig.textFormat` field, so your own tools can go with it. The models on the provider's list, which are the Claude 3 models and Claude Sonnet 4 and Opus 4, get the object through a tool that replaces your tools. If the model calls the tool more than once, the first call is the object. Every model also gets the tool when `structuredOutputs` is `false`, and when the prompt holds tool calls but no tools go out, because Converse rejects such a request without a `toolConfig`. On the tool path, models that reject a forced `toolChoice`, such as Claude Opus 5.5 and Sonnet 5.5, get the tool unforced. `Config.structuredOutputs` overrides the choice of path, `Config.forcedToolChoice` overrides whether the tool is forced, and `Config.outputConfig` takes `effort`.
|
|
24
|
+
- Send `file` prompt parts as Converse `image` and `document` blocks. The provider accepts JPEG, PNG, GIF, and WebP images, and PDF, CSV, DOC, DOCX, XLS, XLSX, HTML, TXT, and Markdown documents. File data can be base64, a `Uint8Array`, or an `s3://` URL. Other media types and other URLs fail with `InvalidUserInputError`.
|
|
25
|
+
- Send a cited `text/*` document as a `text` source instead of base64. An uncited text document still goes as `bytes`, because Converse rejects a `text` source without a citations config.
|
|
26
|
+
- Send `context` on a document block through the `amazonBedrock.context` option of a file part.
|
|
27
|
+
- Add document citations. The `amazonBedrock.citations` option of a file part opts the document in. Citations come back as `source` parts whose `amazonBedrock` metadata names the cited span.
|
|
28
|
+
- Add prompt caching. The `amazonBedrock.cachePoint` option of a message or part adds a cache point after that content, with an optional `ttl` of `"5m"` or `"1h"`.
|
|
29
|
+
- Add tool-definition caching. Annotate a tool with `AmazonBedrockLanguageModel.ToolCachePoint` to cache the tool list up to that tool.
|
|
30
|
+
- Add reasoning. `reasoningContent` blocks decode into `reasoning` parts on both paths, and reasoning parts go back to Bedrock in later turns with their signature or redacted payload.
|
|
31
|
+
- Pass model-specific fields through `Config.additionalModelRequestFields`, such as the `thinking` block that turns on extended thinking.
|
|
32
|
+
- Fail a 401 or 403 response with an `AuthenticationError` that keeps the AWS error message. The message tells wrong credentials apart from a model that the account cannot use. The `kind` of the error comes from the AWS exception name in the `x-amzn-errortype` header. `AccessDeniedException` gives `InsufficientPermissions`, and `ExpiredTokenException` gives `ExpiredKey`. `UnrecognizedClientException`, `InvalidSignatureException`, and `IncompleteSignatureException` give `InvalidKey`, and `MissingAuthenticationTokenException` gives `MissingKey`. Without the header, or with another name, a 401 gives `InvalidKey` and a 403 gives `InsufficientPermissions`.
|
|
33
|
+
- Fail `generateText` and `streamText` with `InvalidUserInputError`, before the request is sent, when the prompt holds tool calls or tool results and the request carries no `toolConfig`. Converse answers such a request with a 400. The error names the workaround: pass the toolkit that produced the tool calls, with `toolChoice: "auto"`.
|
|
34
|
+
- Redact the `X-Amz-Security-Token` header in the request and response headers of an `AiError` and in an HTTP span that records headers. `converse` and `converseStream` add the header to the names in `Headers.CurrentRedactedNames`, so the names that you set stay redacted too. An error from a request signed with temporary credentials does not show the session token.
|
package/LICENSE
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2023 Effectful Technologies Inc
|
|
4
|
+
Copyright (c) 2026 Wayne Maurer
|
|
5
|
+
|
|
6
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
7
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
8
|
+
in the Software without restriction, including without limitation the rights
|
|
9
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
10
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
11
|
+
furnished to do so, subject to the following conditions:
|
|
12
|
+
|
|
13
|
+
The above copyright notice and this permission notice shall be included in all
|
|
14
|
+
copies or substantial portions of the Software.
|
|
15
|
+
|
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
17
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
18
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
19
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
20
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
21
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
22
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,430 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @wmaurer/effect-ai-amazon-bedrock
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
An [Amazon Bedrock](https://aws.amazon.com/bedrock/) provider for the Effect AI modules in `effect/ai`. `AmazonBedrockLanguageModel` provides a `LanguageModel` that calls the Bedrock Runtime Converse and ConverseStream APIs. `AmazonBedrockClient` signs each request with AWS Signature Version 4 through [`aws4fetch`](https://github.com/mhart/aws4fetch).
|
|
4
|
+
|
|
5
|
+
The package supports text generation, streaming, tool calls, structured output, images, documents, citations, prompt caching, and reasoning.
|
|
6
|
+
|
|
7
|
+
## Install the package
|
|
8
|
+
|
|
9
|
+
```sh
|
|
10
|
+
pnpm add @wmaurer/effect-ai-amazon-bedrock effect
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
The package declares `effect` `^4.0.5` as a peer dependency. Its tests run on `effect` 4.0.5.
|
|
14
|
+
|
|
15
|
+
Set these options in your `tsconfig.json`:
|
|
16
|
+
|
|
17
|
+
```json
|
|
18
|
+
{
|
|
19
|
+
"compilerOptions": {
|
|
20
|
+
"target": "esnext",
|
|
21
|
+
"exactOptionalPropertyTypes": true
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Effect's declaration files use `AsyncDisposable` and `Symbol.asyncDispose`, which the default library of TypeScript 6.0 and 7 does not declare. Without `"target": "esnext"`, `tsc` reports errors TS2304 and TS2550 in those files. Without `exactOptionalPropertyTypes`, `tsc` reports error TS2411 on the `amazonBedrock` option types. Effect's own AI providers at 4.0.5, such as `@effect/ai-anthropic` and `@effect/ai-openai`, need the same option, so a project that uses one of them has it already. `skipLibCheck` hides all of these errors and can replace both options.
|
|
27
|
+
|
|
28
|
+
The options work with TypeScript 6.0 and later. TypeScript 5.9 also needs `"strict": true` and `"module": "nodenext"`.
|
|
29
|
+
|
|
30
|
+
### Link the source to use unreleased changes
|
|
31
|
+
|
|
32
|
+
To use a change that is not on npm yet, link the package source from a clone of this repository. The steps need pnpm 10.5.1 or later, because they put a pnpm override in `pnpm-workspace.yaml`. Earlier versions ignore the override or reject the file. Replace `<checkout>` with the absolute path of the clone.
|
|
33
|
+
|
|
34
|
+
1. Clone the repository and run `pnpm install` in the clone. The install creates `<checkout>/packages/amazon-bedrock/node_modules/effect`, which the next step needs.
|
|
35
|
+
|
|
36
|
+
```sh
|
|
37
|
+
git clone https://github.com/wmaurer/effect-ai-amazon-bedrock.git <checkout>
|
|
38
|
+
cd <checkout>
|
|
39
|
+
pnpm install
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
2. In your project, add an override to `pnpm-workspace.yaml` that points `effect` at the copy in the clone.
|
|
43
|
+
|
|
44
|
+
```yaml
|
|
45
|
+
overrides:
|
|
46
|
+
effect: link:<checkout>/packages/amazon-bedrock/node_modules/effect
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
The linked package and your project must load the same copy of `effect`. With two copies, the client cannot read a `Redacted` value that the other copy made, and it fails with `Error: Unable to get redacted value` before it sends a request.
|
|
50
|
+
|
|
51
|
+
3. Add the linked package and `effect` to your project.
|
|
52
|
+
|
|
53
|
+
```sh
|
|
54
|
+
pnpm add link:<checkout>/packages/amazon-bedrock effect
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Because of the override, pnpm records `effect` in your `package.json` as a `link:` path and not as a version range.
|
|
58
|
+
|
|
59
|
+
4. Set these options in your `tsconfig.json`.
|
|
60
|
+
|
|
61
|
+
```json
|
|
62
|
+
{
|
|
63
|
+
"compilerOptions": {
|
|
64
|
+
"target": "esnext",
|
|
65
|
+
"exactOptionalPropertyTypes": true,
|
|
66
|
+
"allowImportingTsExtensions": true,
|
|
67
|
+
"noEmit": true
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
The linked package's imports resolve to its `.ts` source files. TypeScript 7 reports TS2411 on that source even with `skipLibCheck`, so set `exactOptionalPropertyTypes`. The source imports its own modules with a `.ts` extension, so without `allowImportingTsExtensions`, `tsc` reports error TS5097. `tsc` accepts `allowImportingTsExtensions` only with `noEmit`, `emitDeclarationOnly`, or `rewriteRelativeImportExtensions`, and reports error TS5096 without one of them. Set `target` for the reason that [Install the package](#install-the-package) gives. TypeScript 5.9 needs the same two extra options as there.
|
|
73
|
+
|
|
74
|
+
## Generate text
|
|
75
|
+
|
|
76
|
+
This program reads AWS credentials from the environment, asks a Claude model on Bedrock for one sentence, and prints the answer.
|
|
77
|
+
|
|
78
|
+
```ts
|
|
79
|
+
import { AmazonBedrockClient, AmazonBedrockLanguageModel } from "@wmaurer/effect-ai-amazon-bedrock"
|
|
80
|
+
import { Config, Effect, Layer, Redacted } from "effect"
|
|
81
|
+
import { LanguageModel } from "effect/ai"
|
|
82
|
+
import { FetchHttpClient } from "effect/http"
|
|
83
|
+
|
|
84
|
+
const BedrockClient = AmazonBedrockClient.layerConfig({
|
|
85
|
+
accessKeyId: Config.String("AWS_ACCESS_KEY_ID"),
|
|
86
|
+
secretAccessKey: Config.Redacted("AWS_SECRET_ACCESS_KEY"),
|
|
87
|
+
sessionToken: Config.Redacted("AWS_SESSION_TOKEN").pipe(Config.withDefault(Redacted.make(""))),
|
|
88
|
+
region: Config.String("AWS_REGION")
|
|
89
|
+
}).pipe(Layer.provide(FetchHttpClient.layer))
|
|
90
|
+
|
|
91
|
+
const Sonnet = AmazonBedrockLanguageModel.model("eu.anthropic.claude-sonnet-4-5-20250929-v1:0")
|
|
92
|
+
|
|
93
|
+
const program = Effect.gen(function*() {
|
|
94
|
+
const response = yield* LanguageModel.generateText({
|
|
95
|
+
prompt: "Describe Amazon Bedrock in one sentence."
|
|
96
|
+
})
|
|
97
|
+
console.log(response.text)
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
program.pipe(
|
|
101
|
+
Effect.provide(Sonnet),
|
|
102
|
+
Effect.provide(BedrockClient),
|
|
103
|
+
Effect.runPromise
|
|
104
|
+
)
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
To run the program, do these steps:
|
|
108
|
+
|
|
109
|
+
1. Create a project whose `package.json` sets `"type": "module"`, and install the package.
|
|
110
|
+
|
|
111
|
+
```sh
|
|
112
|
+
mkdir bedrock-example
|
|
113
|
+
cd bedrock-example
|
|
114
|
+
pnpm init --init-type module
|
|
115
|
+
pnpm add @wmaurer/effect-ai-amazon-bedrock effect
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
Without a `type` field, Node still runs `main.ts` as an ES module, but it first prints the warning `MODULE_TYPELESS_PACKAGE_JSON`.
|
|
119
|
+
|
|
120
|
+
2. Save the program as `main.ts` in the project.
|
|
121
|
+
|
|
122
|
+
3. Set your AWS credentials in the environment.
|
|
123
|
+
|
|
124
|
+
```sh
|
|
125
|
+
export AWS_ACCESS_KEY_ID=<access key ID>
|
|
126
|
+
export AWS_SECRET_ACCESS_KEY=<secret access key>
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
If you use temporary credentials, such as the result of `aws sts assume-role`, also set `AWS_SESSION_TOKEN`. Bedrock rejects a temporary key without its session token. If either variable is missing, Node stops with an `UnhandledPromiseRejection` whose reason is `"[object Object]"`, and the message does not name the variable.
|
|
130
|
+
|
|
131
|
+
4. Run the program with Node 22.18 or a later 22 release, or with Node 23.6 or later. These versions run a TypeScript file without a build step.
|
|
132
|
+
|
|
133
|
+
```sh
|
|
134
|
+
AWS_REGION=eu-west-1 node main.ts
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
## Configure the client
|
|
138
|
+
|
|
139
|
+
`AmazonBedrockClient` needs an `HttpClient`. The example above uses `FetchHttpClient.layer` from `effect/http`.
|
|
140
|
+
|
|
141
|
+
The client authenticates each request in one of three ways:
|
|
142
|
+
|
|
143
|
+
- Fixed keys. `accessKeyId`, `secretAccessKey`, and an optional `sessionToken`. The client signs each request with SigV4.
|
|
144
|
+
- A credential provider. `credentialProvider` is a function or an `Effect` that returns credentials. The client signs each request with SigV4 and asks the provider for new credentials before the old ones expire.
|
|
145
|
+
- A Bedrock API key. `apiKey` is sent as `Authorization: Bearer <key>` in place of a SigV4 signature.
|
|
146
|
+
|
|
147
|
+
Give exactly one of them. Options that mix two ways, or give none, do not compile. `AmazonBedrockClient.layerConfig` reads each value from a `Config`. `AmazonBedrockClient.layer` takes plain values, with the secrets as `Redacted` values. The client reads no environment variable and no file by itself.
|
|
148
|
+
|
|
149
|
+
### Use fixed keys
|
|
150
|
+
|
|
151
|
+
The example above reads fixed keys from the environment. `sessionToken` is optional, but `layerConfig` fails with a `ConfigError` if a `Config` that you give it finds no value. For that reason the example gives `AWS_SESSION_TOKEN` an empty default with `Config.withDefault`. The client sends no session token when the token is empty, so the same program runs with temporary and long-lived keys. AWS Lambda sets `AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY`, and `AWS_SESSION_TOKEN` for the function's execution role, so the example runs there without changes.
|
|
152
|
+
|
|
153
|
+
The client reads fixed keys once, when the layer is built. When temporary keys expire, every request fails with an `AuthenticationError` of kind `ExpiredKey`. A program that runs longer than its session needs a credential provider.
|
|
154
|
+
|
|
155
|
+
### Use a credential provider
|
|
156
|
+
|
|
157
|
+
A function in the shape of the AWS SDK's credential providers fits `credentialProvider` without an adapter. `fromNodeProviderChain()` from `@aws-sdk/credential-providers` looks for credentials where the AWS CLI does, in the environment, in the profiles and IAM Identity Center sessions of `~/.aws`, and in the container credentials of Amazon ECS and Amazon EKS and the instance metadata of Amazon EC2. This package does not depend on the AWS SDK, so install `@aws-sdk/credential-providers` yourself.
|
|
158
|
+
|
|
159
|
+
```ts
|
|
160
|
+
import { fromNodeProviderChain } from "@aws-sdk/credential-providers"
|
|
161
|
+
import { AmazonBedrockClient } from "@wmaurer/effect-ai-amazon-bedrock"
|
|
162
|
+
import { Layer } from "effect"
|
|
163
|
+
import { FetchHttpClient } from "effect/http"
|
|
164
|
+
|
|
165
|
+
const BedrockClient = AmazonBedrockClient.layer({
|
|
166
|
+
region: "eu-west-1",
|
|
167
|
+
credentialProvider: fromNodeProviderChain()
|
|
168
|
+
}).pipe(Layer.provide(FetchHttpClient.layer))
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
The client runs the provider when a request needs credentials, and runs it once for requests that need them at the same time:
|
|
172
|
+
|
|
173
|
+
- It keeps the credentials until five minutes before their `expiration`. It keeps credentials that expire in less than five minutes for 10 seconds, and credentials without an `expiration` until Bedrock rejects them.
|
|
174
|
+
- If the provider fails while the last credentials have not expired, the client signs with the last credentials. It asks the provider again after 30 seconds, or when the last credentials expire if that is sooner.
|
|
175
|
+
- If Bedrock rejects the credentials with an `AuthenticationError` of kind `ExpiredKey`, `InvalidKey`, or `MissingKey`, the request fails, and the next request gets new credentials. The client calls a function provider with `{ forceRefresh: true }` for them. Kind `InsufficientPermissions` keeps the credentials, because new credentials for the same role have the same IAM policy.
|
|
176
|
+
- If the promise of a function provider rejects, the request fails with an `AuthenticationError` of kind `Unknown`. If the provider returns an empty access key ID or secret access key, the request fails with an `AuthenticationError` of kind `MissingKey`.
|
|
177
|
+
|
|
178
|
+
An `Effect` provider can load credentials from a service of your own. The layer then requires that service:
|
|
179
|
+
|
|
180
|
+
```ts
|
|
181
|
+
import { AmazonBedrockClient } from "@wmaurer/effect-ai-amazon-bedrock"
|
|
182
|
+
import { Context, Effect } from "effect"
|
|
183
|
+
import type { AiError } from "effect/ai"
|
|
184
|
+
|
|
185
|
+
class Vault extends Context.Service<Vault, {
|
|
186
|
+
readonly bedrockCredentials: Effect.Effect<AmazonBedrockClient.Credentials, AiError.AiError>
|
|
187
|
+
}>()("Vault") {}
|
|
188
|
+
|
|
189
|
+
// Layer<AmazonBedrockClient, never, HttpClient | Vault>
|
|
190
|
+
export const BedrockClient = AmazonBedrockClient.layer({
|
|
191
|
+
region: "eu-west-1",
|
|
192
|
+
credentialProvider: Effect.gen(function*() {
|
|
193
|
+
const vault = yield* Vault
|
|
194
|
+
return yield* vault.bedrockCredentials
|
|
195
|
+
})
|
|
196
|
+
})
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
The provider runs with the services of the request first, and the services that the layer was built with fill in the rest. A span that the provider opens is a child of the span of the request that runs the provider.
|
|
200
|
+
|
|
201
|
+
The `Effect` can fail with an `AiError`, which the request fails with unchanged, or with an `AiError` reason such as an `AuthenticationError`, which the client puts in an `AiError`.
|
|
202
|
+
|
|
203
|
+
A retry policy in `transformClient` does not retry a failed provider, because the client gets the credentials before it sends the request. Each retry of a request signs again with the credentials of the first attempt.
|
|
204
|
+
|
|
205
|
+
### Use a Bedrock API key
|
|
206
|
+
|
|
207
|
+
```ts
|
|
208
|
+
import { AmazonBedrockClient } from "@wmaurer/effect-ai-amazon-bedrock"
|
|
209
|
+
import { Config, Layer } from "effect"
|
|
210
|
+
import { FetchHttpClient } from "effect/http"
|
|
211
|
+
|
|
212
|
+
const BedrockClient = AmazonBedrockClient.layerConfig({
|
|
213
|
+
region: Config.String("AWS_REGION"),
|
|
214
|
+
apiKey: Config.Redacted("AWS_BEARER_TOKEN_BEDROCK")
|
|
215
|
+
}).pipe(Layer.provide(FetchHttpClient.layer))
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
`AWS_BEARER_TOKEN_BEDROCK` is the variable that the AWS SDK for JavaScript reads the key from. The client reads it only through the `Config` that you give it. An `AiError` and an HTTP span that records headers show the `authorization` header as `<redacted>`.
|
|
219
|
+
|
|
220
|
+
AWS rejects an API key that it does not accept, such as an expired or revoked key, with `AccessDeniedException`, as it rejects a request that the IAM policy does not allow. The request then fails with an `AuthenticationError` of kind `InsufficientPermissions`, and the client cannot tell the two causes apart.
|
|
221
|
+
|
|
222
|
+
### Set the other options
|
|
223
|
+
|
|
224
|
+
The other options are these:
|
|
225
|
+
|
|
226
|
+
- `region` defaults to `us-east-1`.
|
|
227
|
+
- `apiUrl` replaces the endpoint, which defaults to `https://bedrock-runtime.<region>.amazonaws.com`.
|
|
228
|
+
- `transformClient` changes the `HttpClient` for every request, for example to add a retry policy. To change it for one effect only, use `AmazonBedrockConfig.withClientTransform`.
|
|
229
|
+
|
|
230
|
+
## Choose a model
|
|
231
|
+
|
|
232
|
+
Give a Bedrock model ID or an inference profile ID as the first argument of `AmazonBedrockLanguageModel.model`, or as the `model` option of `AmazonBedrockLanguageModel.layer`. Some models accept only an inference profile ID. The live suites call Claude Sonnet 4.5 through `eu.anthropic.claude-sonnet-4-5-20250929-v1:0`, and their setup notes that Bedrock rejects its bare foundation-model ID. The model must be enabled for your account in the region of the client.
|
|
233
|
+
|
|
234
|
+
To set request defaults, give them as the second argument of `model` or as the `config` option of `layer`. The defaults take these fields:
|
|
235
|
+
|
|
236
|
+
- `inferenceConfig`, `additionalModelRequestFields`, and `toolConfig`. The provider sends each one as the Converse request field of the same name.
|
|
237
|
+
- `outputConfig`, which takes only `effort`. The provider sets `outputConfig.textFormat` itself. `effort` is `low`, `medium`, `high`, `xhigh`, or `max`, and a higher level lets the model reason longer before it answers. The Bedrock Runtime API model documents that the level is capped at `high` when extended thinking is off. Not every model takes `effort`. On 2026-10-17 Bedrock rejected it for Claude Sonnet 4.5 and Haiku 4.5 with `This model doesn't support the effort field`.
|
|
238
|
+
- `structuredOutputs` and `forcedToolChoice`, which choose how `generateObject` asks for the object. The provider does not send them to Bedrock. [Get structured output](#get-structured-output) explains them.
|
|
239
|
+
|
|
240
|
+
When `generateObject` asks for the object through a tool, that tool replaces your `toolConfig`. Any other request with tools replaces it with a `toolConfig` built from those tools, unless `toolChoice` is `"none"`. With no tools, or with `toolChoice` set to `"none"`, the provider sends your `toolConfig` unchanged.
|
|
241
|
+
|
|
242
|
+
To change the defaults for one request, wrap the effect in `AmazonBedrockLanguageModel.withConfigOverride`. It takes the same fields and `modelId`. Set `modelId` to send that request to a different model. Each field you give replaces the whole default field.
|
|
243
|
+
|
|
244
|
+
This example turns on extended thinking for one request on a Claude model that takes a thinking budget, such as Claude Sonnet 4.5. The thinking budget comes out of `maxTokens`, so `maxTokens` must be greater than `budget_tokens`. When `answer` runs with `Sonnet`, the override replaces all of `inferenceConfig`, so the request sends `maxTokens: 4096` and no `temperature`.
|
|
245
|
+
|
|
246
|
+
```ts
|
|
247
|
+
import { AmazonBedrockLanguageModel } from "@wmaurer/effect-ai-amazon-bedrock"
|
|
248
|
+
import { LanguageModel } from "effect/ai"
|
|
249
|
+
|
|
250
|
+
export const Sonnet = AmazonBedrockLanguageModel.model("eu.anthropic.claude-sonnet-4-5-20250929-v1:0", {
|
|
251
|
+
inferenceConfig: { maxTokens: 1024, temperature: 0.2 }
|
|
252
|
+
})
|
|
253
|
+
|
|
254
|
+
export const answer = LanguageModel.generateText({
|
|
255
|
+
prompt: "How many r's are in strawberry?"
|
|
256
|
+
}).pipe(
|
|
257
|
+
AmazonBedrockLanguageModel.withConfigOverride({
|
|
258
|
+
additionalModelRequestFields: {
|
|
259
|
+
thinking: { type: "enabled", budget_tokens: 1024 }
|
|
260
|
+
},
|
|
261
|
+
inferenceConfig: { maxTokens: 4096 }
|
|
262
|
+
})
|
|
263
|
+
)
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
The live reasoning suite treats Claude Opus 4.7 and 4.8 and the Claude Opus, Sonnet, Fable, and Mythos 5.x models as rejecting `budget_tokens` with a 400, and sends them adaptive thinking instead. On 2026-10-17 Bedrock rejected `budget_tokens` for Opus 4.8, Opus 5.5, and Sonnet 5.5. On these models `outputConfig.effort` sets how much they reason. The suite's notes say that their reasoning text is empty unless `display` is `"summarized"`. When `BEDROCK_MODEL_ID` names one of them, the suite sends this form:
|
|
267
|
+
|
|
268
|
+
```ts
|
|
269
|
+
import { AmazonBedrockLanguageModel } from "@wmaurer/effect-ai-amazon-bedrock"
|
|
270
|
+
import { LanguageModel } from "effect/ai"
|
|
271
|
+
|
|
272
|
+
export const adaptiveAnswer = LanguageModel.generateText({
|
|
273
|
+
prompt: "How many r's are in strawberry?"
|
|
274
|
+
}).pipe(
|
|
275
|
+
AmazonBedrockLanguageModel.withConfigOverride({
|
|
276
|
+
additionalModelRequestFields: {
|
|
277
|
+
thinking: { type: "adaptive", display: "summarized" }
|
|
278
|
+
},
|
|
279
|
+
outputConfig: { effort: "high" },
|
|
280
|
+
inferenceConfig: { maxTokens: 4096 }
|
|
281
|
+
})
|
|
282
|
+
)
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
## Get structured output
|
|
286
|
+
|
|
287
|
+
`LanguageModel.generateText`, `LanguageModel.streamText`, and `LanguageModel.generateObject` all work with this provider. By default, `generateObject` sends the schema in the Converse `outputConfig.textFormat` field, and the tools you pass go with it. The provider converts the schema with the rules for Anthropic models.
|
|
288
|
+
|
|
289
|
+
A probe on 2026-10-17 sent one Converse request per path to 17 Claude inference profiles through the `eu-west-1` endpoint. Every profile that served the probe's requests, 12 of the 17, accepted `outputConfig.textFormat`: Claude Haiku 4.5 and 5.5, Opus 4.5, 4.6, 4.7, 4.8, 5, and 5.5, and Sonnet 4.5, 4.6, 5, and 5.5. On Opus 4.8, Opus 5.5, and Sonnet 5.5 it also accepted `outputConfig.textFormat` together with `effort`, with adaptive thinking, and next to a strict tool. Fable 5 and 5.1 rejected every request, with or without `outputConfig.textFormat`, with `data retention mode 'default' is not available for this model`, and the other three profiles have reached end of life.
|
|
290
|
+
|
|
291
|
+
The provider keeps a list of models that get the schema as a tool instead: the Claude 3 models and Claude Sonnet 4 and Opus 4. The list comes from Anthropic's table of models with structured output. The probe could not check it on Bedrock. Claude 3 Haiku, Claude 3 Sonnet, and Sonnet 4 have reached end of life in `eu-west-1`, and the probe did not try the other models on the list. The tool replaces the tools you pass, and the tool's input becomes the object. If the model calls the tool more than once, the first call is the object. If the model answers in text instead of calling the tool, `effect/ai` decodes the text as the object.
|
|
292
|
+
|
|
293
|
+
On the tool path the provider forces the model to call the tool. In the probe, Claude Opus 5.5 and Sonnet 5.5 rejected a forced `toolChoice` with `tool_choice: type "tool" and "any" are not supported for this model`. The provider also treats Fable 5.1 and Mythos 5.1 as rejecting it, which nobody has checked on Bedrock. For these four models the provider leaves the tool unforced and adds a system prompt line that tells the model to call it. They take the tool path only when `structuredOutputs` is `false`, or when the prompt holds tool calls and no tools go out.
|
|
294
|
+
|
|
295
|
+
When the prompt holds tool calls or tool results and the request offers no tools, `generateObject` uses the tool on every model, even with `structuredOutputs: true`. Converse rejects such a request unless it carries a `toolConfig`, and the object tool is one.
|
|
296
|
+
|
|
297
|
+
The provider matches the model ID against its lists. Two config fields override the match, and the provider sends neither to Bedrock:
|
|
298
|
+
|
|
299
|
+
- `structuredOutputs` set to `true` sends `outputConfig.textFormat` to any model, except when the prompt holds tool calls and no tools go out. Set to `false`, it sends the tool.
|
|
300
|
+
- `forcedToolChoice` set to `true` forces the tool, and set to `false` leaves it unforced. It has an effect only on the tool path. It does not change a `toolChoice` you pass with your own tools: `"required"` and a named tool go to Bedrock as `any` and `tool`, and the probe's error from Opus 5.5 and Sonnet 5.5 names both as unsupported.
|
|
301
|
+
|
|
302
|
+
A model ID that does not name the model, such as an application inference profile ARN, gets `outputConfig.textFormat`, or a forced tool when `structuredOutputs` is `false`. With tool history and no tools it also gets a forced tool by default. Behind an ARN that wraps Opus 5.5 or Sonnet 5.5, set `forcedToolChoice: false` for that case. To send the unforced tool through such an ARN in every case, set both fields:
|
|
303
|
+
|
|
304
|
+
```ts
|
|
305
|
+
import { AmazonBedrockLanguageModel } from "@wmaurer/effect-ai-amazon-bedrock"
|
|
306
|
+
|
|
307
|
+
export const Opus = AmazonBedrockLanguageModel.model(
|
|
308
|
+
"arn:aws:bedrock:eu-west-1:123456789012:application-inference-profile/a1b2c3d4e5f6",
|
|
309
|
+
{ structuredOutputs: false, forcedToolChoice: false }
|
|
310
|
+
)
|
|
311
|
+
```
|
|
312
|
+
|
|
313
|
+
## Set Bedrock options on a prompt
|
|
314
|
+
|
|
315
|
+
The provider reads its options from the `amazonBedrock` key of the `options` field on prompt messages and parts.
|
|
316
|
+
|
|
317
|
+
To cache a prompt prefix, add `cachePoint` to the last message or part of the prefix. Bedrock caches everything before the cache point. `ttl` is `"5m"` or `"1h"`. If you leave it out, Bedrock chooses the lifetime.
|
|
318
|
+
|
|
319
|
+
```ts
|
|
320
|
+
import { Prompt } from "effect/ai"
|
|
321
|
+
|
|
322
|
+
declare const longInstructions: string
|
|
323
|
+
|
|
324
|
+
export const prompt = Prompt.fromMessages([
|
|
325
|
+
Prompt.makeMessage("system", {
|
|
326
|
+
content: longInstructions,
|
|
327
|
+
options: { amazonBedrock: { cachePoint: { type: "default", ttl: "1h" } } }
|
|
328
|
+
}),
|
|
329
|
+
Prompt.makeMessage("user", {
|
|
330
|
+
content: [Prompt.makePart("text", { text: "Summarize the rules." })]
|
|
331
|
+
})
|
|
332
|
+
])
|
|
333
|
+
```
|
|
334
|
+
|
|
335
|
+
To cache the tool list, annotate the last tool of the cached prefix with `ToolCachePoint`.
|
|
336
|
+
|
|
337
|
+
```ts
|
|
338
|
+
import { AmazonBedrockLanguageModel } from "@wmaurer/effect-ai-amazon-bedrock"
|
|
339
|
+
import { Schema } from "effect"
|
|
340
|
+
import { Tool } from "effect/ai"
|
|
341
|
+
|
|
342
|
+
export const search = Tool.make("search", {
|
|
343
|
+
parameters: Schema.Struct({ query: Schema.String })
|
|
344
|
+
}).annotate(AmazonBedrockLanguageModel.ToolCachePoint, { type: "default" })
|
|
345
|
+
```
|
|
346
|
+
|
|
347
|
+
A `file` part becomes an `image` block or a `document` block. Bedrock accepts JPEG, PNG, GIF, and WebP images, and PDF, CSV, DOC, DOCX, XLS, XLSX, HTML, TXT, and Markdown documents. Give the data as base64, as a `Uint8Array`, or as an `s3://` URL. The provider does not fetch other URLs.
|
|
348
|
+
|
|
349
|
+
A document part takes two more options. `citations` lets the model cite the document, and the response then holds `source` parts that point at the cited text. `context` tells the model how to read the document.
|
|
350
|
+
|
|
351
|
+
```ts
|
|
352
|
+
import { Prompt } from "effect/ai"
|
|
353
|
+
|
|
354
|
+
declare const reportBase64: string
|
|
355
|
+
|
|
356
|
+
export const prompt = Prompt.fromMessages([
|
|
357
|
+
Prompt.makeMessage("user", {
|
|
358
|
+
content: [
|
|
359
|
+
Prompt.makePart("file", {
|
|
360
|
+
mediaType: "application/pdf",
|
|
361
|
+
fileName: "q3-report.pdf",
|
|
362
|
+
data: reportBase64,
|
|
363
|
+
options: {
|
|
364
|
+
amazonBedrock: {
|
|
365
|
+
citations: { enabled: true },
|
|
366
|
+
context: "A quarterly earnings report."
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
}),
|
|
370
|
+
Prompt.makePart("text", { text: "What was the revenue? Cite the report." })
|
|
371
|
+
]
|
|
372
|
+
})
|
|
373
|
+
])
|
|
374
|
+
```
|
|
375
|
+
|
|
376
|
+
Reasoning parts carry the Bedrock signature in `metadata.amazonBedrock.info`. Bedrock accepts reasoning in a later turn only with that signature. `Prompt.fromResponseParts` and `Chat` copy it into the options of the next prompt. A reasoning part without it is left out of the request.
|
|
377
|
+
|
|
378
|
+
### Avoid a clash with another Bedrock provider
|
|
379
|
+
|
|
380
|
+
The provider adds the `amazonBedrock` key to the option and metadata types of `effect/ai` through module augmentation. If Effect ships its own Amazon Bedrock provider that uses the same key, both packages declare the same property. Install only one of them.
|
|
381
|
+
|
|
382
|
+
## Check what the provider does not support
|
|
383
|
+
|
|
384
|
+
Converse has features that this provider does not model yet:
|
|
385
|
+
|
|
386
|
+
- `additionalModelResponseFieldPaths` and the `additionalModelResponseFields` it returns.
|
|
387
|
+
- Video blocks. A `video/*` file part fails with `InvalidUserInputError`.
|
|
388
|
+
- Documents given as pre-chunked `content`.
|
|
389
|
+
- The `error` field of an image block.
|
|
390
|
+
- The `audio`, `guardContent`, `searchResult`, `toolAddition`, and `toolRemoval` content blocks, which the provider decodes and ignores.
|
|
391
|
+
- Citations with a `web` or `searchResultLocation` location, which the provider drops.
|
|
392
|
+
- System tools, which run on Bedrock.
|
|
393
|
+
- The Converse request fields `guardrailConfig`, `promptVariables`, `requestMetadata`, `performanceConfig`, and `serviceTier`. The request config has no key for them.
|
|
394
|
+
|
|
395
|
+
`LanguageModel.streamText` always asks for text, so streamed structured output is not available. That limit is in `effect/ai`, not in this provider.
|
|
396
|
+
|
|
397
|
+
Converse has no `"none"` tool choice, and it rejects a request whose messages hold tool calls or tool results unless the request carries a `toolConfig`. A `generateText` or `streamText` call whose prompt holds tool calls therefore fails with `InvalidUserInputError` before the provider sends it, when the call has no tools, its `toolChoice` is a `oneOf` that names none of them, or it sets `toolChoice: "none"`. A `toolConfig` in the request config counts, and `generateObject` sends the object tool in this case, as [Get structured output](#get-structured-output) explains. To continue such a conversation with text, pass the toolkit that produced the tool calls, with `toolChoice: "auto"`:
|
|
398
|
+
|
|
399
|
+
```ts
|
|
400
|
+
import { Schema } from "effect"
|
|
401
|
+
import { LanguageModel, type Prompt, Tool, Toolkit } from "effect/ai"
|
|
402
|
+
|
|
403
|
+
const ReadFile = Tool.make("readFile", {
|
|
404
|
+
parameters: Schema.Struct({ path: Schema.String }),
|
|
405
|
+
success: Schema.String
|
|
406
|
+
})
|
|
407
|
+
export const toolkit = Toolkit.make(ReadFile)
|
|
408
|
+
|
|
409
|
+
declare const history: Prompt.Prompt
|
|
410
|
+
|
|
411
|
+
export const answer = LanguageModel.generateText({
|
|
412
|
+
prompt: history,
|
|
413
|
+
toolkit,
|
|
414
|
+
toolChoice: "auto"
|
|
415
|
+
})
|
|
416
|
+
```
|
|
417
|
+
|
|
418
|
+
The model may then call a tool instead of answering. If you cannot accept that, rewrite the tool calls and results in the history as text first.
|
|
419
|
+
|
|
420
|
+
[`docs/TODO.md`](https://github.com/wmaurer/effect-ai-amazon-bedrock/blob/main/docs/TODO.md) explains each gap and what was tested against real Bedrock.
|
|
421
|
+
|
|
422
|
+
## Where the package comes from
|
|
423
|
+
|
|
424
|
+
Tom McKenzie wrote the first version of this provider as `packages/ai/amazon-bedrock` in a fork of [`effect-ts/effect`](https://github.com/Effect-TS/effect). Wayne Maurer added tool calls, streaming tool calls, files, citations, prompt caching, and reasoning.
|
|
425
|
+
|
|
426
|
+
The package may go back to Effect as a pull request. Until then it is published here under its own name. Its source follows Effect's lint, format, and TypeScript rules, and it differs from the fork only in the package name.
|
|
427
|
+
|
|
428
|
+
## License
|
|
429
|
+
|
|
430
|
+
MIT. See [`LICENSE`](./LICENSE).
|