Skip to content

feat: serverless-offline-bedrock + serverless-offline-transcribe (local AI emulators, MVP)#299

Open
silouone wants to merge 8 commits into
masterfrom
feat/offline-bedrock-transcribe
Open

feat: serverless-offline-bedrock + serverless-offline-transcribe (local AI emulators, MVP)#299
silouone wants to merge 8 commits into
masterfrom
feat/offline-bedrock-transcribe

Conversation

@silouone

@silouone silouone commented Jul 6, 2026

Copy link
Copy Markdown
Member

Summary

Two new plugins that emulate AWS AI data-plane services locally, so the our internal TLS batch pipeline (audio → Transcribe → Bedrock-translate) runs fully offline — the same role ElasticMQ plays for SQS. Implements spec 002-offline-bedrock-transcribe MVP (Phase A + Phase C; streaming B/D deferred).

  • serverless-offline-bedrock — local Bedrock Runtime endpoint: POST /model/{modelId}/converse, pure OpenAI- and Anthropic-compatible backend adapters, per-modelId backend mapping, endpoint injection via AWS_ENDPOINT_URL_BEDROCK_RUNTIME.
  • serverless-offline-transcribe — local Transcribe (AWS JSON 1.1): Start/Get/List transcription jobs, in-memory job registry, S3/Minio media I/O, OpenAI-Whisper worker (execFile, no shell), AWS-shaped transcript JSON (string numerics, untimed punctuation items).

Proven spikes

  • Endpoint injection (AC-A1): an unmodified BedrockRuntimeClient with only the injected service env var dials the local emulator — encoded as a permanent AVA integration test.
  • Whisper parsing: transform validated against a real whisper --word_timestamps True run (fixture checked in).

Evidence-forced deviations from the plan (documented in code)

  1. Bedrock emulator uses Node's built-in http2 (h2c) server, not @hapi/hapi — the SDK's default NodeHttp2Handler speaks cleartext HTTP/2, which hapi cannot serve. One dep fewer.
  2. Only service-specific AWS_ENDPOINT_URL_* vars are injected; the plan's generic AWS_ENDPOINT_URL fallback would misroute the app's other AWS clients (S3/SQS/DDB).

Test plan

  • npx eslint packages/serverless-offline-bedrock packages/serverless-offline-transcribe — clean
  • npx ava both suites — 58 tests green (32 + 26), incl. both integration tests
  • Independent validator pass: all MVP acceptance criteria + 14 constitution gates
  • Manual smoke test of the transcribe COMPLETED path against live Minio (known gap — unit-covered)

Notes for reviewers

  • Base includes the (not-yet-pushed-to-master) release commit 1d67ee8; the diff shrinks to the 4 plugin commits once master is pushed after lerna publish.
  • Not published to npm; both names verified free on the registry (2026-07-06).
  • Integration tests require whisper on PATH (else that single test fails fast per AC-C5) and bind ports 4019/4569 — flagged for a follow-up hermeticity tweak.

🤖 Generated with Claude Code

silouone and others added 5 commits July 6, 2026 15:17
 - dynamodb-streams-readable@5.0.0
 - serverless-apigateway-access-logs@3.0.0
 - serverless-offline-dynamodb-streams@8.0.0
 - @coorpacademy/serverless-offline-eventbridge@1.0.0
 - serverless-offline-kinesis@9.0.0
 - serverless-offline-s3@8.0.0
 - serverless-offline-sqs@9.0.0
 - serverless-offline-ssm-provider@4.0.0
 - serverless-offline-plugins-integration@5.0.0
Copy plugin skeleton (log.js, client-config.js) from serverless-offline-sqs,
add bedrock package.json + OpenAI Converse adapter (pure request/response
translation).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
SPIKE 1 PROVEN: unmodified BedrockRuntimeClient, with only the injected
AWS_ENDPOINT_URL_BEDROCK_RUNTIME env var, dials our local server and parses
our Converse body (end-to-end AVA test). Key finding: Bedrock Runtime SDK uses
NodeHttp2Handler (h2c), so the emulator uses Node's http2 server, not @hapi/hapi
(dropped from deps) — the one evidence-forced deviation from the plan.

- src/index.js: thin plugin (hooks, _mergeOptions, endpoint injection, enabled guard)
- src/bedrock.js: h2c emulator (POST /model/{modelId}/converse, url-decode modelId)
- src/converse.js: orchestration + AC-A4 error shaping (never crashes offline)
- src/adapters/{openai,anthropic}.js: pure Converse<->backend translation
- src/backend-config.js: resolveBackend per-modelId deep-merge, fail-fast
- test/index.js: 32 AVA tests (adapters, resolveBackend, error/disabled paths, SPIKE 1)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
SPIKE 2 PROVEN with a live Whisper 'base' run: verified words carry a leading
space + glued trailing punctuation (' world,' -> 'world' + ','); the fixture
(test/fixtures/whisper-word-timestamps.json) is the real output, and
whisperJsonToTranscribe reproduces the AWS shape (STRING numerics, punctuation
items omit start/end times). Transcribe batch uses the SDK's HTTP/1.1 handler
(verified), so @hapi/hapi is correct here.

- src/index.js: thin plugin, AWS_ENDPOINT_URL_TRANSCRIBE injection (no generic var)
- src/transcribe.js: @hapi/hapi AWS-JSON-1.1 server, x-amz-target routing, async job pipeline
- src/job-registry.js: pure descriptor builders + Map holder
- src/s3-io.js: s3:// parse, OutputKey rules, path-style Minio URL, forcePathStyle S3 client
- src/whisper.js: execFile worker (no shell), fail-fast on missing binary (AC-C5)
- src/transcript.js: pure whisperJsonToTranscribe + splitWordPunctuation
- test/index.js: 26 AVA tests incl. real TranscribeClient Start->Get->FAILED round-trip

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…INT_URL

- READMEs (AC-X3): prerequisites, config table, model/engine map, copy-paste example.
- bedrock index.js: inject ONLY the service-specific AWS_ENDPOINT_URL_BEDROCK_RUNTIME,
  not the generic AWS_ENDPOINT_URL (which would misroute the app's other AWS clients
  — S3/SQS/DynamoDB — to the bedrock port in a multi-service offline stack). SPIKE 1
  proved the service-specific var alone is sufficient.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@silouone silouone marked this pull request as ready for review July 6, 2026 15:59
@silouone silouone self-assigned this Jul 6, 2026
silouone and others added 3 commits July 6, 2026 18:02
npm ci on CI fails EUSAGE (Missing: serverless-offline-bedrock@1.0.0 ...) because
the lockfile predates the two new packages and their @aws-sdk deps.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…hemeral ports)

Travis has no whisper binary and fixed ports 4019/4569 collide with live
offline sessions on dev machines. The transcribe lifecycle test now skips
cleanly when whisper is absent (its fail-fast behavior keeps its own unit
test) and both integration tests bind OS-assigned free ports.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…r class, shutdown, packaging)

Correctness:
- transcribe/s3-io: stream S3 media to disk (pipeline) instead of buffering the
  whole object with transformToByteArray — a large-but-valid recording no longer
  OOMs the offline process.
- bedrock+transcribe: honor an explicit `port: 0` (OS-assigned free port). A new
  resolvePort() replaces `Number(port) || DEFAULT_PORT`, which treated the valid 0
  as unset and rebound the fixed port (collision/EADDRINUSE).
- bedrock/converse: move toBackendRequest inside the guard so a malformed request
  (e.g. toolConfig entry without toolSpec) returns 400 ValidationException instead
  of escaping handleConverse's "never throws" contract as a wrong 500.
- transcribe/whisper: add `--verbose False` and raise maxBuffer to 256MB so a long
  recording's stdout can't trip ERR_CHILD_PROCESS_STDIO_MAXBUFFER and false-FAIL a
  run that actually succeeded.
- bedrock: stop() now tracks raw sockets (incl. stray HTTP/1.1 keep-alives) and
  honors its timeout arg, so shutdown can't hang the offline process.
- bedrock/openai: collapse array content-parts to a string so a non-string OpenAI
  `content` never leaks into a malformed Converse `text` block.

Cleanup:
- transcribe/transcript: fold the word→item shaping into a single traversal that
  emits items and per-segment id-ranges together (was 3x per job with two
  hand-written id counters kept in lockstep). _processJob reuses the shaped items
  for the AC-C4 empty-speech guard instead of re-running the pass.
- packaging: move the test/README-only @aws-sdk/client-bedrock-runtime and
  @aws-sdk/client-transcribe to devDependencies; delete the orphaned bedrock
  src/client-config.js (never required) and drop it from files[].

Tests: +5 (resolvePort x2, malformed-request 400, array content, keep-alive
shutdown). 63 pass, eslint clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant