AWS Bedrock

@crvouga/mockingbird-service-bedrock

Stateful, scriptable mock of AWS Bedrock Runtime (Converse, ConverseStream, InvokeModel incl. Titan embeddings, Nova Sonic bidirectional streams over h2c) and AgentCore InvokeHarness, with exact event-stream framing.

Work in progress. Usable, but incomplete: operations, response shapes and options can still change between releases. Pin an exact version. Check the README's "Deliberately not modelled" notes before relying on it.
Work in progressAI & speech Runs in the browser5 of 6 operationsIn-process fetchNode servermockingbird-bedrock
$npm install -D @crvouga/mockingbird-service-bedrock

Playground

Requests go to the real mock running in this tab. State persists across requests, and the journal shows what it received. A green dot marks operations whose sample succeeds as-is; others need ids from earlier responses.

Loads the real @crvouga/mockingbird-service-bedrock into this tab on first send
https://bedrock-runtime.us-east-1.amazonaws.com

AWS SigV4 (service `bedrock` / `bedrock-agentcore`). Accepted without verification; the access key id selects a namespace.

Send a request to see the mock's response.

Operations

5 of 6 operations in the vendored contract are mocked. Shapes follow the AWS SDK v3 Smithy schemas (restJson1). Errors carry `x-amzn-ErrorType: <Name>:http://internal.amazon.com/coral/com.amazon.bedrock/` and a `{"message"}` body, as Bedrock sends them.

MethodPathOperationStatus
POST/model/global.anthropic.claude-sonnet-4-6/converseConverse
POST/model/global.anthropic.claude-sonnet-4-6/converse-streamConverseStream
POST/model/global.anthropic.claude-sonnet-4-6/invokeInvokeModelTitan text embeddings (`amazon.titan-embed-text-*`: `{inputText, dimensions, normalize}` → `{embedding, inputTextTokenCount}`) or an Anth…
POST/model/global.anthropic.claude-sonnet-4-6/invoke-with-response-streamInvokeModelWithResponseStreamNot mocked
POST/model/global.anthropic.claude-sonnet-4-6/invoke-with-bidirectional-streamInvokeModelWithBidirectionalStreamNova Sonic. HTTP/2 duplex: the request body is an event stream of `chunk` events (`{bytes: base64(JSON {event})}`, SigV4-wrapped) that st…
POST/harnesses/invokeInvokeHarnessAgentCore data plane (`bedrock-agentcore.<region>.amazonaws.com`).

Why each gap exists is recorded in SUPPORT.md.

Documentation

The package README, the same file that ships in the npm tarball.Edit on GitHub

Stateful, scriptable mock of Amazon Bedrock Runtime for test suites: Converse, ConverseStream (byte-exact application/vnd.amazon.eventstream frames), InvokeModel (Anthropic Messages bodies and Titan text embeddings), InvokeModelWithBidirectionalStream (Nova Sonic over HTTP/2 duplex), and the AgentCore InvokeHarness event stream.

The mock never generates language. It replays scripts: a test says "when the chat model sees dizzy with report_rx_symptom available, emit this toolUse; after the tool result comes back, say this". Chat turns, approval cards, guardrail blocks, structured output and throttles become deterministic and take milliseconds.

  • Operation coverage: SUPPORT.md
  • Proven with the official clients our consumer pins: @aws-sdk/client-bedrock-runtime@3.1132.0, @aws-sdk/client-bedrock-agentcore@3.1074.0, @ai-sdk/amazon-bedrock@4.0.176 + ai@6.0.283.

Install

Shell
npm install -D @crvouga/mockingbird-service-bedrock

ESM only. Node >= 22 or Bun >= 1.2. No native dependencies. Serve it with npx mockingbird-bedrock serve (h2c + HTTP/1.1 on one port), createServer from ./server, or createRuntime with any Fetch server (HTTP/1.1 only — see below).

Usage

Point the app at it. No code change: every client honours the endpoint variables.

Shell
npx mockingbird-bedrock serve --port 8796
export AWS_ENDPOINT_URL_BEDROCK_RUNTIME=http://127.0.0.1:8796    # SDK v3, AI SDK, botocore
export AWS_ENDPOINT_URL_BEDROCK_AGENTCORE=http://127.0.0.1:8796  # AgentCore InvokeHarness
TypeScript
import { createServer } from "@crvouga/mockingbird-service-bedrock/server"

const bedrock = await createServer({ port: 8796 })
await fetch(`${bedrock.url}/__admin/scripts`, {
  method: "PUT",
  headers: { "content-type": "application/json" },
  body: JSON.stringify({
    scripts: [
      {
        id: "rx-symptom-approval",
        match: { modelId: "*sonnet*", lastUserText: { contains: "dizzy" }, toolsInclude: ["report_rx_symptom"] },
        turns: [
          {
            toolUse: { name: "report_rx_symptom", input: { symptoms: [{ symptomDefinitionId: 7, severity: 3 }] } },
            stopReason: "tool_use",
          },
          {
            expectToolResult: { name: "report_rx_symptom" },
            text: "I've flagged that for your care team.",
            chunkSize: 12,
            usage: { inputTokens: 1200, outputTokens: 40, cacheReadInputTokens: 900 },
          },
        ],
      },
    ],
  }),
})
// …the backend's streamText turn now emits the approval card; approving resumes the turn.
await bedrock.close()

Protocols

The AWS SDK v3 clients for Bedrock Runtime default to NodeHttp2Handler: against an http:// endpoint they speak h2c (cleartext HTTP/2 with prior knowledge), and Nova Sonic needs HTTP/2 duplex. The AI SDK and AgentCore use HTTP/1.1. mockingbird-bedrock serve and createServer sniff each connection's first bytes and serve both on one port. (serve --config from another service's CLI, and createRuntime behind a plain Fetch server, speak HTTP/1.1 only — fine for the AI SDK, not for the SDK v3 Bedrock client.)

Routes

Route Behaviour
POST /model/{modelId}/converse Converse JSON: output.message.content[] (text, toolUse, reasoningContent), stopReason, usage (with cacheRead/WriteInputTokens when a cachePoint is present), metrics, trace.guardrail (with guardrailConfig.trace: "enabled"), x-amzn-RequestId.
POST /model/{modelId}/converse-stream The same answer as event frames: messageStart, contentBlockStart (tool use), contentBlockDelta (text, toolUse.input partial JSON, reasoningContent), contentBlockStop, messageStop, metadata; exception frames mid-stream.
POST /model/{modelId}/invoke amazon.titan-embed-text-*: {embedding, inputTextTokenCount} — a deterministic unit vector from SHA-256(inputText) (1024-d by default; dimensions 256/512/1024). Claude models: an Anthropic Messages body in, a Messages response out.
POST /model/{modelId}/invoke-with-bidirectional-stream Nova Sonic (*sonic*), HTTP/2 duplex: reads chunk events as they arrive (SigV4 envelopes unwrapped), answers each user turn with textOutput + 24 kHz PCM tone audioOutput + toolUse, then usageEvent; completionEnd after sessionEnd.
POST /harnesses/invoke?harnessArn= AgentCore harness stream: messageStart, contentBlockDelta (text or toolResult[]), contentBlockStop, messageStop, metadata; validationException / internalServerException / runtimeClientError frames.

modelId is any model id, inference-profile id (global. / us.) or URL-encoded ARN. SigV4 is accepted without verification. Request checks Bedrock makes and our code branches on are enforced: role alternation, first/last message is the user (assistant prefill is rejected for Claude 4.5+), tool-use/tool-result pairing, toolConfig required with tool blocks, a document needs a sibling text block, temperature + top_p together on Claude 4.5+. Errors are x-amzn-ErrorType: <Name>:http://internal.amazon.com/coral/com.amazon.bedrock/ + {"message"}. Output longer than maxTokens (≈4 chars/token) is cut with stopReason: "max_tokens".

Scripts (PUT /__admin/scripts)

A script is {id, match?, turns, times?}. The first script (in insertion order) whose match accepts a call and has a turn for that point in the conversation answers it; times caps how many calls it answers.

  • Match keys: modelId (glob), operation (Converse, ConverseStream, InvokeModel, InvokeModelWithBidirectionalStream, InvokeHarness), lastUserText (string = contains, or {contains, regex, flags}), systemHash (SHA-256 hex of the system text blocks joined with \n), toolsInclude, toolChoice (auto / any / a tool name), hasDocument, hasImage, callIndex (0-based index of the call in the namespace).
  • Turn selection reads the conversation, not server state: turn n answers the call that comes after n assistant messages since the member last said something. So turn 0 is the first call of a user turn, turn 1 is the call that resumes after a tool result, and every new conversation starts over. expectToolResult: {name} makes a turn answer only when the last user message carries that tool's result. Nova Sonic counts answers within the session.
  • A turn is any of: text (streamed in chunkSize-character deltas, delayMsPerChunk mock-clock ms apart), reasoning, toolUse ({name, input, toolUseId?} or a list), json (structured output, rendered in the form the request asked for — see below), guardrail (true or {text, trace}: guardrail_intervened with Bedrock's refusal text and a trace), toolResult (harness), userTranscript (Nova Sonic), stopReason, usage, fault.
  • Structured output (json) goes out as text JSON for outputConfig.textFormat.structure.jsonSchema (Makor) and additionalModelRequestFields.output_config.format (AI SDK native), and as a toolUse of the forced tool for toolChoice: {tool} or {any} (the AI SDK's synthetic json tool, insight reports' record_chat_*).
  • Unscripted defaults, each counted as unscripted (GET /__admin/scriptsstats): chat → "OK." (PUT /__admin/settings {"defaultText"}); structured output → the minimal object valid against the request's schema; our intent classifier (its system prompt asks for {"category","confidence"}) → {"category":"general","confidence":0.9}; InvokeModel with a Claude body (the EMR scribe) → a 4-section SOAP JSON; Titan → the SHA-256 vector; InvokeHarness → an eligible_for_clinician_review prescreen summary; Nova Sonic → "OK." spoken.

Faults

A turn's fault (or a preset, POST /__admin/faults {"preset": "<name>", "count"?: n}, which applies to every model and harness call in the calling namespace):

Preset / fault Effect
throttling 429 ThrottlingException before the first chunk
mid_stream_exception (afterChunks, exceptionType) content chunks, then a modelStreamErrorException frame (harness: internalServerException)
mid_stream_throttling the same with a throttlingException frame
validation_exception / validation 400 ValidationException
max_tokens output cut in half, stopReason: "max_tokens"
latency (latencyMs) the response starts after 2 s on the mock clock
truncated_frame the stream stops half-way through a frame (both decoders throw)
model_timeout, service_unavailable, access_denied, internal_server 408 / 503 / 403 / 500 with the matching x-amzn-ErrorType

Chunk pacing and latency wait on the mock clock: freeze it (POST /__admin/clock {"freeze": true}) and advance it to release each chunk, so time-to-first-token tests are exact.

Admin (beyond the standard contract)

Route Effect
PUT /__admin/scripts Replace the namespace's scripts ({"scripts": [...]}); validated.
POST /__admin/scripts Add scripts (same ids overwrite).
GET /__admin/scripts Scripts plus stats (calls, scripted, unscripted, byScript, byFallback, byOperation).
DELETE /__admin/scripts[?id=] Remove one or all.
GET /__admin/model-metrics Just the stats.
GET/PUT /__admin/settings defaultText, chunkSize (16), delayMsPerChunk (0), audioTurnChunks (0: a Nova Sonic spoken turn ends at the audio contentEnd; n: after n audio frames).

The request journal (GET /__admin/requests) records per call only modelId, script (or unscripted:<default>), tool names, flags (cachePoint, guardrail, document, image, structured:<form>), stopReason and token counts — never prompt or message text.

Namespaces

x-mockingbird-namespace, a /ns/<name> prefix on the endpoint URL, or by credential: the SDKs cannot add headers, so map each worker's access key id: PUT /__admin/credentials {"credentials": {"<AWS_ACCESS_KEY_ID>": "<namespace>"}}.

Deliberately not modelled

  • Language: output only ever comes from scripts or the fixed defaults.
  • Real speech: Nova Sonic audio out is a 440 Hz PCM tone whose length follows the text; audio in is counted, never transcribed (a spoken turn matches with lastUserText: "").
  • InvokeModelWithResponseStream (no consumer calls it), guardrail evaluation itself (ApplyGuardrail; scripts decide when the guardrail intervenes), prompt caching arithmetic (cache token counts are 0 unless scripted), model-specific output token limits.
  • SigV4 signatures are not verified; the access key id only selects a namespace.

API

Export Kind Description
BedrockAPI class The in-process mock: fetch, reset, scripts(), putScripts(scripts, replace?), removeScripts(id?), stats(). Options: sqlite, now, namespace, settings, scripts, sleep.
createRuntime function The mock with the full service contract (health, admin, namespaces, SigV4 credentials, presets, scripts). Options: settings, scripts, clock, seed, adminKey, onLog, sqlite.
BEDROCK_PRESETS object Every named fault preset.
BEDROCK_NAMESPACE string The service name, "bedrock".
bedrockError function A Bedrock error response (status, x-amzn-ErrorType, {message}).
accessKeyCredential function The SigV4 access key id of a request (how credentials map to namespaces).
clockSleep function A sleep that waits on a (possibly frozen) mock clock.
titanEmbedding function The deterministic unit vector Titan answers with.
sampleSchema function The minimal instance of a JSON Schema (the unscripted structured output).
parseScript function Validate one script (what PUT /__admin/scripts runs).
MODEL_OPERATIONS, STOP_REASONS, TURN_FAULTS arrays The values operation, stopReason and fault accept.
DEFAULT_SETTINGS, DEFAULT_CHAT_TEXT, DEFAULT_CLASSIFIER, DEFAULT_SOAP_NOTE, GUARDRAIL_BLOCKED_TEXT values The defaults.
encodeMessage, decodeMessage, FrameReader, readFrames, eventFrame, exceptionFrame, unwrapSigned, crc32, EventStreamError codec The event-stream codec (exact prelude, headers and CRC32s), in both directions.
document, operationIds, supportedOperationIds values The vendored OpenAPI contract and its operation ids.
createServer, serveTarget, DEFAULT_PORT, listenH2c (./server) Node Serve h2c + HTTP/1.1 on one port; the serve CLI target; port 8796; the dual-protocol listener for any Fetch handler.

Part of mockingbird.

to navigate to open