diff --git a/.changeset/quiet-baboons-glow.md b/.changeset/quiet-baboons-glow.md new file mode 100644 index 0000000000..7d3efa1bef --- /dev/null +++ b/.changeset/quiet-baboons-glow.md @@ -0,0 +1,5 @@ +--- +"@effect/ai-amazon-bedrock": major +--- + +add @effect/ai-amazon-bedrock provider package, ported from Effect v3 diff --git a/packages/ai/amazon-bedrock/docgen.json b/packages/ai/amazon-bedrock/docgen.json new file mode 100644 index 0000000000..d918a46e20 --- /dev/null +++ b/packages/ai/amazon-bedrock/docgen.json @@ -0,0 +1,21 @@ +{ + "$schema": "../../node_modules/@effect/docgen/schema.json", + "srcLink": "https://github.com/Effect-TS/effect/tree/main/packages/ai/amazon-bedrock/src/", + "exclude": ["src/internal/**/*.ts"], + "tscExecutable": "tsgo", + "examplesCompilerOptions": { + "noEmit": true, + "strict": true, + "skipLibCheck": true, + "moduleResolution": "Bundler", + "module": "ES2022", + "target": "ES2022", + "lib": ["ES2022", "DOM", "DOM.Iterable"], + "rewriteRelativeImportExtensions": true, + "allowImportingTsExtensions": true, + "paths": { + "effect": ["../../../effect/src/index.js"], + "effect/*": ["../../../effect/src/*.js"] + } + } +} diff --git a/packages/ai/amazon-bedrock/package.json b/packages/ai/amazon-bedrock/package.json new file mode 100644 index 0000000000..dca6e8134e --- /dev/null +++ b/packages/ai/amazon-bedrock/package.json @@ -0,0 +1,76 @@ +{ + "name": "@effect/ai-amazon-bedrock", + "version": "4.0.0-beta.47", + "type": "module", + "license": "MIT", + "description": "An Amazon Bedrock provider integration for Effect AI SDK", + "homepage": "https://effect.website", + "repository": { + "type": "git", + "url": "https://github.com/Effect-TS/effect-smol.git", + "directory": "packages/ai/amazon-bedrock" + }, + "bugs": { + "url": "https://github.com/Effect-TS/effect-smol/issues" + }, + "tags": [ + "typescript", + "ai", + "amazon-bedrock", + "aws" + ], + "keywords": [ + "typescript", + "ai", + "amazon-bedrock", + "aws" + ], + "sideEffects": [], + "exports": { + "./package.json": "./package.json", + ".": "./src/index.ts", + "./*": "./src/*.ts", + "./internal/*": null, + "./*/index": null + }, + "files": [ + "src/**/*.ts", + "dist/**/*.js", + "dist/**/*.js.map", + "dist/**/*.d.ts", + "dist/**/*.d.ts.map" + ], + "publishConfig": { + "access": "public", + "provenance": true, + "exports": { + "./package.json": "./package.json", + ".": "./dist/index.js", + "./*": "./dist/*.js", + "./internal/*": null, + "./*/index": null + } + }, + "scripts": { + "codegen": "effect-utils codegen", + "build": "tsc -b tsconfig.json && pnpm babel", + "build:tsgo": "tsgo -b tsconfig.json && pnpm babel", + "babel": "babel dist --plugins annotate-pure-calls --out-dir dist --source-maps", + "check": "tsc -b tsconfig.json", + "test": "vitest", + "coverage": "vitest --coverage" + }, + "dependencies": { + "aws4fetch": "^1.0.20", + "@smithy/eventstream-codec": "^4.0.2", + "@smithy/util-utf8": "^4.0.0" + }, + "devDependencies": { + "@effect/ai-anthropic": "workspace:^", + "effect": "workspace:^" + }, + "peerDependencies": { + "@effect/ai-anthropic": "workspace:^", + "effect": "workspace:^" + } +} diff --git a/packages/ai/amazon-bedrock/src/AmazonBedrockClient.ts b/packages/ai/amazon-bedrock/src/AmazonBedrockClient.ts new file mode 100644 index 0000000000..ff653493c7 --- /dev/null +++ b/packages/ai/amazon-bedrock/src/AmazonBedrockClient.ts @@ -0,0 +1,302 @@ +/** + * @since 1.0.0 + */ +import { AwsV4Signer } from "aws4fetch" +import * as Arr from "effect/Array" +import type * as Config from "effect/Config" +import * as Context from "effect/Context" +import * as Effect from "effect/Effect" +import { identity } from "effect/Function" +import * as Layer from "effect/Layer" +import * as Predicate from "effect/Predicate" +import * as Redacted from "effect/Redacted" +import type * as Schema from "effect/Schema" +import * as Stream from "effect/Stream" +import type * as AiError from "effect/unstable/ai/AiError" +import * as Headers from "effect/unstable/http/Headers" +import * as HttpBody from "effect/unstable/http/HttpBody" +import * as HttpClient from "effect/unstable/http/HttpClient" +import * as HttpClientError from "effect/unstable/http/HttpClientError" +import * as HttpClientRequest from "effect/unstable/http/HttpClientRequest" +import * as HttpClientResponse from "effect/unstable/http/HttpClientResponse" +import { AmazonBedrockConfig } from "./AmazonBedrockConfig.ts" +import type { ConverseRequest } from "./AmazonBedrockSchema.ts" +import { ConverseResponse, ConverseResponseStreamEvent } from "./AmazonBedrockSchema.ts" +import * as EventStreamEncoding from "./EventStreamEncoding.ts" +import * as Errors from "./internal/errors.ts" + +const RedactedBedrockHeaders = { + SecurityToken: "X-Amz-Security-Token" +} + +/** + * @since 1.0.0 + * @category services + */ +export class AmazonBedrockClient extends Context.Service()( + "@effect/ai-amazon-bedrock/AmazonBedrockClient" +) {} + +/** + * @since 1.0.0 + * @category models + */ +export interface Service { + readonly client: Client + + readonly streamRequest: ( + request: HttpClientRequest.HttpClientRequest, + schema: Schema.Schema + ) => Stream.Stream + + readonly converse: (options: { + readonly params?: { "anthropic-beta"?: string | undefined } | undefined + readonly payload: typeof ConverseRequest.Encoded + }) => Effect.Effect + + readonly converseStream: (options: { + readonly params?: { "anthropic-beta"?: string | undefined } | undefined + readonly payload: typeof ConverseRequest.Encoded + }) => Stream.Stream +} + +/** + * @since 1.0.0 + * @category constructors + */ +export const make = Effect.fnUntraced( + function*(options: { + readonly apiUrl?: string | undefined + readonly accessKeyId: string + readonly secretAccessKey: Redacted.Redacted + readonly sessionToken?: Redacted.Redacted | undefined + readonly region?: string | undefined + readonly transformClient?: ( + client: HttpClient.HttpClient + ) => HttpClient.HttpClient + }) { + const region = options.region ?? "us-east-1" + + const httpClient = (yield* HttpClient.HttpClient).pipe( + HttpClient.mapRequest((request) => + request.pipe( + HttpClientRequest.prependUrl(options.apiUrl ?? `https://bedrock-runtime.${region}.amazonaws.com`), + HttpClientRequest.acceptJson + ) + ), + HttpClient.mapRequestEffect(Effect.fnUntraced(function*(request) { + const originalHeaders = request.headers + const signer = new AwsV4Signer({ + service: "bedrock", + url: request.url, + method: request.method, + headers: Object.entries(originalHeaders), + body: prepareBody(request.body), + region, + accessKeyId: options.accessKeyId, + secretAccessKey: Redacted.value(options.secretAccessKey), + ...(options.sessionToken ? { sessionToken: Redacted.value(options.sessionToken) } : {}) + }) + const { headers: signedHeaders } = yield* Effect.promise(() => signer.sign()) + const headers = Headers.merge(originalHeaders, Headers.fromInput(signedHeaders)) + return HttpClientRequest.setHeaders(request, headers) + })), + options.transformClient ? options.transformClient : identity + ) + + const httpClientOk = HttpClient.filterStatusOk(httpClient) + + const client = makeClient(httpClient, { + transformClient: (client) => + AmazonBedrockConfig.getOrUndefined.pipe( + Effect.map((config) => config?.transformClient ? config.transformClient(client) : client) + ) + }) + + const converse: Service["converse"] = Effect.fnUntraced( + function*(request) { + return yield* client.converse(request).pipe( + Effect.catchTags({ + HttpClientError: (error: HttpClientError.HttpClientError) => Errors.mapHttpClientError(error, "converse"), + SchemaError: (error: Schema.SchemaError) => Effect.fail(Errors.mapSchemaError(error, "converse")) + }) + ) + } + ) + + const streamRequest = ( + request: HttpClientRequest.HttpClientRequest, + schema: Schema.Schema + ): Stream.Stream => + httpClientOk.execute(request).pipe( + Effect.map((r) => r.stream), + Stream.unwrap, + Stream.pipeThroughChannel(EventStreamEncoding.makeChannel(schema)), + Stream.catchTags({ + HttpClientError: (error: HttpClientError.HttpClientError) => + Stream.unwrap(Errors.mapHttpClientError(error, "streamRequest")), + SchemaError: (error: Schema.SchemaError) => Stream.fail(Errors.mapSchemaError(error, "streamRequest")) + }) + ) as any + + const converseStream: Service["converseStream"] = (options) => { + const { modelId, ...body } = options.payload + const request = HttpClientRequest.post(`/model/${modelId}/converse-stream`, { + headers: Headers.fromInput({ + "anthropic-beta": options.params?.["anthropic-beta"] + }), + body: HttpBody.jsonUnsafe(body) + }) + return streamRequest(request, ConverseResponseStreamEvent) + } + + return AmazonBedrockClient.of({ + client, + streamRequest, + converse, + converseStream + }) + }, + Effect.updateService( + Headers.CurrentRedactedNames, + Arr.appendAll(Object.values(RedactedBedrockHeaders)) + ) +) + +/** + * @since 1.0.0 + * @category layers + */ +export const layer = (options: { + readonly apiUrl?: string | undefined + readonly accessKeyId: string + readonly secretAccessKey: Redacted.Redacted + readonly sessionToken?: Redacted.Redacted | undefined + readonly region?: string | undefined + readonly transformClient?: ( + client: HttpClient.HttpClient + ) => HttpClient.HttpClient +}): Layer.Layer => Layer.effect(AmazonBedrockClient, make(options)) + +/** + * @since 1.0.0 + * @category layers + */ +export const layerConfig = ( + options: { + readonly apiUrl?: Config.Config | undefined + readonly accessKeyId: Config.Config + readonly secretAccessKey: Config.Config + readonly sessionToken?: Config.Config | undefined + readonly region?: Config.Config | undefined + readonly transformClient?: ( + client: HttpClient.HttpClient + ) => HttpClient.HttpClient + } +): Layer.Layer => + Layer.effect( + AmazonBedrockClient, + Effect.gen(function*() { + const apiUrl = Predicate.isNotUndefined(options.apiUrl) + ? yield* options.apiUrl + : undefined + const accessKeyId = yield* options.accessKeyId + const secretAccessKey = yield* options.secretAccessKey + const sessionToken = Predicate.isNotUndefined(options.sessionToken) + ? yield* options.sessionToken + : undefined + const region = Predicate.isNotUndefined(options.region) + ? yield* options.region + : undefined + return yield* make({ + apiUrl, + accessKeyId, + secretAccessKey, + sessionToken, + region, + ...(options.transformClient ? { transformClient: options.transformClient } : undefined) + }) + }) + ) + +// ============================================================================= +// Client +// ============================================================================= + +/** + * @since 1.0.0 + * @category models + */ +export interface Client { + readonly converse: (options: { + readonly params?: { "anthropic-beta"?: string | undefined } | undefined + readonly payload: typeof ConverseRequest.Encoded + }) => Effect.Effect +} + +const makeClient = ( + httpClient: HttpClient.HttpClient, + options: { + readonly transformClient?: ((client: HttpClient.HttpClient) => Effect.Effect) | undefined + } +): Client => { + const unexpectedStatus = (response: HttpClientResponse.HttpClientResponse) => + Effect.flatMap( + Effect.orElseSucceed(response.json, () => "Unexpected status code"), + (description) => + Effect.fail( + new HttpClientError.HttpClientError({ + reason: new HttpClientError.StatusCodeError({ + request: response.request, + response, + description: typeof description === "string" ? description : JSON.stringify(description) + }) + }) + ) + ) + const withResponse: ( + f: (response: HttpClientResponse.HttpClientResponse) => Effect.Effect + ) => ( + request: HttpClientRequest.HttpClientRequest + ) => Effect.Effect = options.transformClient + ? (f) => (request) => + Effect.flatMap( + Effect.flatMap(options.transformClient!(httpClient), (client) => client.execute(request)), + f + ) + : (f) => (request) => Effect.flatMap(httpClient.execute(request), f) + const decodeSuccess = (schema: Schema.Schema) => (response: HttpClientResponse.HttpClientResponse) => + HttpClientResponse.schemaBodyJson(schema)(response) + return { + converse: ({ params, payload: { modelId, ...payload } }) => + HttpClientRequest.post(`/model/${modelId}/converse`).pipe( + HttpClientRequest.setHeaders({ + "anthropic-beta": params?.["anthropic-beta"] ?? undefined + }), + HttpClientRequest.bodyJsonUnsafe(payload), + withResponse(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(ConverseResponse), + orElse: unexpectedStatus + })) + ) as any + } +} + +const prepareBody = (body: HttpBody.HttpBody): string => { + switch (body._tag) { + case "Raw": + case "Uint8Array": { + if (typeof body.body === "string") { + return body.body + } + if (body.body instanceof Uint8Array) { + return new TextDecoder().decode(body.body) + } + if (body.body instanceof ArrayBuffer) { + return new TextDecoder().decode(body.body) + } + return JSON.stringify(body.body) + } + } + throw new Error("Unsupported HttpBody: " + body._tag) +} diff --git a/packages/ai/amazon-bedrock/src/AmazonBedrockConfig.ts b/packages/ai/amazon-bedrock/src/AmazonBedrockConfig.ts new file mode 100644 index 0000000000..06390869f7 --- /dev/null +++ b/packages/ai/amazon-bedrock/src/AmazonBedrockConfig.ts @@ -0,0 +1,56 @@ +/** + * @since 1.0.0 + */ +import * as Context from "effect/Context" +import * as Effect from "effect/Effect" +import { dual } from "effect/Function" +import type { HttpClient } from "effect/unstable/http/HttpClient" + +/** + * @since 1.0.0 + * @category services + */ +export class AmazonBedrockConfig extends Context.Service< + AmazonBedrockConfig, + AmazonBedrockConfig.Service +>()("@effect/ai-amazon-bedrock/AmazonBedrockConfig") { + /** + * @since 1.0.0 + */ + static readonly getOrUndefined: Effect.Effect = Effect.map( + Effect.context(), + (context) => context.mapUnsafe.get(AmazonBedrockConfig.key) + ) +} + +/** + * @since 1.0.0 + */ +export declare namespace AmazonBedrockConfig { + /** + * @since 1.0.0 + * @category models + */ + export interface Service { + readonly transformClient?: (client: HttpClient) => HttpClient + } +} + +/** + * @since 1.0.0 + * @category configuration + */ +export const withClientTransform: { + (transform: (client: HttpClient) => HttpClient): (self: Effect.Effect) => Effect.Effect + (self: Effect.Effect, transform: (client: HttpClient) => HttpClient): Effect.Effect +} = dual< + (transform: (client: HttpClient) => HttpClient) => (self: Effect.Effect) => Effect.Effect, + (self: Effect.Effect, transform: (client: HttpClient) => HttpClient) => Effect.Effect +>( + 2, + (self, transformClient) => + Effect.flatMap( + AmazonBedrockConfig.getOrUndefined, + (config) => Effect.provideService(self, AmazonBedrockConfig, { ...config, transformClient }) + ) +) diff --git a/packages/ai/amazon-bedrock/src/AmazonBedrockError.ts b/packages/ai/amazon-bedrock/src/AmazonBedrockError.ts new file mode 100644 index 0000000000..459ed3dda9 --- /dev/null +++ b/packages/ai/amazon-bedrock/src/AmazonBedrockError.ts @@ -0,0 +1,60 @@ +/** + * Amazon Bedrock error metadata augmentation. + * + * Provides Amazon Bedrock-specific metadata fields for AI error types through + * module augmentation, enabling typed access to Bedrock error details. + * + * @since 1.0.0 + */ + +import type { MutableJson } from "effect/Schema" + +/** + * @since 1.0.0 + * @category models + */ +export interface AmazonBedrockErrorMetadata { + readonly [key: string]: MutableJson +} + +declare module "effect/unstable/ai/AiError" { + export interface RateLimitErrorMetadata { + readonly bedrock?: AmazonBedrockErrorMetadata | null + } + + export interface QuotaExhaustedErrorMetadata { + readonly bedrock?: AmazonBedrockErrorMetadata | null + } + + export interface AuthenticationErrorMetadata { + readonly bedrock?: AmazonBedrockErrorMetadata | null + } + + export interface ContentPolicyErrorMetadata { + readonly bedrock?: AmazonBedrockErrorMetadata | null + } + + export interface InvalidRequestErrorMetadata { + readonly bedrock?: AmazonBedrockErrorMetadata | null + } + + export interface InternalProviderErrorMetadata { + readonly bedrock?: AmazonBedrockErrorMetadata | null + } + + export interface InvalidOutputErrorMetadata { + readonly bedrock?: AmazonBedrockErrorMetadata | null + } + + export interface StructuredOutputErrorMetadata { + readonly bedrock?: AmazonBedrockErrorMetadata | null + } + + export interface UnsupportedSchemaErrorMetadata { + readonly bedrock?: AmazonBedrockErrorMetadata | null + } + + export interface UnknownErrorMetadata { + readonly bedrock?: AmazonBedrockErrorMetadata | null + } +} diff --git a/packages/ai/amazon-bedrock/src/AmazonBedrockLanguageModel.ts b/packages/ai/amazon-bedrock/src/AmazonBedrockLanguageModel.ts new file mode 100644 index 0000000000..2dc7a32322 --- /dev/null +++ b/packages/ai/amazon-bedrock/src/AmazonBedrockLanguageModel.ts @@ -0,0 +1,1197 @@ +/** + * @since 1.0.0 + */ +import * as Context from "effect/Context" +import * as DateTime from "effect/DateTime" +import * as Effect from "effect/Effect" +import * as Encoding from "effect/Encoding" +import { dual } from "effect/Function" +import * as Layer from "effect/Layer" +import * as Predicate from "effect/Predicate" +import type { JsonObject } from "effect/Schema" +import * as SchemaAST from "effect/SchemaAST" +import * as Stream from "effect/Stream" +import type { Span } from "effect/Tracer" +import type { Mutable, Simplify } from "effect/Types" +import * as AiError from "effect/unstable/ai/AiError" +import type * as IdGenerator from "effect/unstable/ai/IdGenerator" +import * as LanguageModel from "effect/unstable/ai/LanguageModel" +import * as AiModel from "effect/unstable/ai/Model" +import type * as Prompt from "effect/unstable/ai/Prompt" +import type * as Response from "effect/unstable/ai/Response" +import { addGenAIAnnotations } from "effect/unstable/ai/Telemetry" +import * as Tool from "effect/unstable/ai/Tool" +import { AmazonBedrockClient } from "./AmazonBedrockClient.ts" +import type { + BedrockFoundationModelId, + CachePointBlock, + ContentBlock, + ConverseRequest, + ConverseResponse, + ConverseResponseStreamEvent, + DocumentFormat, + Message, + SystemContentBlock, + Tool as AmazonBedrockTool, + ToolChoice, + ToolConfiguration +} from "./AmazonBedrockSchema.ts" +import { ImageFormat } from "./AmazonBedrockSchema.ts" +import * as InternalUtilities from "./internal/utilities.ts" + +const BEDROCK_CACHE_POINT: { + readonly cachePoint: typeof CachePointBlock.Encoded +} = { cachePoint: { type: "default" } } + +/** + * @since 1.0.0 + * @category models + */ +export type Model = typeof BedrockFoundationModelId.Type + +// ============================================================================= +// Configuration +// ============================================================================= + +/** + * @since 1.0.0 + * @category services + */ +export class Config extends Context.Service()( + "@effect/ai-amazon-bedrock/AmazonBedrockLanguageModel/Config" +) { + /** + * @since 1.0.0 + */ + static readonly getOrUndefined: Effect.Effect = Effect.map( + Effect.context(), + (context) => context.mapUnsafe.get(Config.key) + ) +} + +/** + * @since 1.0.0 + */ +export declare namespace Config { + /** + * @since 1.0.0 + * @category configuration + */ + export interface Service extends + Simplify< + Partial< + Omit< + typeof ConverseRequest.Encoded, + "messages" | "system" | "toolConfig" + > + > + > + {} +} + +// ============================================================================= +// Amazon Bedrock Provider Options / Metadata +// ============================================================================= + +/** + * @since 1.0.0 + * @category provider options + */ +export type AmazonBedrockReasoningInfo = { + readonly type: "thinking" + readonly signature: string +} | { + readonly type: "redacted_thinking" + readonly redactedData: string +} + +declare module "effect/unstable/ai/Prompt" { + export interface SystemMessageOptions extends ProviderOptions { + readonly bedrock?: { + readonly cachePoint?: typeof CachePointBlock.Encoded | null + } | null + } + + export interface UserMessageOptions extends ProviderOptions { + readonly bedrock?: { + readonly cachePoint?: typeof CachePointBlock.Encoded | null + } | null + } + + export interface AssistantMessageOptions extends ProviderOptions { + readonly bedrock?: { + readonly cachePoint?: typeof CachePointBlock.Encoded | null + } | null + } + + export interface ToolMessageOptions extends ProviderOptions { + readonly bedrock?: { + readonly cachePoint?: typeof CachePointBlock.Encoded | null + } | null + } + + export interface ReasoningPartOptions extends ProviderOptions { + readonly bedrock?: AmazonBedrockReasoningInfo | null + } +} + +declare module "effect/unstable/ai/Response" { + export interface ReasoningPartMetadata extends ProviderMetadata { + readonly bedrock?: AmazonBedrockReasoningInfo | null + } + + export interface FinishPartMetadata extends ProviderMetadata { + readonly bedrock?: { + readonly trace?: JsonObject | null + readonly usage: { + readonly cacheWriteInputTokens?: number | null + } + } | null + } +} + +// ============================================================================= +// Amazon Bedrock Language Model +// ============================================================================= + +/** + * @since 1.0.0 + * @category models + */ +export const model = ( + modelName: (string & {}) | Model, + config?: Omit | undefined +): AiModel.Model<"amazon-bedrock", LanguageModel.LanguageModel, AmazonBedrockClient> => + AiModel.make("amazon-bedrock", modelName, layer({ model: modelName, config })) + +/** + * @since 1.0.0 + * @category constructors + */ +export const make = Effect.fnUntraced(function*(options: { + readonly model: (string & {}) | Model + readonly config?: Omit | undefined +}) { + const client = yield* AmazonBedrockClient + + const makeRequest = Effect.fnUntraced( + function*(providerOptions: LanguageModel.ProviderOptions) { + const context = yield* Effect.context() + const config = { modelId: options.model, ...options.config, ...context.mapUnsafe.get(Config.key) } + const { messages, system } = yield* prepareMessages(providerOptions) + const { additionalTools, betas, toolConfig, nameMapper } = yield* prepareTools(providerOptions, config) + const responseFormat = providerOptions.responseFormat + const request: typeof ConverseRequest.Encoded = { + ...config, + system, + messages, + // Handle tool configuration + ...(responseFormat.type === "json" + ? { + toolConfig: { + tools: [{ + toolSpec: { + name: responseFormat.objectName, + description: SchemaAST.resolveDescription(responseFormat.schema.ast) ?? + "Respond with a JSON object", + inputSchema: { + json: Tool.getJsonSchemaFromSchema(responseFormat.schema) as any + } + } + }], + toolChoice: { tool: { name: responseFormat.objectName } } + } + } + : Predicate.isNotUndefined(toolConfig.tools) && (toolConfig.tools as Array).length > 0 + ? { toolConfig } + : {}), + // Handle additional model request fields + ...(Predicate.isNotUndefined(additionalTools) + ? { + additionalModelRequestFields: { + ...config.additionalModelRequestFields, + ...additionalTools + } + } + : {}) + } + return { betas, request, nameMapper } + } + ) + + return yield* LanguageModel.make({ + generateText: Effect.fnUntraced( + function*(options) { + const { betas, request, nameMapper } = yield* makeRequest(options) + annotateRequest(options.span, request) + const anthropicBeta = betas.size > 0 ? Array.from(betas).join(",") : undefined + const rawResponse = yield* client.converse({ + params: anthropicBeta !== undefined ? { "anthropic-beta": anthropicBeta } : undefined, + payload: request + }) + annotateResponse(options.span, request, rawResponse) + return yield* makeResponse(request, rawResponse, options, nameMapper) + } + ), + streamText: Effect.fnUntraced( + function*(options) { + const { betas, request, nameMapper } = yield* makeRequest(options) + annotateRequest(options.span, request) + const anthropicBeta = betas.size > 0 ? Array.from(betas).join(",") : undefined + const stream = client.converseStream({ + params: anthropicBeta !== undefined ? { "anthropic-beta": anthropicBeta } : undefined, + payload: request + }) + return { request, stream, nameMapper } + }, + (effect, options) => + effect.pipe( + Effect.flatMap(({ request, stream, nameMapper }) => makeStreamResponse(request, stream, options, nameMapper)), + Stream.unwrap, + Stream.map((response) => { + annotateStreamResponse(options.span, response) + return response + }) + ) + ) + }) +}) + +/** + * @since 1.0.0 + * @category layers + */ +export const layer = (options: { + readonly model: (string & {}) | Model + readonly config?: Omit | undefined +}): Layer.Layer => + Layer.effect(LanguageModel.LanguageModel, make({ model: options.model, config: options.config })) + +/** + * @since 1.0.0 + * @category configuration + */ +export const withConfigOverride: { + (config: Config.Service): (self: Effect.Effect) => Effect.Effect + (self: Effect.Effect, config: Config.Service): Effect.Effect +} = dual< + (config: Config.Service) => (self: Effect.Effect) => Effect.Effect, + (self: Effect.Effect, config: Config.Service) => Effect.Effect +>(2, (self, overrides) => + Effect.flatMap( + Config.getOrUndefined, + (config) => Effect.provideService(self, Config, { ...config, ...overrides }) + )) + +// ============================================================================= +// Prompt Conversion +// ============================================================================= + +const prepareMessages: (options: LanguageModel.ProviderOptions) => Effect.Effect<{ + readonly system: ReadonlyArray + readonly messages: ReadonlyArray +}, AiError.AiError> = Effect.fnUntraced( + function*(options) { + const groups = groupMessages(options.prompt) + + const system: Array = [] + const messages: Array = [] + + let documentCounter = 0 + const nextDocumentName = () => `document-${++documentCounter}` + + for (let i = 0; i < groups.length; i++) { + const group = groups[i] + const isLastGroup = i === groups.length - 1 + + switch (group.type) { + case "system": { + if (messages.length > 0) { + return yield* AiError.make({ + module: "AmazonBedrockLanguageModel", + method: "prepareMessages", + reason: new AiError.InvalidUserInputError({ + description: "Multiple system messages separated by user / assistant messages" + }) + }) + } + for (const message of group.messages) { + system.push({ text: message.content }) + if (Predicate.isNotUndefined(getCachePoint(message))) { + system.push(BEDROCK_CACHE_POINT) + } + } + break + } + + case "user": { + const content: Array = [] + + for (const message of group.messages) { + switch (message.role) { + case "user": { + for (let j = 0; j < message.content.length; j++) { + const part = message.content[j] + + switch (part.type) { + case "text": { + content.push({ text: part.text }) + break + } + + case "file": { + if (part.data instanceof URL) { + return yield* AiError.make({ + module: "AmazonBedrockLanguageModel", + method: "prepareMessages", + reason: new AiError.InvalidUserInputError({ + description: "File URL inputs are not supported at this time" + }) + }) + } + if (part.mediaType.startsWith("image/")) { + content.push({ + image: { + format: yield* getImageFormat(part.mediaType), + source: { bytes: convertToBase64(part.data) } + } + }) + } else { + content.push({ + document: { + format: yield* getDocumentFormat(part.mediaType), + name: nextDocumentName(), + source: { bytes: convertToBase64(part.data) } + } + }) + } + break + } + } + } + break + } + + case "tool": { + for (const part of message.content) { + if (part.type !== "tool-result") continue + content.push({ + toolResult: { + toolUseId: part.id, + content: [{ text: JSON.stringify(part.result) }] + } + }) + } + break + } + } + + if (getCachePoint(message)) { + content.push(BEDROCK_CACHE_POINT) + } + } + + messages.push({ role: "user", content }) + + break + } + + case "assistant": { + const content: Array = [] + + for (let j = 0; j < group.messages.length; j++) { + const message = group.messages[j] + const isLastMessage = j === group.messages.length - 1 + + for (let k = 0; k < message.content.length; k++) { + const part = message.content[k] + const isLastPart = k === message.content.length - 1 + + switch (part.type) { + case "text": { + // Skip empty text blocks + if (part.text.trim().length === 0) { + break + } + content.push({ + // Amazon Bedrock does not allow trailing whitespace in + // assistant content blocks + text: trimIfLast(isLastGroup, isLastMessage, isLastPart, part.text) + }) + break + } + + case "reasoning": { + const options = part.options.bedrock + if (options != null) { + if (options.type === "thinking") { + content.push({ + reasoningContent: { + reasoningText: { + // Amazon Bedrock does not allow trailing whitespace in + // assistant content blocks + text: trimIfLast(isLastGroup, isLastMessage, isLastPart, part.text), + signature: options.signature + } + } + }) + } + if (options.type === "redacted_thinking") { + content.push({ + reasoningContent: { + redactedContent: options.redactedData + } + }) + } + } + break + } + + case "tool-call": { + content.push({ + toolUse: { + toolUseId: part.id, + name: part.name, + input: part.params + } + }) + break + } + } + } + + if (getCachePoint(message)) { + content.push(BEDROCK_CACHE_POINT) + } + } + + messages.push({ role: "assistant", content }) + + break + } + } + } + + return { system, messages } + } +) + +// ============================================================================= +// Response Conversion +// ============================================================================= + +const makeResponse: ( + request: typeof ConverseRequest.Encoded, + response: ConverseResponse, + options: LanguageModel.ProviderOptions, + nameMapper: Tool.NameMapper> +) => Effect.Effect< + Array, + never, + IdGenerator.IdGenerator +> = Effect.fnUntraced(function*(request, response, options, nameMapper) { + const parts: Array = [] + + parts.push({ + type: "response-metadata", + id: undefined, + modelId: request.modelId, + timestamp: DateTime.formatIso(yield* DateTime.now), + request: undefined + }) + + for (const part of response.output.message.content) { + if ("text" in part) { + if (options.responseFormat.type === "text") { + parts.push({ + type: "text", + text: part.text + }) + } + } else if ("reasoningContent" in part) { + if ("reasoningText" in part.reasoningContent) { + const signature = part.reasoningContent.reasoningText.signature + parts.push({ + type: "reasoning", + text: part.reasoningContent.reasoningText.text, + metadata: Predicate.isNotUndefined(signature) ? + { bedrock: { type: "thinking" as const, signature } } + : undefined + }) + } + if ("redactedContent" in part.reasoningContent) { + parts.push({ + type: "reasoning", + text: "", + metadata: { + bedrock: { + type: "redacted_thinking" as const, + redactedData: part.reasoningContent.redactedContent + } + } + }) + } + } else if ("toolUse" in part) { + if (options.responseFormat.type === "json") { + parts.push({ + type: "text", + text: JSON.stringify(part.toolUse.input) + }) + } else { + const customName = nameMapper.getCustomName(part.toolUse.name) + parts.push({ + type: "tool-call", + id: part.toolUse.toolUseId, + name: customName, + params: part.toolUse.input, + providerExecuted: false + }) + } + } + } + + const finishReason = InternalUtilities.resolveFinishReason(response.stopReason) + const cacheReadTokens = response.usage.cacheReadInputTokens ?? 0 + const cacheWriteTokens = response.usage.cacheWriteInputTokens ?? 0 + + parts.push({ + type: "finish", + reason: finishReason, + usage: { + inputTokens: { + uncached: response.usage.inputTokens, + total: response.usage.inputTokens + cacheReadTokens + cacheWriteTokens, + cacheRead: cacheReadTokens, + cacheWrite: cacheWriteTokens + }, + outputTokens: { + total: response.usage.outputTokens, + text: undefined, + reasoning: undefined + } + }, + response: undefined, + metadata: { + bedrock: { + ...(response.trace !== undefined + ? { trace: response.trace as unknown as JsonObject } + : undefined), + usage: { + ...(response.usage.cacheWriteInputTokens !== undefined + ? { cacheWriteInputTokens: response.usage.cacheWriteInputTokens } + : undefined) + } + } as any + } + }) + + return parts +}) + +const makeStreamResponse: ( + request: typeof ConverseRequest.Encoded, + stream: Stream.Stream, + options: LanguageModel.ProviderOptions, + nameMapper: Tool.NameMapper> +) => Effect.Effect< + Stream.Stream, + never, + IdGenerator.IdGenerator +> = Effect.fnUntraced( + function*(request, stream, options, nameMapper) { + const contentBlocks: Record< + number, + | { + readonly type: "text" + } + | { + readonly type: "reasoning" + } + | { + readonly type: "tool-call" + readonly id: string + readonly name: string + params: string + readonly providerExecuted: boolean + } + > = {} + + let trace: JsonObject | undefined = undefined + let cacheWriteInputTokens: number | undefined = undefined + let finishReason: Response.FinishReason | undefined = undefined + let hasMetadata = false + const usage: Mutable = { + inputTokens: { + uncached: undefined, + total: undefined, + cacheRead: undefined, + cacheWrite: undefined + }, + outputTokens: { + total: undefined, + text: undefined, + reasoning: undefined + } + } + + const tryEmitFinish = (parts: Array) => { + if (finishReason !== undefined && hasMetadata) { + parts.push({ + type: "finish", + reason: finishReason, + usage, + response: undefined, + metadata: { + bedrock: { + ...(trace !== undefined ? { trace } : undefined), + usage: { + ...(cacheWriteInputTokens !== undefined + ? { cacheWriteInputTokens } + : undefined) + } + } as any + } + }) + } + } + + return stream.pipe( + Stream.mapEffect(Effect.fnUntraced(function*(event) { + const parts: Array = [] + + if ("messageStart" in event) { + parts.push({ + type: "response-metadata", + id: undefined, + modelId: request.modelId, + timestamp: DateTime.formatIso(yield* DateTime.now), + request: undefined + }) + } else if ("messageStop" in event) { + finishReason = InternalUtilities.resolveFinishReason(event.messageStop.stopReason) + tryEmitFinish(parts) + } else if ("contentBlockStart" in event) { + const index = event.contentBlockStart.contentBlockIndex + const block = event.contentBlockStart + if (Predicate.isNotUndefined(block.start.toolUse)) { + const toolUse = block.start.toolUse + const toolName = toolUse.name + const customName = nameMapper.getCustomName(toolName) + + contentBlocks[index] = { + type: "tool-call", + id: toolUse.toolUseId, + name: customName, + params: "", + providerExecuted: false + } + if (options.responseFormat.type === "text") { + parts.push({ + type: "tool-params-start", + id: toolUse.toolUseId, + name: toolUse.name, + providerExecuted: false + }) + } + } else { + contentBlocks[index] = { type: "text" } + parts.push({ + type: "text-start", + id: index.toString() + }) + } + } else if ("contentBlockDelta" in event) { + const index = event.contentBlockDelta.contentBlockIndex + const delta = event.contentBlockDelta.delta + + if ("text" in delta) { + const block = contentBlocks[index] + if (Predicate.isUndefined(block)) { + contentBlocks[index] = { type: "text" } + if (options.responseFormat.type === "text") { + parts.push({ + type: "text-start", + id: index.toString() + }) + } + } + if (options.responseFormat.type === "text") { + parts.push({ + type: "text-delta", + id: index.toString(), + delta: delta.text + }) + } + } else if ("reasoningContent" in delta) { + if ("text" in delta.reasoningContent) { + const block = contentBlocks[index] + if (Predicate.isUndefined(block)) { + contentBlocks[index] = { type: "reasoning" } + parts.push({ + type: "reasoning-start", + id: index.toString() + }) + } + parts.push({ + type: "reasoning-delta", + id: index.toString(), + delta: delta.reasoningContent.text + }) + } else if ("signature" in delta.reasoningContent) { + parts.push({ + type: "reasoning-delta", + id: index.toString(), + delta: "", + metadata: { + bedrock: { + type: "thinking" as const, + signature: delta.reasoningContent.signature + } + } + }) + } else { + parts.push({ + type: "reasoning-delta", + id: index.toString(), + delta: "", + metadata: { + bedrock: { + type: "redacted_thinking" as const, + redactedData: delta.reasoningContent.redactedContent + } + } + }) + } + } else if ("toolUse" in delta) { + const block = contentBlocks[index] + if (Predicate.isNotUndefined(block) && block.type === "tool-call") { + const params = delta.toolUse.input + if (options.responseFormat.type === "text") { + parts.push({ + type: "tool-params-delta", + id: block.id, + delta: params + }) + } + block.params += params + } + } + } else if ("contentBlockStop" in event) { + const index = event.contentBlockStop.contentBlockIndex + const block = contentBlocks[index] + if (Predicate.isNotUndefined(block)) { + switch (block.type) { + case "text": { + if (options.responseFormat.type === "text") { + parts.push({ + type: "text-end", + id: index.toString() + }) + } + break + } + + case "reasoning": { + parts.push({ + type: "reasoning-end", + id: index.toString() + }) + break + } + + case "tool-call": { + if (options.responseFormat.type === "text") { + parts.push({ + type: "tool-params-end", + id: block.id + }) + + const toolName = block.name + const toolParams = block.params + + const params = yield* Effect.try({ + try: () => Tool.unsafeSecureJsonParse(toolParams), + catch: () => + AiError.make({ + module: "AmazonBedrockLanguageModel", + method: "makeStreamResponse", + reason: new AiError.InvalidOutputError({ + description: "Failed to securely parse tool call parameters " + + `for tool '${toolName}':\nParameters: ${toolParams}` + }) + }) + }) + + parts.push({ + type: "tool-call", + id: block.id, + name: toolName, + params, + providerExecuted: block.providerExecuted + }) + } else { + parts.push({ + type: "text-start", + id: index.toString() + }) + parts.push({ + type: "text-delta", + id: index.toString(), + delta: block.params + }) + parts.push({ + type: "text-end", + id: index.toString() + }) + } + break + } + } + delete contentBlocks[index] + } + } else if ("metadata" in event) { + const cacheRead = event.metadata.usage.cacheReadInputTokens ?? 0 + const cacheWrite = event.metadata.usage.cacheWriteInputTokens ?? 0 + usage.inputTokens = { + uncached: event.metadata.usage.inputTokens, + total: event.metadata.usage.inputTokens + cacheRead + cacheWrite, + cacheRead, + cacheWrite + } + usage.outputTokens = { + total: event.metadata.usage.outputTokens, + text: undefined, + reasoning: undefined + } + if (Predicate.isNotUndefined(event.metadata.usage.cacheWriteInputTokens)) { + cacheWriteInputTokens = event.metadata.usage.cacheWriteInputTokens + } + if (Predicate.isNotUndefined(event.metadata.trace)) { + trace = event.metadata.trace as unknown as JsonObject + } + hasMetadata = true + tryEmitFinish(parts) + } else if ("internalServerException" in event) { + parts.push({ type: "error", error: event.internalServerException }) + } else if ("modelStreamErrorException" in event) { + parts.push({ type: "error", error: event.modelStreamErrorException }) + } else if ("serviceUnavailableException" in event) { + parts.push({ type: "error", error: event.serviceUnavailableException }) + } else if ("throttlingException" in event) { + parts.push({ type: "error", error: event.throttlingException }) + } else if ("validationException" in event) { + parts.push({ type: "error", error: event.validationException }) + } + + return parts + })), + Stream.flattenIterable + ) + } +) + +// ============================================================================= +// Tool Calling +// ============================================================================= + +// Map of known Anthropic provider tool IDs to their required betas +const anthropicToolBetas: Record = { + "anthropic.bash_20241022": "computer-use-2024-10-22", + "anthropic.bash_20250124": "computer-use-2025-01-24", + "anthropic.computer_use_20241022": "computer-use-2024-10-22", + "anthropic.computer_20250124": "computer-use-2025-01-24", + "anthropic.computer_20251124": "computer-use-2025-11-24", + "anthropic.text_editor_20241022": "computer-use-2024-10-22", + "anthropic.text_editor_20250124": "computer-use-2025-01-24", + "anthropic.text_editor_20250429": "computer-use-2025-01-24", + "anthropic.code_execution_20250522": "code-execution-2025-05-22", + "anthropic.code_execution_20250825": "code-execution-2025-08-25", + "anthropic.memory_20250818": "context-management-2025-06-27", + "anthropic.web_search_20250305": "", + "anthropic.web_fetch_20250910": "web-fetch-2025-09-10", + "anthropic.tool_search_tool_bm25_20251119": "advanced-tool-use-2025-11-20", + "anthropic.tool_search_tool_regex_20251119": "" +} + +const prepareTools: ( + options: LanguageModel.ProviderOptions, + config: Config.Service +) => Effect.Effect<{ + readonly betas: ReadonlySet + readonly toolConfig: Partial + readonly additionalTools?: Record | undefined + readonly nameMapper: Tool.NameMapper> +}, AiError.AiError> = Effect.fnUntraced(function*(options, config) { + const betas = new Set() + const nameMapper = new Tool.NameMapper(options.tools) + + if (options.tools.length === 0) { + return { toolConfig: {}, betas, nameMapper } + } + + const isAnthropicModel = config.modelId!.includes("anthropic.") + const userDefinedTools: Array = [] + const providerDefinedTools: Array = [] + for (const tool of options.tools) { + if (Tool.isUserDefined(tool)) { + userDefinedTools.push(tool) + } else if (Tool.isProviderDefined(tool)) { + providerDefinedTools.push(tool as Tool.AnyProviderDefined) + } + } + + const hasAnthropicTools = isAnthropicModel && providerDefinedTools.length > 0 + + let tools: Array = [] + let additionalTools: Record | undefined = undefined + + // Handle Anthropic provider-defined tools for Anthropic models on Bedrock + if (hasAnthropicTools) { + for (const providerTool of providerDefinedTools) { + // Add required betas + const beta = anthropicToolBetas[providerTool.id] + if (Predicate.isNotUndefined(beta) && beta.length > 0) { + betas.add(beta) + } + + // Add tool definition in Bedrock format + const description = Tool.getDescription(providerTool as any) + tools.push({ + toolSpec: { + name: providerTool.providerName, + ...(description !== undefined ? { description } : undefined), + inputSchema: { + json: Tool.getJsonSchema(providerTool as any) as any + } + } + }) + } + } + + // Handle conversion of user-defined tools to Amazon Bedrock tool definitions + for (const tool of userDefinedTools) { + const description = Tool.getDescription(tool as any) + tools.push({ + toolSpec: { + name: tool.name, + ...(description !== undefined ? { description } : undefined), + inputSchema: { + json: Tool.getJsonSchema(tool as any) as any + } + } + }) + } + + // Handle resolution of tool choice for Amazon Bedrock user-defined tools + let toolChoice: typeof ToolChoice.Encoded | undefined = undefined + if (!hasAnthropicTools && tools.length > 0 && Predicate.isNotUndefined(options.toolChoice)) { + if (options.toolChoice === "none") { + tools.length = 0 + toolChoice = undefined + } else if (options.toolChoice === "auto") { + toolChoice = { auto: {} } + } else if (options.toolChoice === "required") { + toolChoice = { any: {} } + } else if ("tool" in options.toolChoice) { + toolChoice = { tool: { name: options.toolChoice.tool } } + } else { + const allowedTools = new Set(options.toolChoice.oneOf) + tools = tools.filter((tool) => allowedTools.has(tool.toolSpec?.name)) + toolChoice = options.toolChoice.mode === "auto" ? { auto: {} } : { any: {} } + } + } + + const toolConfig: Partial = tools.length > 0 + ? { tools, ...(toolChoice !== undefined ? { toolChoice } : undefined) } + : {} + + return { additionalTools, betas, toolConfig, nameMapper } +}) + +// ============================================================================= +// Telemetry +// ============================================================================= + +const annotateRequest = ( + span: Span, + request: typeof ConverseRequest.Encoded +): void => { + addGenAIAnnotations(span, { + system: "aws.bedrock", + operation: { name: "chat" }, + request: { + model: request.modelId, + temperature: request.inferenceConfig?.temperature, + topP: request.inferenceConfig?.topP, + maxTokens: request.inferenceConfig?.maxTokens, + stopSequences: request.inferenceConfig?.stopSequences ?? [] + } + }) +} + +const annotateResponse = ( + span: Span, + request: typeof ConverseRequest.Encoded, + response: ConverseResponse +): void => { + addGenAIAnnotations(span, { + response: { + model: request.modelId, + finishReasons: response.stopReason ? [response.stopReason] : undefined + }, + usage: { + inputTokens: response.usage.inputTokens, + outputTokens: response.usage.outputTokens + } + }) +} + +const annotateStreamResponse = (span: Span, part: Response.StreamPartEncoded) => { + if (part.type === "response-metadata") { + addGenAIAnnotations(span, { + response: { + id: part.id, + model: part.modelId + } + }) + } + if (part.type === "finish") { + addGenAIAnnotations(span, { + response: { + finishReasons: [part.reason] + }, + usage: { + inputTokens: part.usage.inputTokens?.total, + outputTokens: part.usage.outputTokens?.total + } + }) + } +} + +// ============================================================================= +// Utilities +// ============================================================================= + +type ContentGroup = SystemMessageGroup | AssistantMessageGroup | UserMessageGroup + +interface SystemMessageGroup { + readonly type: "system" + readonly messages: Array +} + +interface AssistantMessageGroup { + readonly type: "assistant" + readonly messages: Array +} + +interface UserMessageGroup { + readonly type: "user" + readonly messages: Array +} + +const groupMessages = (prompt: Prompt.Prompt): Array => { + const messages: Array = [] + let current: ContentGroup | undefined = undefined + for (const message of prompt.content) { + switch (message.role) { + case "system": { + if (current?.type !== "system") { + current = { type: "system", messages: [] } + messages.push(current) + } + current.messages.push(message) + break + } + case "assistant": { + if (current?.type !== "assistant") { + current = { type: "assistant", messages: [] } + messages.push(current) + } + current.messages.push(message) + break + } + case "tool": + case "user": { + if (current?.type !== "user") { + current = { type: "user", messages: [] } + messages.push(current) + } + current.messages.push(message) + break + } + } + } + return messages +} + +const trimIfLast = ( + isLastGroup: boolean, + isLastMessage: boolean, + isLastPart: boolean, + text: string +) => isLastGroup && isLastMessage && isLastPart ? text.trim() : text + +const getCachePoint = ( + part: + | Prompt.SystemMessage + | Prompt.UserMessage + | Prompt.AssistantMessage + | Prompt.ToolMessage +): typeof CachePointBlock.Encoded | undefined => part.options.bedrock?.cachePoint ?? undefined + +const convertToBase64 = (data: string | Uint8Array): string => + typeof data === "string" ? data : Encoding.encodeBase64(data) + +const DOCUMENT_MIME_TYPES: Record = { + "application/pdf": "pdf", + "text/csv": "csv", + "application/msword": "doc", + "application/vnd.openxmlformats-officedocument.wordprocessingml.document": "docx", + "application/vnd.ms-excel": "xls", + "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet": "xlsx", + "text/html": "html", + "text/plain": "txt", + "text/markdown": "md" +} + +const getDocumentFormat: ( + mediaType: string +) => Effect.Effect = Effect.fnUntraced( + function*(mediaType) { + const format = DOCUMENT_MIME_TYPES[mediaType] + + if (Predicate.isUndefined(format)) { + return yield* AiError.make({ + module: "AmazonBedrockLanguageModel", + method: "getDocumentFormat", + reason: new AiError.InvalidUserInputError({ + description: `Unsupported document MIME type: ${mediaType} - expected ` + + `one of: ${Object.keys(DOCUMENT_MIME_TYPES)}` + }) + }) + } + + return format + } +) + +const getImageFormat: ( + mediaType: string +) => Effect.Effect = Effect.fnUntraced( + function*(mediaType) { + const format = ImageFormat.literals.find((format: string) => mediaType === `image/${format}`) + + if (Predicate.isUndefined(format)) { + return yield* AiError.make({ + module: "AmazonBedrockLanguageModel", + method: "getImageFormat", + reason: new AiError.InvalidUserInputError({ + description: `Unsupported image MIME type: ${mediaType} - expected ` + + `one of: ${ImageFormat.literals.map((format: string) => `image/${format}`).join(",")}` + }) + }) + } + + return format + } +) diff --git a/packages/ai/amazon-bedrock/src/AmazonBedrockSchema.ts b/packages/ai/amazon-bedrock/src/AmazonBedrockSchema.ts new file mode 100644 index 0000000000..95cbc95748 --- /dev/null +++ b/packages/ai/amazon-bedrock/src/AmazonBedrockSchema.ts @@ -0,0 +1,1101 @@ +/** + * @since 1.0.0 + */ +import * as Schema from "effect/Schema" + +const prefix = "@effect/ai-amazon-bedrock" + +const makeIdentifier = (name: string) => `${prefix}/${name}` + +/** + * The foundation models supported by Amazon Bedrock. + * + * An up-to-date list can be generated by sampling multiple regions and + * deduplicating. Regions are ordered by rough estimated popularity/volume: + * + * ```sh + * for region in us-east-1 us-west-2 eu-west-1 ap-northeast-1 ap-southeast-1 eu-central-1 ap-south-1 us-east-2; do + * aws bedrock list-foundation-models --region "$region" --output json + * done | jq -s '[.[].modelSummaries[].modelId] | unique | sort' + * ``` + * + * @since 1.0.0 + * @category schemas + */ +export const BedrockFoundationModelId = Schema.Literals([ + "ai21.jamba-1-5-large-v1:0", + "ai21.jamba-1-5-mini-v1:0", + "amazon.nova-2-lite-v1:0", + "amazon.nova-2-lite-v1:0:256k", + "amazon.nova-2-multimodal-embeddings-v1:0", + "amazon.nova-2-sonic-v1:0", + "amazon.nova-canvas-v1:0", + "amazon.nova-lite-v1:0", + "amazon.nova-lite-v1:0:24k", + "amazon.nova-lite-v1:0:300k", + "amazon.nova-micro-v1:0", + "amazon.nova-micro-v1:0:128k", + "amazon.nova-micro-v1:0:24k", + "amazon.nova-premier-v1:0", + "amazon.nova-premier-v1:0:1000k", + "amazon.nova-premier-v1:0:20k", + "amazon.nova-premier-v1:0:8k", + "amazon.nova-premier-v1:0:mm", + "amazon.nova-pro-v1:0", + "amazon.nova-pro-v1:0:24k", + "amazon.nova-pro-v1:0:300k", + "amazon.nova-reel-v1:0", + "amazon.nova-reel-v1:1", + "amazon.nova-sonic-v1:0", + "amazon.rerank-v1:0", + "amazon.titan-embed-g1-text-02", + "amazon.titan-embed-image-v1", + "amazon.titan-embed-image-v1:0", + "amazon.titan-embed-text-v1", + "amazon.titan-embed-text-v1:2:8k", + "amazon.titan-embed-text-v2:0", + "amazon.titan-embed-text-v2:0:8k", + "amazon.titan-image-generator-v2:0", + "amazon.titan-tg1-large", + "anthropic.claude-3-5-haiku-20241022-v1:0", + "anthropic.claude-3-5-sonnet-20240620-v1:0", + "anthropic.claude-3-5-sonnet-20241022-v2:0", + "anthropic.claude-3-7-sonnet-20250219-v1:0", + "anthropic.claude-3-haiku-20240307-v1:0", + "anthropic.claude-3-haiku-20240307-v1:0:200k", + "anthropic.claude-3-haiku-20240307-v1:0:48k", + "anthropic.claude-3-sonnet-20240229-v1:0", + "anthropic.claude-3-sonnet-20240229-v1:0:200k", + "anthropic.claude-3-sonnet-20240229-v1:0:28k", + "anthropic.claude-haiku-4-5-20251001-v1:0", + "anthropic.claude-opus-4-1-20250805-v1:0", + "anthropic.claude-opus-4-20250514-v1:0", + "anthropic.claude-opus-4-5-20251101-v1:0", + "anthropic.claude-opus-4-6-v1", + "anthropic.claude-sonnet-4-20250514-v1:0", + "anthropic.claude-sonnet-4-5-20250929-v1:0", + "anthropic.claude-sonnet-4-6", + "cohere.command-r-plus-v1:0", + "cohere.command-r-v1:0", + "cohere.embed-english-v3", + "cohere.embed-english-v3:0:512", + "cohere.embed-multilingual-v3", + "cohere.embed-multilingual-v3:0:512", + "cohere.embed-v4:0", + "cohere.rerank-v3-5:0", + "deepseek.r1-v1:0", + "deepseek.v3-v1:0", + "deepseek.v3.2", + "google.gemma-3-12b-it", + "google.gemma-3-27b-it", + "google.gemma-3-4b-it", + "luma.ray-v2:0", + "meta.llama3-1-405b-instruct-v1:0", + "meta.llama3-1-70b-instruct-v1:0", + "meta.llama3-1-70b-instruct-v1:0:128k", + "meta.llama3-1-8b-instruct-v1:0", + "meta.llama3-1-8b-instruct-v1:0:128k", + "meta.llama3-2-11b-instruct-v1:0", + "meta.llama3-2-11b-instruct-v1:0:128k", + "meta.llama3-2-1b-instruct-v1:0", + "meta.llama3-2-1b-instruct-v1:0:128k", + "meta.llama3-2-3b-instruct-v1:0", + "meta.llama3-2-3b-instruct-v1:0:128k", + "meta.llama3-2-90b-instruct-v1:0", + "meta.llama3-2-90b-instruct-v1:0:128k", + "meta.llama3-3-70b-instruct-v1:0", + "meta.llama3-3-70b-instruct-v1:0:128k", + "meta.llama3-70b-instruct-v1:0", + "meta.llama3-8b-instruct-v1:0", + "meta.llama4-maverick-17b-instruct-v1:0", + "meta.llama4-scout-17b-instruct-v1:0", + "minimax.minimax-m2", + "minimax.minimax-m2.1", + "minimax.minimax-m2.5", + "mistral.devstral-2-123b", + "mistral.magistral-small-2509", + "mistral.ministral-3-14b-instruct", + "mistral.ministral-3-3b-instruct", + "mistral.ministral-3-8b-instruct", + "mistral.mistral-7b-instruct-v0:2", + "mistral.mistral-large-2402-v1:0", + "mistral.mistral-large-2407-v1:0", + "mistral.mistral-large-3-675b-instruct", + "mistral.mistral-small-2402-v1:0", + "mistral.mixtral-8x7b-instruct-v0:1", + "mistral.pixtral-large-2502-v1:0", + "mistral.voxtral-mini-3b-2507", + "mistral.voxtral-small-24b-2507", + "moonshot.kimi-k2-thinking", + "moonshotai.kimi-k2.5", + "nvidia.nemotron-nano-12b-v2", + "nvidia.nemotron-nano-3-30b", + "nvidia.nemotron-nano-9b-v2", + "nvidia.nemotron-super-3-120b", + "openai.gpt-oss-120b-1:0", + "openai.gpt-oss-20b-1:0", + "openai.gpt-oss-safeguard-120b", + "openai.gpt-oss-safeguard-20b", + "qwen.qwen3-235b-a22b-2507-v1:0", + "qwen.qwen3-32b-v1:0", + "qwen.qwen3-coder-30b-a3b-v1:0", + "qwen.qwen3-coder-480b-a35b-v1:0", + "qwen.qwen3-coder-next", + "qwen.qwen3-next-80b-a3b", + "qwen.qwen3-vl-235b-a22b", + "stability.sd3-5-large-v1:0", + "stability.stable-conservative-upscale-v1:0", + "stability.stable-creative-upscale-v1:0", + "stability.stable-fast-upscale-v1:0", + "stability.stable-image-control-sketch-v1:0", + "stability.stable-image-control-structure-v1:0", + "stability.stable-image-core-v1:1", + "stability.stable-image-erase-object-v1:0", + "stability.stable-image-inpaint-v1:0", + "stability.stable-image-remove-background-v1:0", + "stability.stable-image-search-recolor-v1:0", + "stability.stable-image-search-replace-v1:0", + "stability.stable-image-style-guide-v1:0", + "stability.stable-image-ultra-v1:1", + "stability.stable-outpaint-v1:0", + "stability.stable-style-transfer-v1:0", + "twelvelabs.marengo-embed-2-7-v1:0", + "twelvelabs.marengo-embed-3-0-v1:0", + "twelvelabs.pegasus-1-2-v1:0", + "writer.palmyra-vision-7b", + "writer.palmyra-x4-v1:0", + "writer.palmyra-x5-v1:0", + "zai.glm-4.7", + "zai.glm-4.7-flash", + "zai.glm-5" +]) + +/** + * @since 1.0.0 + * @category schemas + */ +export type BedrockFoundationModelId = typeof BedrockFoundationModelId.Type + +/** + * @since 1.0.0 + * @category schemas + */ +export class CachePointBlock extends Schema.Class(makeIdentifier("CachePointBlock"))({ + type: Schema.Literal("default") +}) {} + +/** + * @since 1.0.0 + * @category schemas + */ +export const DocumentFormat = Schema.Literals([ + "csv", + "doc", + "docx", + "html", + "md", + "pdf", + "txt", + "xls", + "xlsx" +]) +/** + * @since 1.0.0 + * @category schemas + */ +export type DocumentFormat = typeof DocumentFormat.Type + +/** + * @since 1.0.0 + * @category schemas + */ +export class DocumentBlock extends Schema.Class(makeIdentifier("DocumentBlock"))({ + name: Schema.String + .check(Schema.isPattern(/^[a-zA-Z0-9()[\]-]+(?: [a-zA-Z0-9()[\]-]+)*$/)) + .check(Schema.isMinLength(1)) + .check(Schema.isMaxLength(200)), + format: DocumentFormat, + source: Schema.Struct({ + bytes: Schema.NonEmptyString + }) +}) {} + +/** + * @since 1.0.0 + * @category schemas + */ +export class GuardrailConverseImageBlock extends Schema.Class( + makeIdentifier("GuardrailConverseImageBlock") +)({ + format: Schema.Literals(["png", "jpg"]), + source: Schema.Struct({ + bytes: Schema.NonEmptyString + }) +}) {} + +/** + * @since 1.0.0 + * @category schemas + */ +export class GuardrailConverseTextBlock extends Schema.Class( + makeIdentifier("GuardrailConverseTextBlock") +)({ + text: Schema.String, + qualifiers: Schema.optionalKey(Schema.Array(Schema.Literals(["guard_content", "grounding_source", "query"]))) +}) {} + +/** + * @since 1.0.0 + * @category schemas + */ +export const GuardrailConverseContentBlock = Schema.Union([ + GuardrailConverseImageBlock, + GuardrailConverseTextBlock +]) +/** + * @since 1.0.0 + * @category schemas + */ +export type GuardrailConverseContentBlock = typeof GuardrailConverseContentBlock.Type + +/** + * @since 1.0.0 + * @category schemas + */ +export const ImageFormat = Schema.Literals(["gif", "jpeg", "png", "webp"]) +/** + * @since 1.0.0 + * @category schemas + */ +export type ImageFormat = typeof ImageFormat.Type + +/** + * @since 1.0.0 + * @category schemas + */ +export class ImageBlock extends Schema.Class(makeIdentifier("ImageBlock"))({ + format: ImageFormat, + source: Schema.Struct({ + bytes: Schema.NonEmptyString + }) +}) {} + +/** + * @since 1.0.0 + * @category schemas + */ +export class JsonBlock extends Schema.Class(makeIdentifier("JsonBlock"))({ + json: Schema.Unknown +}) {} + +/** + * @since 1.0.0 + * @category schemas + */ +export const ReasoningContentBlock = Schema.Union([ + Schema.Struct({ + reasoningText: Schema.Struct({ + text: Schema.String, + signature: Schema.optionalKey(Schema.String) + }) + }), + Schema.Struct({ + redactedContent: Schema.String + }) +]) +/** + * @since 1.0.0 + * @category schemas + */ +export type ReasoningContentBlock = typeof ReasoningContentBlock.Type + +/** + * @since 1.0.0 + * @category schemas + */ +export class VideoBlock extends Schema.Class(makeIdentifier("VideoBlock"))({ + format: Schema.Literals(["flv", "mkv", "mov", "mp4", "mpg", "mpeg", "three_gp", "webm"]), + source: Schema.Union([ + Schema.Struct({ + bytes: Schema.NonEmptyString + }), + Schema.Struct({ + s3Location: Schema.Struct({ + uri: Schema.String + .check(Schema.isPattern(/^s3:\/\/[a-z0-9][.\-a-z0-9]{1,61}[a-z0-9](\/.*)?$/)) + .check(Schema.isMinLength(1)) + .check(Schema.isMaxLength(1024)), + bucketOwner: Schema.optionalKey( + Schema.String.check(Schema.isPattern(/^[0-9]{12}$/)) + ) + }) + }) + ]) +}) {} + +/** + * @since 1.0.0 + * @category schemas + */ +export class ToolResultBlock extends Schema.Class(makeIdentifier("ToolResultBlock"))({ + content: Schema.Array(Schema.Union([ + Schema.Struct({ document: DocumentBlock }), + Schema.Struct({ image: ImageBlock }), + Schema.Struct({ text: Schema.String }), + Schema.Struct({ json: JsonBlock }), + Schema.Struct({ video: VideoBlock }) + ])), + toolUseId: Schema.String + .check(Schema.isPattern(/^[a-zA-Z0-9_-]+$/)) + .check(Schema.isMinLength(1)) + .check(Schema.isMaxLength(64)), + status: Schema.optionalKey(Schema.Literals(["success", "error"])) +}) {} + +/** + * @since 1.0.0 + * @category schemas + */ +export class ToolUseBlock extends Schema.Class(makeIdentifier("ToolUseBlock"))({ + name: Schema.String + .check(Schema.isPattern(/^[a-zA-Z0-9_-]+$/)) + .check(Schema.isMinLength(1)) + .check(Schema.isMaxLength(64)), + input: Schema.Unknown, + toolUseId: Schema.String + .check(Schema.isPattern(/^[a-zA-Z0-9_-]+$/)) + .check(Schema.isMinLength(1)) + .check(Schema.isMaxLength(64)) +}) {} + +/** + * @since 1.0.0 + * @category schemas + */ +export const ContentBlock = Schema.Union([ + Schema.Struct({ cachePoint: CachePointBlock }), + Schema.Struct({ document: DocumentBlock }), + Schema.Struct({ guardContent: GuardrailConverseContentBlock }), + Schema.Struct({ image: ImageBlock }), + Schema.Struct({ reasoningContent: ReasoningContentBlock }), + Schema.Struct({ text: Schema.String }), + Schema.Struct({ toolResult: ToolResultBlock }), + Schema.Struct({ toolUse: ToolUseBlock }), + Schema.Struct({ video: VideoBlock }) +]) +/** + * @since 1.0.0 + * @category schemas + */ +export type ContentBlock = typeof ContentBlock.Type + +/** + * @since 1.0.0 + * @category schemas + */ +export class Message extends Schema.Class(makeIdentifier("Message"))({ + role: Schema.Literals(["user", "assistant"]), + content: Schema.Array(ContentBlock) +}) {} + +/** + * @since 1.0.0 + * @category schemas + */ +export class ConverseOutput extends Schema.Class(makeIdentifier("ConverseOutput"))({ + message: Message +}) {} + +/** + * @since 1.0.0 + * @category schemas + */ +export class ConverseMetrics extends Schema.Class(makeIdentifier("ConverseMetrics"))({ + latencyMs: Schema.DurationFromMillis +}) {} + +/** + * @since 1.0.0 + * @category schemas + */ +export class GuardrailContentFilter extends Schema.Class( + makeIdentifier("GuardrailContentFilter") +)({ + type: Schema.Literals(["HATE", "INSULTS", "MISCONDUCT", "PROMPT_ATTACK", "SEXUAL", "VIOLENCE"]), + action: Schema.Literals(["BLOCKED", "NONE"]), + confidence: Schema.Literals(["NONE", "LOW", "MEDIUM", "HIGH"]), + detected: Schema.optionalKey(Schema.Boolean), + filterStrength: Schema.optionalKey(Schema.Literals(["NONE", "LOW", "MEDIUM", "HIGH"])) +}) {} + +/** + * @since 1.0.0 + * @category schemas + */ +export class GuardrailContentPolicyAssessment extends Schema.Class( + makeIdentifier("GuardrailContentPolicyAssessment") +)({ + filters: Schema.Array(GuardrailContentFilter) +}) {} + +/** + * @since 1.0.0 + * @category schemas + */ +export class GuardrailContextualGroundingFilter extends Schema.Class( + makeIdentifier("GuardrailContextualGroundingFilter") +)({ + type: Schema.Literals(["GROUNDING", "RELEVANCE"]), + action: Schema.Literals(["BLOCKED", "NONE"]), + score: Schema.Number.check(Schema.isBetween({ minimum: 0, maximum: 1 })), + threshold: Schema.Number.check(Schema.isBetween({ minimum: 0, maximum: 1 })), + detected: Schema.optionalKey(Schema.Boolean) +}) {} + +/** + * @since 1.0.0 + * @category schemas + */ +export class GuardrailContextualGroundingPolicyAssessment + extends Schema.Class( + makeIdentifier("GuardrailContextualGroundingPolicyAssessment") + )({ + filters: Schema.optionalKey(Schema.Array(GuardrailContextualGroundingFilter)) + }) +{} + +/** + * @since 1.0.0 + * @category schemas + */ +export class GuardrailImageCoverage extends Schema.Class( + makeIdentifier("GuardrailImageCoverage") +)({ + guarded: Schema.optionalKey(Schema.Int), + total: Schema.optionalKey(Schema.Int) +}) {} + +/** + * @since 1.0.0 + * @category schemas + */ +export class GuardrailTextCharactersCoverage extends Schema.Class( + makeIdentifier("GuardrailTextCharactersCoverage") +)({ + guarded: Schema.optionalKey(Schema.Int), + total: Schema.optionalKey(Schema.Int) +}) {} + +/** + * @since 1.0.0 + * @category schemas + */ +export class GuardrailCoverage extends Schema.Class(makeIdentifier("GuardrailCoverage"))({ + images: Schema.optionalKey(GuardrailImageCoverage), + textCharacters: Schema.optionalKey(GuardrailTextCharactersCoverage) +}) {} + +/** + * @since 1.0.0 + * @category schemas + */ +export class GuardrailUsage extends Schema.Class(makeIdentifier("GuardrailUsage"))({ + contentPolicyUnits: Schema.Int, + contextualGroundingPolicyUnits: Schema.Int, + sensitiveInformationPolicyFreeUnits: Schema.Int, + sensitiveInformationPolicyUnits: Schema.Int, + topicPolicyUnits: Schema.Int, + wordPolicyUnits: Schema.Int, + contentPolicyImageUnits: Schema.optionalKey(Schema.Int) +}) {} + +/** + * @since 1.0.0 + * @category schemas + */ +export class GuardrailInvocationMetrics extends Schema.Class( + makeIdentifier("GuardrailInvocationMetrics") +)({ + guardrailCoverage: Schema.optionalKey(GuardrailCoverage), + guardrailProcessingLatency: Schema.optionalKey(Schema.Number), + usage: Schema.optionalKey(GuardrailUsage) +}) {} + +/** + * @since 1.0.0 + * @category schemas + */ +export class GuardrailPiiEntityFilter extends Schema.Class( + makeIdentifier("GuardrailPiiEntityFilter") +)({ + type: Schema.Literals([ + "ADDRESS", + "AGE", + "AWS_ACCESS_KEY", + "AWS_SECRET_KEY", + "CA_HEALTH_NUMBER", + "CA_SOCIAL_INSURANCE_NUMBER", + "CREDIT_DEBIT_CARD_CVV", + "CREDIT_DEBIT_CARD_EXPIRY", + "CREDIT_DEBIT_CARD_NUMBER", + "DRIVER_ID", + "EMAIL", + "INTERNATIONAL_BANK_ACCOUNT_NUMBER", + "IP_ADDRESS", + "LICENSE_PLATE", + "MAC_ADDRESS", + "NAME", + "PASSWORD", + "PHONE", + "PIN", + "SWIFT_CODE", + "UK_NATIONAL_HEALTH_SERVICE_NUMBER", + "UK_NATIONAL_INSURANCE_NUMBER", + "UK_UNIQUE_TAXPAYER_REFERENCE_NUMBER", + "URL", + "USERNAME", + "US_BANK_ACCOUNT_NUMBER", + "US_BANK_ROUTING_NUMBER", + "US_INDIVIDUAL_TAX_IDENTIFICATION_NUMBER", + "US_PASSPORT_NUMBER", + "US_SOCIAL_SECURITY_NUMBER", + "VEHICLE_IDENTIFICATION_NUMBER" + ]), + action: Schema.Literals(["ANONYMIZED", "BLOCKED", "NONE"]), + match: Schema.String, + detected: Schema.optionalKey(Schema.Boolean) +}) {} + +/** + * @since 1.0.0 + * @category schemas + */ +export class GuardrailRegexFilter extends Schema.Class( + makeIdentifier("GuardrailRegexFilter") +)({ + action: Schema.Literals(["ANONYMIZED", "BLOCKED", "NONE"]), + name: Schema.optionalKey(Schema.String), + match: Schema.optionalKey(Schema.String), + regex: Schema.optionalKey(Schema.String), + detected: Schema.optionalKey(Schema.Boolean) +}) {} + +/** + * @since 1.0.0 + * @category schemas + */ +export class GuardrailSensitiveInformationPolicyAssessment + extends Schema.Class( + makeIdentifier("GuardrailSensitiveInformationPolicyAssessment") + )({ + piiEntities: Schema.Array(GuardrailPiiEntityFilter), + regexes: Schema.Array(GuardrailRegexFilter) + }) +{} + +/** + * @since 1.0.0 + * @category schemas + */ +export class GuardrailTopic extends Schema.Class(makeIdentifier("GuardrailTopic"))({ + action: Schema.Literals(["BLOCKED", "NONE"]), + name: Schema.String, + type: Schema.Literal("DENY"), + detected: Schema.optionalKey(Schema.Boolean) +}) {} + +/** + * @since 1.0.0 + * @category schemas + */ +export class GuardrailTopicPolicyAssessment extends Schema.Class( + makeIdentifier("GuardrailTopicPolicyAssessment") +)({ + topics: Schema.Array(GuardrailTopic) +}) {} + +/** + * @since 1.0.0 + * @category schemas + */ +export class GuardrailCustomWord extends Schema.Class( + makeIdentifier("GuardrailCustomWord") +)({ + action: Schema.Literals(["BLOCKED", "NONE"]), + match: Schema.String, + detected: Schema.optionalKey(Schema.Boolean) +}) {} + +/** + * @since 1.0.0 + * @category schemas + */ +export class GuardrailManagedWord extends Schema.Class( + makeIdentifier("GuardrailManagedWord") +)({ + action: Schema.Literals(["BLOCKED", "NONE"]), + match: Schema.String, + type: Schema.Literal("PROFANITY"), + detected: Schema.optionalKey(Schema.Boolean) +}) {} + +/** + * @since 1.0.0 + * @category schemas + */ +export class GuardrailWordPolicyAssessment extends Schema.Class( + makeIdentifier("GuardrailWordPolicyAssessment") +)({ + customWords: GuardrailCustomWord, + managedWordLists: GuardrailManagedWord +}) {} + +/** + * @since 1.0.0 + * @category schemas + */ +export class GuardrailAssessment extends Schema.Class(makeIdentifier("GuardrailAssessment"))({ + contentPolicy: Schema.optionalKey(GuardrailContentPolicyAssessment), + contextualGroundingPolicy: Schema.optionalKey(GuardrailContextualGroundingPolicyAssessment), + invocationMetrics: Schema.optionalKey(GuardrailInvocationMetrics), + sensitiveInformationPolicy: Schema.optionalKey(GuardrailSensitiveInformationPolicyAssessment), + topicPolicy: Schema.optionalKey(GuardrailTopicPolicyAssessment), + wordPolicy: Schema.optionalKey(GuardrailWordPolicyAssessment) +}) {} + +/** + * @since 1.0.0 + * @category schemas + */ +export class GuardrailTraceAssessment extends Schema.Class( + makeIdentifier("GuardrailTraceAssessment") +)({ + actionReason: Schema.optionalKey(Schema.String), + inputAssessment: Schema.optionalKey(Schema.Record(Schema.String, GuardrailAssessment)), + modelOutput: Schema.optionalKey(Schema.Array(Schema.String)), + outputAssessments: Schema.optionalKey(Schema.Record(Schema.String, GuardrailAssessment)) +}) {} + +/** + * @since 1.0.0 + * @category schemas + */ +export class PromptRouterTrace extends Schema.Class(makeIdentifier("PromptRouterTrace"))({ + invokedModelId: Schema.optionalKey( + Schema.String.check( + Schema.isPattern( + /^(arn:aws(-[^:]+)?:bedrock:[a-z0-9-]{1,20}::foundation-model\/[a-z0-9-]{1,63}[.]{1}[a-z0-9-]{1,63}([a-z0-9-]{1,63}[.]){0,2}[a-z0-9-]{1,63}([:][a-z0-9-]{1,63}){0,2})|(arn:aws(|-us-gov|-cn|-iso|-iso-b):bedrock:(|[0-9a-z-]{1,20}):(|[0-9]{12}):inference-profile\/[a-zA-Z0-9-:.]+)$/ + ) + ) + ) +}) {} + +/** + * @since 1.0.0 + * @category schemas + */ +export class ConverseTrace extends Schema.Class(makeIdentifier("ConverseTrace"))({ + guardrail: Schema.optionalKey(GuardrailTraceAssessment), + promptRouter: Schema.optionalKey(PromptRouterTrace) +}) {} + +/** + * @since 1.0.0 + * @category schemas + */ +export const IntZeroOrGreater = Schema.Int.check(Schema.isGreaterThanOrEqualTo(0)) + +/** + * @since 1.0.0 + * @category schemas + */ +export class TokenUsage extends Schema.Class(makeIdentifier("TokenUsage"))({ + inputTokens: IntZeroOrGreater, + outputTokens: IntZeroOrGreater, + totalTokens: IntZeroOrGreater, + cacheReadInputTokens: Schema.optionalKey(IntZeroOrGreater), + cacheWriteInputTokens: Schema.optionalKey(IntZeroOrGreater) +}) {} + +/** + * @since 1.0.0 + * @category schemas + */ +export const SystemContentBlock = Schema.Union([ + Schema.Struct({ cachePoint: CachePointBlock }), + Schema.Struct({ guardContent: GuardrailConverseContentBlock }), + Schema.Struct({ text: Schema.String.check(Schema.isMinLength(1)) }) +]) +/** + * @since 1.0.0 + * @category schemas + */ +export type SystemContentBlock = typeof SystemContentBlock.Type + +/** + * @since 1.0.0 + * @category schemas + */ +export class GuardrailConfiguration extends Schema.Class( + makeIdentifier("GuardrailConfiguration") +)({ + guardrailIdentifier: Schema.String + .check(Schema.isMinLength(0)) + .check(Schema.isMaxLength(2048)) + .check( + Schema.isPattern(/^(([a-z0-9]+)|(arn:aws(-[^:]+)?:bedrock:[a-z0-9-]{1,20}:[0-9]{12}:guardrail\/[a-z0-9]+))$/) + ), + guardrailVersion: Schema.String + .check(Schema.isPattern(/^(([1-9][0-9]{0,7})|(DRAFT))$/)), + trace: Schema.optionalKey(Schema.Literals(["enabled", "disabled", "enabled_full"])) +}) {} + +/** + * @since 1.0.0 + * @category schemas + */ +export class InferenceConfiguration extends Schema.Class( + makeIdentifier("InferenceConfiguration") +)({ + maxTokens: Schema.optionalKey(Schema.Int.check(Schema.isGreaterThanOrEqualTo(1))), + stopSequences: Schema.optionalKey( + Schema.Array(Schema.String.check(Schema.isMinLength(1))) + .check(Schema.isMaxLength(4)) + ), + temperature: Schema.optionalKey(Schema.Number.check(Schema.isBetween({ minimum: 0, maximum: 1 }))), + topP: Schema.optionalKey(Schema.Number.check(Schema.isBetween({ minimum: 0, maximum: 1 }))) +}) {} + +/** + * @since 1.0.0 + * @category schemas + */ +export class PerformanceConfiguration extends Schema.Class( + makeIdentifier("PerformanceConfiguration") +)({ + latency: Schema.optionalKey(Schema.Literals(["standard", "optimized"])) +}) {} + +/** + * @since 1.0.0 + * @category schemas + */ +export class ToolSpecification extends Schema.Class( + makeIdentifier("ToolSpecification") +)({ + name: Schema.String + .check(Schema.isMinLength(1)) + .check(Schema.isMaxLength(64)) + .check(Schema.isPattern(/^[a-zA-Z0-9_-]+$/)), + inputSchema: Schema.Struct({ + json: Schema.Record(Schema.String, Schema.Unknown) + }), + description: Schema.optionalKey(Schema.String.check(Schema.isMinLength(1))) +}) {} + +/** + * @since 1.0.0 + * @category schemas + */ +export class Tool extends Schema.Class( + makeIdentifier("Tool") +)({ + cachePoint: Schema.optionalKey(CachePointBlock), + toolSpec: Schema.optionalKey(ToolSpecification) +}) {} + +/** + * @since 1.0.0 + * @category schemas + */ +export const ToolChoice = Schema.Union([ + Schema.Struct({ any: Schema.Struct({}) }), + Schema.Struct({ auto: Schema.Struct({}) }), + Schema.Struct({ + tool: Schema.Struct({ + name: Schema.String + .check(Schema.isMinLength(1)) + .check(Schema.isMaxLength(64)) + .check(Schema.isPattern(/^[a-zA-Z0-9_-]+$/)) + }) + }) +]) +/** + * @since 1.0.0 + * @category schemas + */ +export type ToolChoice = typeof ToolChoice.Type + +/** + * @since 1.0.0 + * @category schemas + */ +export class ToolConfiguration extends Schema.Class( + makeIdentifier("ToolConfiguration") +)({ + tools: Schema.Array(Tool).check(Schema.isMinLength(1)), + toolChoice: Schema.optionalKey(ToolChoice) +}) {} + +/** + * @since 1.0.0 + * @category schemas + */ +export class ConverseRequest extends Schema.Class(makeIdentifier("ConverseRequest"))({ + modelId: Schema.String, + messages: Schema.Array(Message), + system: Schema.optionalKey(Schema.Array(SystemContentBlock)), + toolConfig: Schema.optionalKey(ToolConfiguration), + guardrailConfig: Schema.optionalKey(GuardrailConfiguration), + inferenceConfig: Schema.optionalKey(InferenceConfiguration), + performanceConfig: Schema.optionalKey(PerformanceConfiguration), + promptVariables: Schema.optionalKey(Schema.Record( + Schema.String, + Schema.Struct({ text: Schema.String }) + )), + requestMetadata: Schema.optionalKey(Schema.Record( + Schema.String + .check(Schema.isMinLength(1)) + .check(Schema.isMaxLength(256)) + .check(Schema.isPattern(/^[a-zA-Z0-9\s:_@$#=/+,-.]{1,256}$/)), + Schema.String + .check(Schema.isMinLength(0)) + .check(Schema.isMaxLength(256)) + .check(Schema.isPattern(/^[a-zA-Z0-9\s:_@$#=/+,-.]{0,256}$/)) + )), + additionalModelRequestFields: Schema.optionalKey(Schema.Record(Schema.String, Schema.Unknown)), + additionalModelResponseFieldPaths: Schema.optionalKey( + Schema.Array( + Schema.String.check(Schema.isMinLength(1)).check(Schema.isMaxLength(256)) + ).check(Schema.isMaxLength(10)) + ) +}) {} + +/** + * @since 1.0.0 + * @category schemas + */ +export class ConverseResponse extends Schema.Class(makeIdentifier("ConverseResponse"))({ + output: ConverseOutput, + metrics: ConverseMetrics, + usage: TokenUsage, + stopReason: Schema.Literals([ + "content_filtered", + "end_turn", + "tool_use", + "max_tokens", + "stop_sequence", + "guardrail_intervened" + ]), + trace: Schema.optionalKey(ConverseTrace), + performanceConfig: Schema.optionalKey(Schema.Struct({ + latency: Schema.Literals(["standard", "optimized"]) + })), + additionalModelResponseFields: Schema.optionalKey(Schema.Record(Schema.String, Schema.Unknown)) +}) {} + +/** + * @since 1.0.0 + * @category schemas + */ +export const ReasoningContentBlockDelta = Schema.Union([ + Schema.Struct({ redactedContent: Schema.String }), + Schema.Struct({ signature: Schema.String }), + Schema.Struct({ text: Schema.String }) +]) +/** + * @since 1.0.0 + * @category schemas + */ +export type ReasoningContentBlockDelta = typeof ReasoningContentBlockDelta.Type + +/** + * @since 1.0.0 + * @category schemas + */ +export class ToolUseBlockStart extends Schema.Class( + makeIdentifier("ToolUseBlockStart") +)({ + name: Schema.String + .check(Schema.isMinLength(1)) + .check(Schema.isMaxLength(64)) + .check(Schema.isPattern(/^[a-zA-Z0-9_-]+$/)), + toolUseId: Schema.String + .check(Schema.isMinLength(1)) + .check(Schema.isMaxLength(64)) + .check(Schema.isPattern(/^[a-zA-Z0-9_-]+$/)) +}) {} + +/** + * @since 1.0.0 + * @category schemas + */ +export class ContentBlockStart extends Schema.Class( + makeIdentifier("ContentBlockStart") +)({ + toolUse: Schema.optionalKey(ToolUseBlockStart) +}) {} + +/** + * @since 1.0.0 + * @category schemas + */ +export class ContentBlockStartEvent extends Schema.Class( + makeIdentifier("ContentBlockStartEvent") +)({ + contentBlockIndex: Schema.Int.check(Schema.isGreaterThanOrEqualTo(0)), + start: ContentBlockStart +}) {} + +/** + * @since 1.0.0 + * @category schemas + */ +export class ContentBlockStopEvent extends Schema.Class( + makeIdentifier("ContentBlockStopEvent") +)({ + contentBlockIndex: Schema.Int.check(Schema.isGreaterThanOrEqualTo(0)) +}) {} + +/** + * @since 1.0.0 + * @category schemas + */ +export class ToolUseBlockDelta extends Schema.Class( + makeIdentifier("ToolUseBlockDelta") +)({ + input: Schema.String +}) {} + +/** + * @since 1.0.0 + * @category schemas + */ +export const ContentBlockDelta = Schema.Union([ + Schema.Struct({ reasoningContent: ReasoningContentBlockDelta }), + Schema.Struct({ text: Schema.String }), + Schema.Struct({ toolUse: ToolUseBlockDelta }) +]) +/** + * @since 1.0.0 + * @category schemas + */ +export type ContentBlockDelta = typeof ContentBlockDelta.Type + +/** + * @since 1.0.0 + * @category schemas + */ +export class ContentBlockDeltaEvent extends Schema.Class( + makeIdentifier("ContentBlockDeltaEvent") +)({ + contentBlockIndex: Schema.Int.check(Schema.isGreaterThanOrEqualTo(0)), + delta: ContentBlockDelta +}) {} + +/** + * @since 1.0.0 + * @category schemas + */ +export class MessageStartEvent extends Schema.Class( + makeIdentifier("MessageStartEvent") +)({ + role: Schema.Literals(["user", "assistant"]) +}) {} + +/** + * @since 1.0.0 + * @category schemas + */ +export const StopReason = Schema.Literals([ + "end_turn", + "tool_use", + "max_tokens", + "stop_sequence", + "guardrail_intervened", + "content_filtered" +]) +/** + * @since 1.0.0 + * @category schemas + */ +export type StopReason = typeof StopReason.Type + +/** + * @since 1.0.0 + * @category schemas + */ +export class MessageStopEvent extends Schema.Class( + makeIdentifier("MessageStopEvent") +)({ + stopReason: StopReason, + additionalModelResponseFields: Schema.optionalKey(Schema.Record(Schema.String, Schema.Unknown)) +}) {} + +/** + * @since 1.0.0 + * @category schemas + */ +export class ConverseStreamMetrics extends Schema.Class( + makeIdentifier("ConverseStreamMetrics") +)({ + latencyMs: Schema.DurationFromMillis +}) {} + +/** + * @since 1.0.0 + * @category schemas + */ +export class ConverseStreamTrace extends Schema.Class( + makeIdentifier("ConverseStreamTrace") +)({ + guardrail: Schema.optionalKey(GuardrailTraceAssessment), + promptRouter: Schema.optionalKey(PromptRouterTrace) +}) {} + +/** + * @since 1.0.0 + * @category schemas + */ +export class ConverseStreamMetadataEvent extends Schema.Class( + makeIdentifier("ConverseStreamMetadataEvent") +)({ + metrics: ConverseStreamMetrics, + usage: TokenUsage, + performanceConfig: Schema.optionalKey(PerformanceConfiguration), + trace: Schema.optionalKey(ConverseStreamTrace) +}) {} + +/** + * @since 1.0.0 + * @category schemas + */ +export const ConverseResponseStreamEvent = Schema.Union([ + Schema.Struct({ messageStart: MessageStartEvent }).annotate({ title: "MessageStartEvent" }), + Schema.Struct({ messageStop: MessageStopEvent }).annotate({ title: "MessageStopEvent" }), + Schema.Struct({ contentBlockStart: ContentBlockStartEvent }).annotate({ title: "ContentBlockStartEvent" }), + Schema.Struct({ contentBlockDelta: ContentBlockDeltaEvent }).annotate({ title: "ContentBlockDeltaEvent" }), + Schema.Struct({ contentBlockStop: ContentBlockStopEvent }).annotate({ title: "ContentBlockStopEvent" }), + Schema.Struct({ metadata: ConverseStreamMetadataEvent }).annotate({ title: "ConverseStreamMetadataEvent" }), + Schema.Struct({ + internalServerException: Schema.Record(Schema.String, Schema.Unknown) + }).annotate({ title: "InternalServerException" }), + Schema.Struct({ + modelStreamErrorException: Schema.Record(Schema.String, Schema.Unknown) + }).annotate({ title: "ModelStreamErrorException" }), + Schema.Struct({ + serviceUnavailableException: Schema.Record(Schema.String, Schema.Unknown) + }).annotate({ title: "ServiceUnavailableException" }), + Schema.Struct({ + throttlingException: Schema.Record(Schema.String, Schema.Unknown) + }).annotate({ title: "ThrottlingException" }), + Schema.Struct({ + validationException: Schema.Record(Schema.String, Schema.Unknown) + }).annotate({ title: "ValidationException" }) +]).annotate({ title: "ConverseResponseStreamEvent" }) + +/** + * @since 1.0.0 + * @category models + */ +export type ConverseResponseStreamEvent = typeof ConverseResponseStreamEvent.Type diff --git a/packages/ai/amazon-bedrock/src/AmazonBedrockTool.ts b/packages/ai/amazon-bedrock/src/AmazonBedrockTool.ts new file mode 100644 index 0000000000..fcd49fb8bf --- /dev/null +++ b/packages/ai/amazon-bedrock/src/AmazonBedrockTool.ts @@ -0,0 +1,93 @@ +/** + * Amazon Bedrock provider-defined tools. + * + * Re-exports Anthropic tools for use with Amazon Bedrock when running + * Anthropic models (e.g., Claude) on the Bedrock platform. + * + * @since 1.0.0 + */ +import * as AnthropicTool from "@effect/ai-anthropic/AnthropicTool" + +/** + * @since 1.0.0 + * @category tools + */ +export const AnthropicBash_20241022 = AnthropicTool.Bash_20241022 + +/** + * @since 1.0.0 + * @category tools + */ +export const AnthropicBash_20250124 = AnthropicTool.Bash_20250124 + +/** + * @since 1.0.0 + * @category tools + */ +export const AnthropicComputerUse_20241022 = AnthropicTool.ComputerUse_20241022 + +/** + * @since 1.0.0 + * @category tools + */ +export const AnthropicComputerUse_20250124 = AnthropicTool.ComputerUse_20250124 + +/** + * @since 1.0.0 + * @category tools + */ +export const AnthropicComputerUse_20251124 = AnthropicTool.ComputerUse_20251124 + +/** + * @since 1.0.0 + * @category tools + */ +export const AnthropicTextEditor_20241022 = AnthropicTool.TextEditor_20241022 + +/** + * @since 1.0.0 + * @category tools + */ +export const AnthropicTextEditor_20250124 = AnthropicTool.TextEditor_20250124 + +/** + * @since 1.0.0 + * @category tools + */ +export const AnthropicTextEditor_20250429 = AnthropicTool.TextEditor_20250429 + +/** + * @since 1.0.0 + * @category tools + */ +export const AnthropicTextEditor_20250728 = AnthropicTool.TextEditor_20250728 + +/** + * @since 1.0.0 + * @category tools + */ +export const AnthropicCodeExecution_20250522 = AnthropicTool.CodeExecution_20250522 + +/** + * @since 1.0.0 + * @category tools + */ +export const AnthropicCodeExecution_20250825 = AnthropicTool.CodeExecution_20250825 + +/** + * @since 1.0.0 + * @category tools + */ +export const AnthropicMemory_20250818 = AnthropicTool.Memory_20250818 + +/** + * @since 1.0.0 + * @category tools + */ +export const AnthropicWebSearch_20250305 = AnthropicTool.WebSearch_20250305 + +/** + * @since 1.0.0 + * @category tools + */ +export const AnthropicWebFetch_20250910 = AnthropicTool.WebFetch_20250910 diff --git a/packages/ai/amazon-bedrock/src/EventStreamEncoding.ts b/packages/ai/amazon-bedrock/src/EventStreamEncoding.ts new file mode 100644 index 0000000000..5b82c6545b --- /dev/null +++ b/packages/ai/amazon-bedrock/src/EventStreamEncoding.ts @@ -0,0 +1,107 @@ +/** + * An event stream encoding parser for Amazon Bedrock streaming responses. + * + * See the [AWS Documentation](https://docs.aws.amazon.com/lexv2/latest/dg/event-stream-encoding.html) + * for more information. + * + * @since 1.0.0 + */ +import { EventStreamCodec } from "@smithy/eventstream-codec" +import { fromUtf8, toUtf8 } from "@smithy/util-utf8" +import type * as Arr from "effect/Array" +import * as Channel from "effect/Channel" +import * as Effect from "effect/Effect" +import type * as Pull from "effect/Pull" +import * as Schema from "effect/Schema" + +const isNonEmpty = (self: Array): self is Arr.NonEmptyArray => self.length > 0 + +/** + * @since 1.0.0 + * @category constructors + */ +export const makeChannel = (schema: Schema.Schema): Channel.Channel< + Arr.NonEmptyReadonlyArray, + Schema.SchemaError, + unknown, + Arr.NonEmptyReadonlyArray>, + unknown, + unknown +> => + Channel.fromTransform(( + upstream: Pull.Pull>, unknown, unknown>, + _scope + ) => { + const codec = new EventStreamCodec(toUtf8, fromUtf8) + const decodeMessage = Schema.decodeUnknownEffect(schema) + const textDecoder = new TextDecoder() + + let buffer = new Uint8Array(0) + let pending: Array = [] + + const pull = Effect.gen(function*() { + // Drain pending messages first + if (isNonEmpty(pending)) { + const result = pending + pending = [] + return result + } + + // Keep pulling upstream until we have at least one decoded message + while (true) { + const chunks = yield* upstream + + for (const chunk of chunks) { + // Append new chunk to buffer + const newBuffer = new Uint8Array(buffer.length + chunk.length) + newBuffer.set(buffer) + newBuffer.set(chunk, buffer.length) + buffer = newBuffer + + // Try to decode messages from the buffer + while (buffer.length >= 4) { + // The first four bytes are the total length of the message (big-endian) + const totalLength = new DataView( + buffer.buffer, + buffer.byteOffset, + buffer.byteLength + ).getUint32(0, false) + + // If we don't have the full message yet, keep looping + if (buffer.length < totalLength) { + break + } + + // Decode exactly the sub-slice for this event + const subView = buffer.subarray(0, totalLength) + const decoded = codec.decode(subView) + + // Slice the used bytes off the buffer, removing this message + buffer = buffer.slice(totalLength) + + // Process the message + if (decoded.headers[":message-type"]?.value === "event") { + const data = textDecoder.decode(decoded.body) + + // Wrap the data in the `":event-type"` field to match the + // expected schema + const message = yield* decodeMessage({ + [decoded.headers[":event-type"]?.value as string]: JSON.parse(data) + }) + + pending.push(message) + } + } + } + + // If we decoded at least one message, emit them + if (isNonEmpty(pending)) { + const result: Arr.NonEmptyArray = [pending[0], ...pending.slice(1)] + pending = [] + return result + } + } + }) + + return Effect.succeed(pull as any) + }) as any diff --git a/packages/ai/amazon-bedrock/src/index.ts b/packages/ai/amazon-bedrock/src/index.ts new file mode 100644 index 0000000000..1506353d69 --- /dev/null +++ b/packages/ai/amazon-bedrock/src/index.ts @@ -0,0 +1,55 @@ +/** + * @since 1.0.0 + */ + +// @barrel: Auto-generated exports. Do not edit manually. + +/** + * @since 1.0.0 + */ +export * as AmazonBedrockClient from "./AmazonBedrockClient.ts" + +/** + * @since 1.0.0 + */ +export * as AmazonBedrockConfig from "./AmazonBedrockConfig.ts" + +/** + * Amazon Bedrock error metadata augmentation. + * + * Provides Amazon Bedrock-specific metadata fields for AI error types through + * module augmentation, enabling typed access to Bedrock error details. + * + * @since 1.0.0 + */ +export * as AmazonBedrockError from "./AmazonBedrockError.ts" + +/** + * @since 1.0.0 + */ +export * as AmazonBedrockLanguageModel from "./AmazonBedrockLanguageModel.ts" + +/** + * @since 1.0.0 + */ +export * as AmazonBedrockSchema from "./AmazonBedrockSchema.ts" + +/** + * Amazon Bedrock provider-defined tools. + * + * Re-exports Anthropic tools for use with Amazon Bedrock when running + * Anthropic models (e.g., Claude) on the Bedrock platform. + * + * @since 1.0.0 + */ +export * as AmazonBedrockTool from "./AmazonBedrockTool.ts" + +/** + * An event stream encoding parser for Amazon Bedrock streaming responses. + * + * See the [AWS Documentation](https://docs.aws.amazon.com/lexv2/latest/dg/event-stream-encoding.html) + * for more information. + * + * @since 1.0.0 + */ +export * as EventStreamEncoding from "./EventStreamEncoding.ts" diff --git a/packages/ai/amazon-bedrock/src/internal/errors.ts b/packages/ai/amazon-bedrock/src/internal/errors.ts new file mode 100644 index 0000000000..2b1d3a5e6c --- /dev/null +++ b/packages/ai/amazon-bedrock/src/internal/errors.ts @@ -0,0 +1,223 @@ +import * as Effect from "effect/Effect" +import { dual } from "effect/Function" +import * as Option from "effect/Option" +import * as Predicate from "effect/Predicate" +import * as Redactable from "effect/Redactable" +import * as Schema from "effect/Schema" +import * as AiError from "effect/unstable/ai/AiError" +import type * as Response from "effect/unstable/ai/Response" +import type * as HttpClientError from "effect/unstable/http/HttpClientError" +import type * as HttpClientRequest from "effect/unstable/http/HttpClientRequest" +import type * as HttpClientResponse from "effect/unstable/http/HttpClientResponse" +import type { AmazonBedrockErrorMetadata } from "../AmazonBedrockError.ts" + +// ============================================================================= +// Amazon Bedrock Error Body Schema +// ============================================================================= + +/** @internal */ +export const BedrockErrorBody = Schema.Struct({ + message: Schema.String +}) + +// ============================================================================= +// Error Mappers +// ============================================================================= + +/** @internal */ +export const mapSchemaError = dual< + (method: string) => (error: Schema.SchemaError) => AiError.AiError, + (error: Schema.SchemaError, method: string) => AiError.AiError +>(2, (error, method) => + AiError.make({ + module: "AmazonBedrockClient", + method, + reason: AiError.InvalidOutputError.fromSchemaError(error) + })) + +/** @internal */ +export const mapHttpClientError = dual< + (method: string) => (error: HttpClientError.HttpClientError) => Effect.Effect, + (error: HttpClientError.HttpClientError, method: string) => Effect.Effect +>(2, (error, method) => { + const reason = error.reason + switch (reason._tag) { + case "TransportError": { + return Effect.fail(AiError.make({ + module: "AmazonBedrockClient", + method, + reason: new AiError.NetworkError({ + reason: "TransportError", + description: reason.description, + request: buildHttpRequestDetails(reason.request) + }) + })) + } + case "EncodeError": { + return Effect.fail(AiError.make({ + module: "AmazonBedrockClient", + method, + reason: new AiError.NetworkError({ + reason: "EncodeError", + description: reason.description, + request: buildHttpRequestDetails(reason.request) + }) + })) + } + case "InvalidUrlError": { + return Effect.fail(AiError.make({ + module: "AmazonBedrockClient", + method, + reason: new AiError.NetworkError({ + reason: "InvalidUrlError", + description: reason.description, + request: buildHttpRequestDetails(reason.request) + }) + })) + } + case "StatusCodeError": { + return mapStatusCodeError(reason, method) + } + case "DecodeError": { + return Effect.fail(AiError.make({ + module: "AmazonBedrockClient", + method, + reason: new AiError.InvalidOutputError({ + description: reason.description ?? "Failed to decode response" + }) + })) + } + case "EmptyBodyError": { + return Effect.fail(AiError.make({ + module: "AmazonBedrockClient", + method, + reason: new AiError.InvalidOutputError({ + description: reason.description ?? "Response body was empty" + }) + })) + } + } +}) + +/** @internal */ +const mapStatusCodeError = Effect.fnUntraced(function*( + error: HttpClientError.StatusCodeError, + method: string +) { + const { request, response, description } = error + const status = response.status + + let body: string | undefined = description + if (!description || !description.startsWith("{")) { + const responseBody = yield* Effect.option(response.text) + if (Option.isSome(responseBody) && responseBody.value) { + body = responseBody.value + } + } + + let json: unknown = undefined + // @effect-diagnostics effect/tryCatchInEffectGen:off + try { + json = Predicate.isNotUndefined(body) ? JSON.parse(body) : undefined + } catch { + json = undefined + } + const decoded = Schema.decodeUnknownOption(BedrockErrorBody)(json) + + const reason = mapStatusCodeToReason({ + status, + message: Option.isSome(decoded) ? decoded.value.message : undefined, + http: buildHttpContext({ request, response, body }) + }) + + return yield* AiError.make({ module: "AmazonBedrockClient", method, reason }) +}) + +// ============================================================================= +// HTTP Context +// ============================================================================= + +/** @internal */ +export const buildHttpRequestDetails = ( + request: HttpClientRequest.HttpClientRequest +): typeof Response.HttpRequestDetails.Type => ({ + method: request.method, + url: request.url, + urlParams: Array.from(request.urlParams), + hash: Option.getOrUndefined(request.hash), + headers: Redactable.redact(request.headers) as Record +}) + +/** @internal */ +export const buildHttpContext = (params: { + readonly request: HttpClientRequest.HttpClientRequest + readonly response?: HttpClientResponse.HttpClientResponse + readonly body?: string | undefined +}): typeof AiError.HttpContext.Type => ({ + request: buildHttpRequestDetails(params.request), + response: Predicate.isNotUndefined(params.response) + ? { + status: params.response.status, + headers: Redactable.redact(params.response.headers) as Record + } + : undefined, + body: params.body +}) + +// ============================================================================= +// HTTP Status Code +// ============================================================================= + +/** @internal */ +export const mapStatusCodeToReason = ({ status, message, http }: { + readonly status: number + readonly message: string | undefined + readonly http: typeof AiError.HttpContext.Type +}): AiError.AiErrorReason => { + const metadata: AmazonBedrockErrorMetadata = {} + + switch (status) { + case 400: + return new AiError.InvalidRequestError({ + description: message ?? `HTTP ${status}`, + metadata: { bedrock: metadata }, + http + }) + case 401: + return new AiError.AuthenticationError({ + kind: "InvalidKey", + metadata: { bedrock: metadata }, + http + }) + case 403: + return new AiError.AuthenticationError({ + kind: "InsufficientPermissions", + metadata: { bedrock: metadata }, + http + }) + case 404: + return new AiError.InvalidRequestError({ + description: message ?? `HTTP ${status}`, + metadata: { bedrock: metadata }, + http + }) + case 429: + return new AiError.RateLimitError({ + metadata: { bedrock: metadata }, + http + }) + default: + if (status >= 500) { + return new AiError.InternalProviderError({ + description: message ?? "Server error", + metadata: { bedrock: metadata }, + http + }) + } + return new AiError.UnknownError({ + description: message, + metadata: { bedrock: metadata }, + http + }) + } +} diff --git a/packages/ai/amazon-bedrock/src/internal/utilities.ts b/packages/ai/amazon-bedrock/src/internal/utilities.ts new file mode 100644 index 0000000000..f43e4f366f --- /dev/null +++ b/packages/ai/amazon-bedrock/src/internal/utilities.ts @@ -0,0 +1,18 @@ +import * as Predicate from "effect/Predicate" +import type * as Response from "effect/unstable/ai/Response" +import type { StopReason } from "../AmazonBedrockSchema.ts" + +const finishReasonMap: Record = { + content_filtered: "content-filter", + end_turn: "stop", + guardrail_intervened: "content-filter", + max_tokens: "length", + stop_sequence: "stop", + tool_use: "tool-calls" +} + +/** @internal */ +export const resolveFinishReason = (stopReason: StopReason): Response.FinishReason => { + const reason = finishReasonMap[stopReason] + return Predicate.isUndefined(reason) ? "unknown" : reason +} diff --git a/packages/ai/amazon-bedrock/test/AmazonBedrockLanguageModel.test.ts b/packages/ai/amazon-bedrock/test/AmazonBedrockLanguageModel.test.ts new file mode 100644 index 0000000000..e4c0582218 --- /dev/null +++ b/packages/ai/amazon-bedrock/test/AmazonBedrockLanguageModel.test.ts @@ -0,0 +1,838 @@ +import { AmazonBedrockClient, AmazonBedrockLanguageModel } from "@effect/ai-amazon-bedrock" +import { assert, describe, it } from "@effect/vitest" +import { EventStreamCodec } from "@smithy/eventstream-codec" +import { fromUtf8, toUtf8 } from "@smithy/util-utf8" +import { Effect, Layer, Redacted, Schema, Stream } from "effect" +import { LanguageModel, Tool, Toolkit } from "effect/unstable/ai" +import { HttpClient, type HttpClientError, HttpClientRequest, HttpClientResponse } from "effect/unstable/http" + +describe("AmazonBedrockLanguageModel", () => { + describe("generateText", () => { + it.effect("parses a simple text response", () => + Effect.gen(function*() { + const layer = makeTestLayer((request) => + Effect.succeed(jsonResponse(request, { + output: { + message: { + role: "assistant", + content: [{ text: "Hello!" }] + } + }, + metrics: { latencyMs: 100 }, + usage: { inputTokens: 10, outputTokens: 5, totalTokens: 15 }, + stopReason: "end_turn" + })) + ) + + const result = yield* LanguageModel.generateText({ + prompt: "Say hello" + }).pipe( + Effect.provide(AmazonBedrockLanguageModel.model("anthropic.claude-3-5-sonnet-20241022-v2:0")), + Effect.provide(layer) + ) + + const textPart = result.content.find((part) => part.type === "text") + assert.isDefined(textPart) + if (textPart?.type !== "text") return + assert.strictEqual(textPart.text, "Hello!") + + const finishPart = result.content.find((part) => part.type === "finish") + assert.isDefined(finishPart) + if (finishPart?.type !== "finish") return + assert.strictEqual(finishPart.reason, "stop") + })) + + it.effect("parses a tool call response", () => + Effect.gen(function*() { + const toolParams = { pattern: "*.ts" } + + const layer = makeTestLayer((request) => + Effect.succeed(jsonResponse(request, { + output: { + message: { + role: "assistant", + content: [{ + toolUse: { + toolUseId: "tool_1", + name: "GlobTool", + input: toolParams + } + }] + } + }, + metrics: { latencyMs: 100 }, + usage: { inputTokens: 10, outputTokens: 5, totalTokens: 15 }, + stopReason: "tool_use" + })) + ) + + const GlobTool = Tool.make("GlobTool", { + description: "Search for files", + parameters: Schema.Struct({ pattern: Schema.String }), + success: Schema.String + }) + + const toolkit = Toolkit.make(GlobTool) + const toolkitLayer = toolkit.toLayer({ + GlobTool: () => Effect.succeed("found.ts") + }) + + const result = yield* LanguageModel.generateText({ + prompt: "find ts files", + toolkit, + disableToolCallResolution: true + }).pipe( + Effect.provide(AmazonBedrockLanguageModel.model("anthropic.claude-3-5-sonnet-20241022-v2:0")), + Effect.provide(toolkitLayer), + Effect.provide(layer) + ) + + const toolCall = result.content.find((part) => part.type === "tool-call") + assert.isDefined(toolCall) + if (toolCall?.type !== "tool-call") return + assert.strictEqual(toolCall.name, "GlobTool") + assert.deepStrictEqual(toolCall.params, toolParams) + })) + + it.effect("maps max_tokens stop reason to length", () => + Effect.gen(function*() { + const layer = makeTestLayer((request) => + Effect.succeed(jsonResponse(request, { + output: { + message: { + role: "assistant", + content: [{ text: "Truncated..." }] + } + }, + metrics: { latencyMs: 100 }, + usage: { inputTokens: 10, outputTokens: 50, totalTokens: 60 }, + stopReason: "max_tokens" + })) + ) + + const result = yield* LanguageModel.generateText({ + prompt: "Write a long essay" + }).pipe( + Effect.provide(AmazonBedrockLanguageModel.model("anthropic.claude-3-5-sonnet-20241022-v2:0")), + Effect.provide(layer) + ) + + const finishPart = result.content.find((part) => part.type === "finish") + assert.isDefined(finishPart) + if (finishPart?.type !== "finish") return + assert.strictEqual(finishPart.reason, "length") + })) + + it.effect("includes cached token counts in finish", () => + Effect.gen(function*() { + const layer = makeTestLayer((request) => + Effect.succeed(jsonResponse(request, { + output: { + message: { + role: "assistant", + content: [{ text: "Hello!" }] + } + }, + metrics: { latencyMs: 100 }, + usage: { + inputTokens: 10, + outputTokens: 5, + totalTokens: 115, + cacheReadInputTokens: 50, + cacheWriteInputTokens: 50 + }, + stopReason: "end_turn" + })) + ) + + const result = yield* LanguageModel.generateText({ + prompt: "Hello" + }).pipe( + Effect.provide(AmazonBedrockLanguageModel.model("anthropic.claude-3-5-sonnet-20241022-v2:0")), + Effect.provide(layer) + ) + + const finishPart = result.content.find((part) => part.type === "finish") + assert.isDefined(finishPart) + if (finishPart?.type !== "finish") return + assert.strictEqual(finishPart.usage.inputTokens.cacheRead, 50) + assert.strictEqual(finishPart.usage.inputTokens.cacheWrite, 50) + assert.strictEqual(finishPart.usage.inputTokens.uncached, 10) + assert.strictEqual(finishPart.usage.inputTokens.total, 110) + })) + + it.effect("cached tokens default to 0 when not in response", () => + Effect.gen(function*() { + const layer = makeTestLayer((request) => + Effect.succeed(jsonResponse(request, { + output: { + message: { + role: "assistant", + content: [{ text: "Hello!" }] + } + }, + metrics: { latencyMs: 100 }, + usage: { inputTokens: 10, outputTokens: 5, totalTokens: 15 }, + stopReason: "end_turn" + })) + ) + + const result = yield* LanguageModel.generateText({ + prompt: "Hello" + }).pipe( + Effect.provide(AmazonBedrockLanguageModel.model("anthropic.claude-3-5-sonnet-20241022-v2:0")), + Effect.provide(layer) + ) + + const finishPart = result.content.find((part) => part.type === "finish") + assert.isDefined(finishPart) + if (finishPart?.type !== "finish") return + assert.strictEqual(finishPart.usage.inputTokens.cacheRead, 0) + assert.strictEqual(finishPart.usage.inputTokens.cacheWrite, 0) + assert.strictEqual(finishPart.usage.inputTokens.total, 10) + })) + + it.effect("cacheReadInputTokens: 0 is preserved as 0, not undefined", () => + Effect.gen(function*() { + const layer = makeTestLayer((request) => + Effect.succeed(jsonResponse(request, { + output: { + message: { + role: "assistant", + content: [{ text: "Hello!" }] + } + }, + metrics: { latencyMs: 100 }, + usage: { + inputTokens: 10, + outputTokens: 5, + totalTokens: 15, + cacheReadInputTokens: 0 + }, + stopReason: "end_turn" + })) + ) + + const result = yield* LanguageModel.generateText({ + prompt: "Hello" + }).pipe( + Effect.provide(AmazonBedrockLanguageModel.model("anthropic.claude-3-5-sonnet-20241022-v2:0")), + Effect.provide(layer) + ) + + const finishPart = result.content.find((part) => part.type === "finish") + assert.isDefined(finishPart) + if (finishPart?.type !== "finish") return + assert.strictEqual(finishPart.usage.inputTokens.cacheRead, 0) + })) + }) + + describe("streamText", () => { + it.effect("streams text and emits finish after messageStop + metadata", () => + Effect.gen(function*() { + const layer = makeTestLayer((request) => { + if (request.url.includes("converse-stream")) { + return Effect.succeed(eventStreamResponse(request, [ + ["messageStart", { role: "assistant" }], + ["contentBlockStart", { contentBlockIndex: 0, start: {} }], + ["contentBlockDelta", { contentBlockIndex: 0, delta: { text: "Hello" } }], + ["contentBlockDelta", { contentBlockIndex: 0, delta: { text: " World" } }], + ["contentBlockStop", { contentBlockIndex: 0 }], + ["messageStop", { stopReason: "end_turn" }], + ["metadata", { + metrics: { latencyMs: 150 }, + usage: { inputTokens: 10, outputTokens: 5, totalTokens: 15 } + }] + ])) + } + return Effect.succeed(jsonResponse(request, {})) + }) + + const parts = yield* LanguageModel.streamText({ + prompt: "Say hello world" + }).pipe( + Stream.runCollect, + Effect.provide(AmazonBedrockLanguageModel.model("anthropic.claude-3-5-sonnet-20241022-v2:0")), + Effect.provide(layer) + ) + + const partsArray = globalThis.Array.from(parts) + + // Should have text deltas + const textDeltas = partsArray.filter((p) => p.type === "text-delta") + assert.isTrue(textDeltas.length >= 1) + + // Should have a finish part + const finishPart = partsArray.find((p) => p.type === "finish") + assert.isDefined(finishPart) + if (finishPart?.type !== "finish") return + assert.strictEqual(finishPart.reason, "stop") + + // Finish part should have usage + assert.isDefined(finishPart.usage) + assert.strictEqual(finishPart.usage.inputTokens.total, 10) + assert.strictEqual(finishPart.usage.outputTokens.total, 5) + })) + + it.effect("streams tool call with params in deltas", () => + Effect.gen(function*() { + const toolParams = { query: "test" } + + const layer = makeTestLayer((request) => { + if (request.url.includes("converse-stream")) { + return Effect.succeed(eventStreamResponse(request, [ + ["messageStart", { role: "assistant" }], + ["contentBlockStart", { + contentBlockIndex: 0, + start: { + toolUse: { + name: "SearchTool", + toolUseId: "tool_1" + } + } + }], + ["contentBlockDelta", { + contentBlockIndex: 0, + delta: { toolUse: { input: JSON.stringify(toolParams) } } + }], + ["contentBlockStop", { contentBlockIndex: 0 }], + ["messageStop", { stopReason: "tool_use" }], + ["metadata", { + metrics: { latencyMs: 100 }, + usage: { inputTokens: 10, outputTokens: 5, totalTokens: 15 } + }] + ])) + } + return Effect.succeed(jsonResponse(request, {})) + }) + + const SearchTool = Tool.make("SearchTool", { + description: "Search", + parameters: Schema.Struct({ query: Schema.String }), + success: Schema.String + }) + + const toolkit = Toolkit.make(SearchTool) + const toolkitLayer = toolkit.toLayer({ + SearchTool: () => Effect.succeed("result") + }) + + const parts = yield* LanguageModel.streamText({ + prompt: "search for test", + toolkit, + disableToolCallResolution: true + }).pipe( + Stream.runCollect, + Effect.provide(AmazonBedrockLanguageModel.model("anthropic.claude-3-5-sonnet-20241022-v2:0")), + Effect.provide(toolkitLayer), + Effect.provide(layer) + ) + + const partsArray = globalThis.Array.from(parts) + + const toolCall = partsArray.find((p) => p.type === "tool-call") + assert.isDefined(toolCall) + if (toolCall?.type !== "tool-call") return + assert.strictEqual(toolCall.name, "SearchTool") + assert.deepStrictEqual(toolCall.params, toolParams) + })) + + it.effect("tryEmitFinish defers until both messageStop and metadata arrive", () => + Effect.gen(function*() { + // Test that finish is only emitted after BOTH messageStop and metadata + const layer = makeTestLayer((request) => { + if (request.url.includes("converse-stream")) { + return Effect.succeed(eventStreamResponse(request, [ + ["messageStart", { role: "assistant" }], + ["contentBlockStart", { contentBlockIndex: 0, start: {} }], + ["contentBlockDelta", { contentBlockIndex: 0, delta: { text: "Hi" } }], + ["contentBlockStop", { contentBlockIndex: 0 }], + // messageStop arrives first — finish should NOT be emitted yet + ["messageStop", { stopReason: "end_turn" }], + // metadata arrives second — NOW finish should be emitted + ["metadata", { + metrics: { latencyMs: 50 }, + usage: { inputTokens: 5, outputTokens: 2, totalTokens: 7 } + }] + ])) + } + return Effect.succeed(jsonResponse(request, {})) + }) + + const parts = yield* LanguageModel.streamText({ + prompt: "hi" + }).pipe( + Stream.runCollect, + Effect.provide(AmazonBedrockLanguageModel.model("anthropic.claude-3-5-sonnet-20241022-v2:0")), + Effect.provide(layer) + ) + + const partsArray = globalThis.Array.from(parts) + + // There should be exactly one finish part + const finishParts = partsArray.filter((p) => p.type === "finish") + assert.strictEqual(finishParts.length, 1) + + // It should be the last meaningful part + const lastPart = partsArray[partsArray.length - 1] + assert.strictEqual(lastPart.type, "finish") + })) + + it.effect("streams cached token counts (cacheRead/cacheWrite) in finish", () => + Effect.gen(function*() { + const layer = makeTestLayer((request) => { + if (request.url.includes("converse-stream")) { + return Effect.succeed(eventStreamResponse(request, [ + ["messageStart", { role: "assistant" }], + ["contentBlockStart", { contentBlockIndex: 0, start: {} }], + ["contentBlockDelta", { contentBlockIndex: 0, delta: { text: "Hi" } }], + ["contentBlockStop", { contentBlockIndex: 0 }], + ["messageStop", { stopReason: "end_turn" }], + ["metadata", { + metrics: { latencyMs: 100 }, + usage: { + inputTokens: 10, + outputTokens: 5, + totalTokens: 115, + cacheReadInputTokens: 50, + cacheWriteInputTokens: 50 + } + }] + ])) + } + return Effect.succeed(jsonResponse(request, {})) + }) + + const parts = yield* LanguageModel.streamText({ + prompt: "Hi" + }).pipe( + Stream.runCollect, + Effect.provide(AmazonBedrockLanguageModel.model("anthropic.claude-3-5-sonnet-20241022-v2:0")), + Effect.provide(layer) + ) + + const partsArray = globalThis.Array.from(parts) + const finishPart = partsArray.find((p) => p.type === "finish") + assert.isDefined(finishPart) + if (finishPart?.type !== "finish") return + assert.strictEqual(finishPart.usage.inputTokens.cacheRead, 50) + assert.strictEqual(finishPart.usage.inputTokens.cacheWrite, 50) + assert.strictEqual(finishPart.usage.inputTokens.uncached, 10) + assert.strictEqual(finishPart.usage.inputTokens.total, 110) + })) + + it.effect("cached tokens default to 0 when not in metadata", () => + Effect.gen(function*() { + const layer = makeTestLayer((request) => { + if (request.url.includes("converse-stream")) { + return Effect.succeed(eventStreamResponse(request, [ + ["messageStart", { role: "assistant" }], + ["contentBlockStart", { contentBlockIndex: 0, start: {} }], + ["contentBlockDelta", { contentBlockIndex: 0, delta: { text: "Hi" } }], + ["contentBlockStop", { contentBlockIndex: 0 }], + ["messageStop", { stopReason: "end_turn" }], + ["metadata", { + metrics: { latencyMs: 100 }, + usage: { inputTokens: 10, outputTokens: 5, totalTokens: 15 } + }] + ])) + } + return Effect.succeed(jsonResponse(request, {})) + }) + + const parts = yield* LanguageModel.streamText({ + prompt: "Hi" + }).pipe( + Stream.runCollect, + Effect.provide(AmazonBedrockLanguageModel.model("anthropic.claude-3-5-sonnet-20241022-v2:0")), + Effect.provide(layer) + ) + + const partsArray = globalThis.Array.from(parts) + const finishPart = partsArray.find((p) => p.type === "finish") + assert.isDefined(finishPart) + if (finishPart?.type !== "finish") return + assert.strictEqual(finishPart.usage.inputTokens.cacheRead, 0) + assert.strictEqual(finishPart.usage.inputTokens.cacheWrite, 0) + assert.strictEqual(finishPart.usage.inputTokens.total, 10) + })) + + it.effect("cacheReadInputTokens: 0 is preserved as 0, not undefined", () => + Effect.gen(function*() { + const layer = makeTestLayer((request) => { + if (request.url.includes("converse-stream")) { + return Effect.succeed(eventStreamResponse(request, [ + ["messageStart", { role: "assistant" }], + ["contentBlockStart", { contentBlockIndex: 0, start: {} }], + ["contentBlockDelta", { contentBlockIndex: 0, delta: { text: "Hi" } }], + ["contentBlockStop", { contentBlockIndex: 0 }], + ["messageStop", { stopReason: "end_turn" }], + ["metadata", { + metrics: { latencyMs: 100 }, + usage: { + inputTokens: 10, + outputTokens: 5, + totalTokens: 15, + cacheReadInputTokens: 0 + } + }] + ])) + } + return Effect.succeed(jsonResponse(request, {})) + }) + + const parts = yield* LanguageModel.streamText({ + prompt: "Hi" + }).pipe( + Stream.runCollect, + Effect.provide(AmazonBedrockLanguageModel.model("anthropic.claude-3-5-sonnet-20241022-v2:0")), + Effect.provide(layer) + ) + + const partsArray = globalThis.Array.from(parts) + const finishPart = partsArray.find((p) => p.type === "finish") + assert.isDefined(finishPart) + if (finishPart?.type !== "finish") return + assert.strictEqual(finishPart.usage.inputTokens.cacheRead, 0) + })) + + it.effect("includes trace metadata in finish part", () => + Effect.gen(function*() { + const layer = makeTestLayer((request) => { + if (request.url.includes("converse-stream")) { + return Effect.succeed(eventStreamResponse(request, [ + ["messageStart", { role: "assistant" }], + ["contentBlockStart", { contentBlockIndex: 0, start: {} }], + ["contentBlockDelta", { contentBlockIndex: 0, delta: { text: "Hi" } }], + ["contentBlockStop", { contentBlockIndex: 0 }], + ["messageStop", { stopReason: "end_turn" }], + ["metadata", { + metrics: { latencyMs: 100 }, + usage: { inputTokens: 10, outputTokens: 5, totalTokens: 15 }, + trace: { + guardrail: { modelOutput: ["safe content"] } + } + }] + ])) + } + return Effect.succeed(jsonResponse(request, {})) + }) + + const parts = yield* LanguageModel.streamText({ + prompt: "Hi" + }).pipe( + Stream.runCollect, + Effect.provide(AmazonBedrockLanguageModel.model("anthropic.claude-3-5-sonnet-20241022-v2:0")), + Effect.provide(layer) + ) + + const partsArray = globalThis.Array.from(parts) + const finishPart = partsArray.find((p) => p.type === "finish") + assert.isDefined(finishPart) + if (finishPart?.type !== "finish") return + const metadata = finishPart.metadata as any + assert.isDefined(metadata?.bedrock?.trace) + })) + + it.effect("does not emit finish if error arrives after messageStop", () => + Effect.gen(function*() { + const layer = makeTestLayer((request) => { + if (request.url.includes("converse-stream")) { + return Effect.succeed(eventStreamResponse(request, [ + ["messageStart", { role: "assistant" }], + ["contentBlockStart", { contentBlockIndex: 0, start: {} }], + ["contentBlockDelta", { contentBlockIndex: 0, delta: { text: "Hi" } }], + ["contentBlockStop", { contentBlockIndex: 0 }], + ["messageStop", { stopReason: "end_turn" }], + // Error instead of metadata — finish should NOT be emitted + ["internalServerException", { message: "Something went wrong" }] + ])) + } + return Effect.succeed(jsonResponse(request, {})) + }) + + const parts = yield* LanguageModel.streamText({ + prompt: "Hi" + }).pipe( + Stream.runCollect, + Effect.provide(AmazonBedrockLanguageModel.model("anthropic.claude-3-5-sonnet-20241022-v2:0")), + Effect.provide(layer) + ) + + const partsArray = globalThis.Array.from(parts) + const finishParts = partsArray.filter((p) => p.type === "finish") + assert.strictEqual(finishParts.length, 0) + const errorParts = partsArray.filter((p) => p.type === "error") + assert.strictEqual(errorParts.length, 1) + })) + + it.effect("does not emit finish if only metadata arrives without messageStop", () => + Effect.gen(function*() { + const layer = makeTestLayer((request) => { + if (request.url.includes("converse-stream")) { + return Effect.succeed(eventStreamResponse(request, [ + ["messageStart", { role: "assistant" }], + ["contentBlockStart", { contentBlockIndex: 0, start: {} }], + ["contentBlockDelta", { contentBlockIndex: 0, delta: { text: "Hi" } }], + ["contentBlockStop", { contentBlockIndex: 0 }], + // No messageStop — only metadata + ["metadata", { + metrics: { latencyMs: 100 }, + usage: { inputTokens: 10, outputTokens: 5, totalTokens: 15 } + }] + ])) + } + return Effect.succeed(jsonResponse(request, {})) + }) + + const parts = yield* LanguageModel.streamText({ + prompt: "Hi" + }).pipe( + Stream.runCollect, + Effect.provide(AmazonBedrockLanguageModel.model("anthropic.claude-3-5-sonnet-20241022-v2:0")), + Effect.provide(layer) + ) + + const partsArray = globalThis.Array.from(parts) + const finishParts = partsArray.filter((p) => p.type === "finish") + assert.strictEqual(finishParts.length, 0) + })) + + it.effect("maps max_tokens stop reason to length in streaming", () => + Effect.gen(function*() { + const layer = makeTestLayer((request) => { + if (request.url.includes("converse-stream")) { + return Effect.succeed(eventStreamResponse(request, [ + ["messageStart", { role: "assistant" }], + ["contentBlockStart", { contentBlockIndex: 0, start: {} }], + ["contentBlockDelta", { contentBlockIndex: 0, delta: { text: "Truncated" } }], + ["contentBlockStop", { contentBlockIndex: 0 }], + ["messageStop", { stopReason: "max_tokens" }], + ["metadata", { + metrics: { latencyMs: 100 }, + usage: { inputTokens: 10, outputTokens: 50, totalTokens: 60 } + }] + ])) + } + return Effect.succeed(jsonResponse(request, {})) + }) + + const parts = yield* LanguageModel.streamText({ + prompt: "Write a long essay" + }).pipe( + Stream.runCollect, + Effect.provide(AmazonBedrockLanguageModel.model("anthropic.claude-3-5-sonnet-20241022-v2:0")), + Effect.provide(layer) + ) + + const partsArray = globalThis.Array.from(parts) + const finishPart = partsArray.find((p) => p.type === "finish") + assert.isDefined(finishPart) + if (finishPart?.type !== "finish") return + assert.strictEqual(finishPart.reason, "length") + })) + + it.effect("streams cached tokens alongside tool call content blocks", () => + Effect.gen(function*() { + const toolParams = { query: "test" } + + const layer = makeTestLayer((request) => { + if (request.url.includes("converse-stream")) { + return Effect.succeed(eventStreamResponse(request, [ + ["messageStart", { role: "assistant" }], + ["contentBlockStart", { + contentBlockIndex: 0, + start: { + toolUse: { + name: "SearchTool", + toolUseId: "tool_1" + } + } + }], + ["contentBlockDelta", { + contentBlockIndex: 0, + delta: { toolUse: { input: JSON.stringify(toolParams) } } + }], + ["contentBlockStop", { contentBlockIndex: 0 }], + ["messageStop", { stopReason: "tool_use" }], + ["metadata", { + metrics: { latencyMs: 100 }, + usage: { + inputTokens: 20, + outputTokens: 10, + totalTokens: 130, + cacheReadInputTokens: 80, + cacheWriteInputTokens: 20 + } + }] + ])) + } + return Effect.succeed(jsonResponse(request, {})) + }) + + const SearchTool = Tool.make("SearchTool", { + description: "Search", + parameters: Schema.Struct({ query: Schema.String }), + success: Schema.String + }) + + const toolkit = Toolkit.make(SearchTool) + const toolkitLayer = toolkit.toLayer({ + SearchTool: () => Effect.succeed("result") + }) + + const parts = yield* LanguageModel.streamText({ + prompt: "search", + toolkit, + disableToolCallResolution: true + }).pipe( + Stream.runCollect, + Effect.provide(AmazonBedrockLanguageModel.model("anthropic.claude-3-5-sonnet-20241022-v2:0")), + Effect.provide(toolkitLayer), + Effect.provide(layer) + ) + + const partsArray = globalThis.Array.from(parts) + + const toolCall = partsArray.find((p) => p.type === "tool-call") + assert.isDefined(toolCall) + + const finishPart = partsArray.find((p) => p.type === "finish") + assert.isDefined(finishPart) + if (finishPart?.type !== "finish") return + assert.strictEqual(finishPart.usage.inputTokens.cacheRead, 80) + assert.strictEqual(finishPart.usage.inputTokens.cacheWrite, 20) + assert.strictEqual(finishPart.usage.inputTokens.total, 120) + assert.strictEqual(finishPart.reason, "tool-calls") + })) + }) + + describe("transformClient", () => { + it.effect("bearer token auth is mutually exclusive with SigV4 signature", () => + Effect.gen(function*() { + let capturedAuthHeader: string | undefined + + // Works because mapRequest appends after SigV4, overwriting the same lowercase "authorization" key + const layer = AmazonBedrockClient.layer({ + accessKeyId: "dummy-key", + secretAccessKey: Redacted.make("dummy-secret"), + region: "us-east-1", + transformClient: (client) => + HttpClient.mapRequest(client, (request) => + HttpClientRequest.setHeader(request, "authorization", "Bearer my-bearer-token")) + }).pipe( + Layer.provide(Layer.succeed( + HttpClient.HttpClient, + makeHttpClient((request) => { + capturedAuthHeader = request.headers["authorization"] + return Effect.succeed(jsonResponse(request, { + output: { + message: { + role: "assistant", + content: [{ text: "Hello!" }] + } + }, + metrics: { latencyMs: 100 }, + usage: { inputTokens: 10, outputTokens: 5, totalTokens: 15 }, + stopReason: "end_turn" + })) + }) + )) + ) + + yield* LanguageModel.generateText({ + prompt: "Hello" + }).pipe( + Effect.provide(AmazonBedrockLanguageModel.model("anthropic.claude-3-5-sonnet-20241022-v2:0")), + Effect.provide(layer) + ) + + assert.isDefined(capturedAuthHeader) + assert.strictEqual(capturedAuthHeader, "Bearer my-bearer-token") + assert.isFalse(capturedAuthHeader!.startsWith("AWS4-HMAC-SHA256")) + })) + }) +}) + +// ============================================================================= +// Test helpers +// ============================================================================= + +const makeTestLayer = ( + handler: ( + request: HttpClientRequest.HttpClientRequest + ) => Effect.Effect +) => + AmazonBedrockClient.layer({ + accessKeyId: "test-key", + secretAccessKey: Redacted.make("test-secret"), + region: "us-east-1" + }).pipe( + Layer.provide(Layer.succeed( + HttpClient.HttpClient, + makeHttpClient(handler) + )) + ) + +const makeHttpClient = ( + handler: ( + request: HttpClientRequest.HttpClientRequest + ) => Effect.Effect +) => + HttpClient.makeWith( + Effect.fnUntraced(function*(requestEffect) { + const request = yield* requestEffect + return yield* handler(request) + }), + Effect.succeed as HttpClient.HttpClient.Preprocess + ) + +const jsonResponse = ( + request: HttpClientRequest.HttpClientRequest, + body: unknown +): HttpClientResponse.HttpClientResponse => + HttpClientResponse.fromWeb( + request, + new Response(JSON.stringify(body), { + status: 200, + headers: { "content-type": "application/json" } + }) + ) + +const eventStreamResponse = ( + request: HttpClientRequest.HttpClientRequest, + events: ReadonlyArray +): HttpClientResponse.HttpClientResponse => { + const codec = new EventStreamCodec(toUtf8, fromUtf8) + const chunks: Array = [] + + for (const [eventType, payload] of events) { + const body = fromUtf8(JSON.stringify(payload)) + const message = codec.encode({ + headers: { + ":event-type": { type: "string", value: eventType }, + ":content-type": { type: "string", value: "application/json" }, + ":message-type": { type: "string", value: "event" } + }, + body + }) + chunks.push(message) + } + + // Concatenate all encoded event chunks into a single Uint8Array + const totalLength = chunks.reduce((acc, c) => acc + c.length, 0) + const combined = new Uint8Array(totalLength) + let offset = 0 + for (const chunk of chunks) { + combined.set(chunk, offset) + offset += chunk.length + } + + return HttpClientResponse.fromWeb( + request, + new Response(combined, { + status: 200, + headers: { "content-type": "application/vnd.amazon.eventstream" } + }) + ) +} diff --git a/packages/ai/amazon-bedrock/test/AmazonBedrockSmoke.test.ts b/packages/ai/amazon-bedrock/test/AmazonBedrockSmoke.test.ts new file mode 100644 index 0000000000..a82d7e6d98 --- /dev/null +++ b/packages/ai/amazon-bedrock/test/AmazonBedrockSmoke.test.ts @@ -0,0 +1,176 @@ +import { AmazonBedrockClient, AmazonBedrockLanguageModel } from "@effect/ai-amazon-bedrock" +import { assert, describe, it } from "@effect/vitest" +import { Config, Effect, Layer, Schema, Stream } from "effect" +import { LanguageModel, Tool, Toolkit } from "effect/unstable/ai" +import { FetchHttpClient } from "effect/unstable/http" + +// Smoke tests against real Bedrock API. +// Silently skipped when AWS credentials are not available. + +const hasCredentials = !!process.env.AWS_ACCESS_KEY_ID + +const TestLayer = AmazonBedrockClient.layerConfig({ + accessKeyId: Config.string("AWS_ACCESS_KEY_ID"), + secretAccessKey: Config.redacted("AWS_SECRET_ACCESS_KEY"), + ...(process.env.AWS_SESSION_TOKEN + ? { sessionToken: Config.redacted("AWS_SESSION_TOKEN") } + : {}), + region: Config.string("AWS_REGION").pipe(Config.withDefault("us-east-1")) +}).pipe(Layer.provide(FetchHttpClient.layer)) + +const NovaModel = AmazonBedrockLanguageModel.model("amazon.nova-micro-v1:0", { + inferenceConfig: { maxTokens: 100 } +}) + +// --------------------------------------------------------------------------- +// Tools used across tests +// --------------------------------------------------------------------------- + +const GetWeather = Tool.make("GetWeather", { + description: "Get the current weather for a city", + parameters: Schema.Struct({ city: Schema.String }), + success: Schema.String +}) + +const weatherToolkit = Toolkit.make(GetWeather) + +// --------------------------------------------------------------------------- +// Text generation +// --------------------------------------------------------------------------- + +describe("AmazonBedrock Smoke Tests", () => { + it.effect.runIf(hasCredentials)("Converse: minimal text generation", () => + Effect.gen(function*() { + const result = yield* LanguageModel.generateText({ + prompt: "Say hi" + }).pipe( + Effect.provide(NovaModel), + Effect.provide(TestLayer) + ) + + const textPart = result.content.find((part) => part.type === "text") + assert.isDefined(textPart) + if (textPart?.type !== "text") return + assert.isTrue(textPart.text.length > 0) + + const finishPart = result.content.find((part) => part.type === "finish") + assert.isDefined(finishPart) + if (finishPart?.type !== "finish") return + assert.isDefined(finishPart.usage) + assert.isTrue((finishPart.usage.inputTokens.total ?? 0) > 0) + }), 30_000) + + it.effect.runIf(hasCredentials)("ConverseStream: minimal streaming", () => + Effect.gen(function*() { + const parts = yield* LanguageModel.streamText({ + prompt: "Say hi" + }).pipe( + Stream.runCollect, + Effect.provide(NovaModel), + Effect.provide(TestLayer) + ) + + const partsArray = globalThis.Array.from(parts) + + const textDeltas = partsArray.filter((p) => p.type === "text-delta") + assert.isTrue(textDeltas.length >= 1) + + const finishPart = partsArray.find((p) => p.type === "finish") + assert.isDefined(finishPart) + if (finishPart?.type !== "finish") return + assert.isDefined(finishPart.usage) + assert.isTrue((finishPart.usage.inputTokens.total ?? 0) > 0) + }), 30_000) + + // --------------------------------------------------------------------------- + // Tool calling + // --------------------------------------------------------------------------- + + it.effect.runIf(hasCredentials)("Converse: tool call", () => + Effect.gen(function*() { + const result = yield* LanguageModel.generateText({ + prompt: "What is the weather in London?", + toolkit: weatherToolkit, + disableToolCallResolution: true + }).pipe( + Effect.provide(NovaModel), + Effect.provide(TestLayer) + ) + + const toolCall = result.content.find((part) => part.type === "tool-call") + assert.isDefined(toolCall) + if (toolCall?.type !== "tool-call") return + assert.strictEqual(toolCall.name, "GetWeather") + assert.isObject(toolCall.params) + assert.isString((toolCall.params as any).city) + }), 30_000) + + it.effect.runIf(hasCredentials)("ConverseStream: tool call", () => + Effect.gen(function*() { + const parts = yield* LanguageModel.streamText({ + prompt: "What is the weather in London?", + toolkit: weatherToolkit, + disableToolCallResolution: true + }).pipe( + Stream.runCollect, + Effect.provide(NovaModel), + Effect.provide(TestLayer) + ) + + const partsArray = globalThis.Array.from(parts) + + const toolParamsStart = partsArray.find((p) => p.type === "tool-params-start") + assert.isDefined(toolParamsStart) + + const toolParamsDeltas = partsArray.filter((p) => p.type === "tool-params-delta") + assert.isTrue(toolParamsDeltas.length >= 1) + + const toolParamsEnd = partsArray.find((p) => p.type === "tool-params-end") + assert.isDefined(toolParamsEnd) + + const toolCall = partsArray.find((p) => p.type === "tool-call") + assert.isDefined(toolCall) + if (toolCall?.type !== "tool-call") return + assert.strictEqual(toolCall.name, "GetWeather") + assert.isObject(toolCall.params) + assert.isString((toolCall.params as any).city) + }), 30_000) + + // --------------------------------------------------------------------------- + // JSON response format + // --------------------------------------------------------------------------- + + it.effect.runIf(hasCredentials)("Converse: JSON response format", () => + Effect.gen(function*() { + const result = yield* LanguageModel.generateObject({ + prompt: "Describe a person named Alice who is 30 years old", + schema: Schema.Struct({ + name: Schema.String, + age: Schema.Number + }), + objectName: "Person" + }).pipe( + Effect.provide(NovaModel), + Effect.provide(TestLayer) + ) + + assert.isString(result.value.name) + assert.isNumber(result.value.age) + }), 30_000) + + // --------------------------------------------------------------------------- + // Not included in this port — placeholder tests + // --------------------------------------------------------------------------- + + it.effect.skip("Reasoning / extended thinking (requires Anthropic model on Bedrock)", () => Effect.void) + + it.effect.skip("Document input (PDF, CSV, etc.)", () => Effect.void) + + it.effect.skip("Image input", () => Effect.void) + + it.effect.skip("Cache points (CachePointBlock)", () => Effect.void) + + it.effect.skip("Provider-defined Anthropic tools (computer use, web search, etc.)", () => Effect.void) + + it.effect.skip("Config override via withConfigOverride", () => Effect.void) +}) diff --git a/packages/ai/amazon-bedrock/tsconfig.json b/packages/ai/amazon-bedrock/tsconfig.json new file mode 100644 index 0000000000..731c814309 --- /dev/null +++ b/packages/ai/amazon-bedrock/tsconfig.json @@ -0,0 +1,9 @@ +{ + "$schema": "http://json.schemastore.org/tsconfig", + "extends": "../../../tsconfig.base.json", + "include": ["src"], + "references": [ + { "path": "../../effect" }, + { "path": "../anthropic" } + ] +} diff --git a/packages/ai/amazon-bedrock/vitest.config.ts b/packages/ai/amazon-bedrock/vitest.config.ts new file mode 100644 index 0000000000..c8a52c1826 --- /dev/null +++ b/packages/ai/amazon-bedrock/vitest.config.ts @@ -0,0 +1,6 @@ +import { mergeConfig, type ViteUserConfig } from "vitest/config" +import shared from "../../../vitest.shared.ts" + +const config: ViteUserConfig = {} + +export default mergeConfig(shared, config) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 16e81958ac..0ea49ca25e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -6,10 +6,10 @@ settings: patchedDependencies: '@changesets/assemble-release-plan': - hash: 29cf7a343aefd3c59ad238a4cb067f0a1a0adbeed07b6d57d6f31d6c80a5e25e + hash: ypa3rdjn2zks4b2vtycade3ity path: patches/@changesets__assemble-release-plan.patch '@changesets/get-github-info': - hash: 314478eeae7ab2776d847d3b2b7ea1cd4231c65907e85597dacd0bea56355bc9 + hash: 64xhrmb5y55535dijmjkayit2m path: patches/@changesets__get-github-info.patch importers: @@ -237,6 +237,25 @@ importers: specifier: ^8.0.0 version: 8.0.0 + packages/ai/amazon-bedrock: + dependencies: + '@smithy/eventstream-codec': + specifier: ^4.0.2 + version: 4.2.13 + '@smithy/util-utf8': + specifier: ^4.0.0 + version: 4.2.2 + aws4fetch: + specifier: ^1.0.20 + version: 1.0.20 + devDependencies: + '@effect/ai-anthropic': + specifier: workspace:^ + version: link:../anthropic + effect: + specifier: workspace:^ + version: link:../../effect + packages/ai/anthropic: devDependencies: effect: @@ -928,6 +947,17 @@ packages: '@asamuzakjp/nwsapi@2.3.9': resolution: {integrity: sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q==} + '@aws-crypto/crc32@5.2.0': + resolution: {integrity: sha512-nLbCWqQNgUiwwtFsen1AdzAtvuLRsQS8rYgMuxCrdKf9kOssamGLuPwyTY9wyYblNr9+1XM8v6zoDTPPSIeANg==} + engines: {node: '>=16.0.0'} + + '@aws-crypto/util@5.2.0': + resolution: {integrity: sha512-4RkU9EsI6ZpBve5fseQlGNUWKMa1RLPQ1dnjnQoe07ldfIzcsGb5hC5W0Dm7u423KWzawlrpbjXBrXCEv9zazQ==} + + '@aws-sdk/types@3.973.7': + resolution: {integrity: sha512-reXRwoJ6CfChoqAsBszUYajAF8Z2LRE+CRcKocvFSMpIiLOtYU3aJ9trmn6VVPAzbbY5LXF+FfmUslbXk1SYFg==} + engines: {node: '>=20.0.0'} + '@azure-rest/core-client@2.6.0': resolution: {integrity: sha512-iuFKDm8XPzNxPfRjhyU5/xKZmcRDzSuEghXDHHk4MjBV/wFL34GmYVBZnn9wmuoLBeS1qAw9ceMdaeJBPcB1QQ==} engines: {node: '>=20.0.0'} @@ -2617,6 +2647,42 @@ packages: '@sinonjs/fake-timers@10.3.0': resolution: {integrity: sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==} + '@smithy/eventstream-codec@4.2.13': + resolution: {integrity: sha512-vYahwBAtRaAcFbOmE9aLr12z7RiHYDSLcnogSdxfm7kKfsNa3wH+NU5r7vTeB5rKvLsWyPjVX8iH94brP7umiQ==} + engines: {node: '>=18.0.0'} + + '@smithy/is-array-buffer@2.2.0': + resolution: {integrity: sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==} + engines: {node: '>=14.0.0'} + + '@smithy/is-array-buffer@4.2.2': + resolution: {integrity: sha512-n6rQ4N8Jj4YTQO3YFrlgZuwKodf4zUFs7EJIWH86pSCWBaAtAGBFfCM7Wx6D2bBJ2xqFNxGBSrUWswT3M0VJow==} + engines: {node: '>=18.0.0'} + + '@smithy/types@4.14.0': + resolution: {integrity: sha512-OWgntFLW88kx2qvf/c/67Vno1yuXm/f9M7QFAtVkkO29IJXGBIg0ycEaBTH0kvCtwmvZxRujrgP5a86RvsXJAQ==} + engines: {node: '>=18.0.0'} + + '@smithy/util-buffer-from@2.2.0': + resolution: {integrity: sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==} + engines: {node: '>=14.0.0'} + + '@smithy/util-buffer-from@4.2.2': + resolution: {integrity: sha512-FDXD7cvUoFWwN6vtQfEta540Y/YBe5JneK3SoZg9bThSoOAC/eGeYEua6RkBgKjGa/sz6Y+DuBZj3+YEY21y4Q==} + engines: {node: '>=18.0.0'} + + '@smithy/util-hex-encoding@4.2.2': + resolution: {integrity: sha512-Qcz3W5vuHK4sLQdyT93k/rfrUwdJ8/HZ+nMUOyGdpeGA1Wxt65zYwi3oEl9kOM+RswvYq90fzkNDahPS8K0OIg==} + engines: {node: '>=18.0.0'} + + '@smithy/util-utf8@2.3.0': + resolution: {integrity: sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==} + engines: {node: '>=14.0.0'} + + '@smithy/util-utf8@4.2.2': + resolution: {integrity: sha512-75MeYpjdWRe8M5E3AW0O4Cx3UadweS+cwdXjwYGBW5h/gxxnbeZ877sLPX/ZJA9GVTlL/qG0dXP29JWFCD1Ayw==} + engines: {node: '>=18.0.0'} + '@solidjs/testing-library@0.8.10': resolution: {integrity: sha512-qdeuIerwyq7oQTIrrKvV0aL9aFeuwTd86VYD3afdq5HYEwoox1OBTJy4y8A3TFZr8oAR0nujYgCzY/8wgHGfeQ==} engines: {node: '>= 14'} @@ -3078,6 +3144,9 @@ packages: resolution: {integrity: sha512-NZKeq9AfyQvEeNlN0zSYAaWrmBffJh3IELMZfRpJVWgrpEbtEpnjvzqBPf+mxoI287JohRDoa+/nsfqqiZmF6g==} engines: {node: '>= 6.0.0'} + aws4fetch@1.0.20: + resolution: {integrity: sha512-/djoAN709iY65ETD6LKCtyyEI04XIBP5xVvfmNxsEP0uJB5tyaGBztSryRr4HqMStr9R06PisQE7m9zDTXKu6g==} + b4a@1.8.0: resolution: {integrity: sha512-qRuSmNSkGQaHwNbM7J78Wwy+ghLEYF1zNrSeMxj4Kgw6y33O3mXcQ6Ie9fRvfU/YnxWkOchPXbaLb73TkIsfdg==} peerDependencies: @@ -4397,7 +4466,6 @@ packages: libsql@0.5.29: resolution: {integrity: sha512-8lMP8iMgiBzzoNbAPQ59qdVcj6UaE/Vnm+fiwX4doX4Narook0a4GPKWBEv+CR8a1OwbfkgL18uBfBjWdF0Fzg==} - cpu: [x64, arm64, wasm32, arm] os: [darwin, linux, win32] lighthouse-logger@1.4.2: @@ -6430,6 +6498,23 @@ snapshots: '@asamuzakjp/nwsapi@2.3.9': {} + '@aws-crypto/crc32@5.2.0': + dependencies: + '@aws-crypto/util': 5.2.0 + '@aws-sdk/types': 3.973.7 + tslib: 2.8.1 + + '@aws-crypto/util@5.2.0': + dependencies: + '@aws-sdk/types': 3.973.7 + '@smithy/util-utf8': 2.3.0 + tslib: 2.8.1 + + '@aws-sdk/types@3.973.7': + dependencies: + '@smithy/types': 4.14.0 + tslib: 2.8.1 + '@azure-rest/core-client@2.6.0': dependencies: '@azure/abort-controller': 2.1.2 @@ -6991,7 +7076,7 @@ snapshots: resolve-from: 5.0.0 semver: 7.7.4 - '@changesets/assemble-release-plan@6.0.9(patch_hash=29cf7a343aefd3c59ad238a4cb067f0a1a0adbeed07b6d57d6f31d6c80a5e25e)': + '@changesets/assemble-release-plan@6.0.9(patch_hash=ypa3rdjn2zks4b2vtycade3ity)': dependencies: '@changesets/errors': 0.2.0 '@changesets/get-dependents-graph': 2.1.3 @@ -7006,7 +7091,7 @@ snapshots: '@changesets/changelog-github@0.6.0': dependencies: - '@changesets/get-github-info': 0.8.0(patch_hash=314478eeae7ab2776d847d3b2b7ea1cd4231c65907e85597dacd0bea56355bc9) + '@changesets/get-github-info': 0.8.0(patch_hash=64xhrmb5y55535dijmjkayit2m) '@changesets/types': 6.1.0 dotenv: 8.6.0 transitivePeerDependencies: @@ -7015,7 +7100,7 @@ snapshots: '@changesets/cli@2.30.0(@types/node@25.6.0)': dependencies: '@changesets/apply-release-plan': 7.1.0 - '@changesets/assemble-release-plan': 6.0.9(patch_hash=29cf7a343aefd3c59ad238a4cb067f0a1a0adbeed07b6d57d6f31d6c80a5e25e) + '@changesets/assemble-release-plan': 6.0.9(patch_hash=ypa3rdjn2zks4b2vtycade3ity) '@changesets/changelog-git': 0.2.1 '@changesets/config': 3.1.3 '@changesets/errors': 0.2.0 @@ -7065,7 +7150,7 @@ snapshots: picocolors: 1.1.1 semver: 7.7.4 - '@changesets/get-github-info@0.8.0(patch_hash=314478eeae7ab2776d847d3b2b7ea1cd4231c65907e85597dacd0bea56355bc9)': + '@changesets/get-github-info@0.8.0(patch_hash=64xhrmb5y55535dijmjkayit2m)': dependencies: dataloader: 1.4.0 node-fetch: 2.7.0 @@ -7074,7 +7159,7 @@ snapshots: '@changesets/get-release-plan@4.0.15': dependencies: - '@changesets/assemble-release-plan': 6.0.9(patch_hash=29cf7a343aefd3c59ad238a4cb067f0a1a0adbeed07b6d57d6f31d6c80a5e25e) + '@changesets/assemble-release-plan': 6.0.9(patch_hash=ypa3rdjn2zks4b2vtycade3ity) '@changesets/config': 3.1.3 '@changesets/pre': 2.0.2 '@changesets/read': 0.6.7 @@ -8158,6 +8243,49 @@ snapshots: dependencies: '@sinonjs/commons': 3.0.1 + '@smithy/eventstream-codec@4.2.13': + dependencies: + '@aws-crypto/crc32': 5.2.0 + '@smithy/types': 4.14.0 + '@smithy/util-hex-encoding': 4.2.2 + tslib: 2.8.1 + + '@smithy/is-array-buffer@2.2.0': + dependencies: + tslib: 2.8.1 + + '@smithy/is-array-buffer@4.2.2': + dependencies: + tslib: 2.8.1 + + '@smithy/types@4.14.0': + dependencies: + tslib: 2.8.1 + + '@smithy/util-buffer-from@2.2.0': + dependencies: + '@smithy/is-array-buffer': 2.2.0 + tslib: 2.8.1 + + '@smithy/util-buffer-from@4.2.2': + dependencies: + '@smithy/is-array-buffer': 4.2.2 + tslib: 2.8.1 + + '@smithy/util-hex-encoding@4.2.2': + dependencies: + tslib: 2.8.1 + + '@smithy/util-utf8@2.3.0': + dependencies: + '@smithy/util-buffer-from': 2.2.0 + tslib: 2.8.1 + + '@smithy/util-utf8@4.2.2': + dependencies: + '@smithy/util-buffer-from': 4.2.2 + tslib: 2.8.1 + '@solidjs/testing-library@0.8.10(solid-js@1.9.12)': dependencies: '@testing-library/dom': 10.4.1 @@ -8733,6 +8861,8 @@ snapshots: aws-ssl-profiles@1.1.2: {} + aws4fetch@1.0.20: {} + b4a@1.8.0: {} babel-jest@29.7.0(@babel/core@7.29.0): diff --git a/tsconfig.json b/tsconfig.json index 103a15f497..2a2cc421ba 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -37,6 +37,8 @@ // that would otherwise be considered cyclic dependencies (e.g. loading `@effect/sql-sqlite-node` in the test // suite of `@effect/platform-node` which itself depends on `@effect/platform-node`). "effect/*": ["./packages/effect/src/*.ts"], + "@effect/ai-amazon-bedrock": ["./packages/ai/amazon-bedrock/src/index.ts"], + "@effect/ai-amazon-bedrock/*": ["./packages/ai/amazon-bedrock/src/*.ts"], "@effect/ai-anthropic": ["./packages/ai/anthropic/src/index.ts"], "@effect/ai-anthropic/*": ["./packages/ai/anthropic/src/*.ts"], "@effect/ai-openai-compat": ["./packages/ai/openai-compat/src/index.ts"], diff --git a/tsconfig.packages.json b/tsconfig.packages.json index 11b7246e05..af2731e240 100644 --- a/tsconfig.packages.json +++ b/tsconfig.packages.json @@ -5,6 +5,7 @@ "references": [ { "path": "ai-docs" }, { "path": "packages/effect" }, + { "path": "packages/ai/amazon-bedrock" }, { "path": "packages/ai/anthropic" }, { "path": "packages/ai/openai-compat" }, { "path": "packages/ai/openai" },