Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
5cdf1d2
chore: add engines field, @types/node, tsx; remove stale spec file
jpr5 Mar 21, 2026
900399b
feat: v1.6.0 — provider endpoints, chaos, metrics, record-and-replay
jpr5 Mar 21, 2026
2773d9b
test: 1250 tests — comprehensive coverage for all v1.6.0 features
jpr5 Mar 21, 2026
402c8fa
docs: v1.6.0 documentation — 6 new pages, update all existing pages
jpr5 Mar 21, 2026
6be3821
chore: bump version to 1.6.0, update Chart.yaml appVersion, add CHANG…
jpr5 Mar 21, 2026
8f14082
fix: type safety — RecordProviderKey, null journal body, exhaustive c…
jpr5 Mar 21, 2026
63e718d
fix: observability — metrics crash guard, Bedrock truncation warning …
jpr5 Mar 21, 2026
3d479ef
docs: correct --strict mode documentation in SKILL.md
jpr5 Mar 21, 2026
de8cfc3
test: cover metrics crash guard and Bedrock CRC truncation
jpr5 Mar 21, 2026
3657cf1
test: add unit tests for drift remediation scripts
jpr5 Mar 21, 2026
3fd1ec1
fix: chaos header validation, range clamping, and disconnect integrat…
jpr5 Mar 21, 2026
65a5b1c
fix: add error handling around metrics instrumentation in response fi…
jpr5 Mar 21, 2026
e454c12
refactor: tighten recorder pipeline typing with RecordProviderKey
jpr5 Mar 21, 2026
7807b1e
feat: validate StreamingProfile and ChaosConfig ranges at fixture loa…
jpr5 Mar 21, 2026
8014b70
docs: correct docker.html errors, add missing endpoints, fix CHANGELO…
jpr5 Mar 21, 2026
72eda7c
fix: structured logger for chaos/stream warnings; EventStream bounds;…
jpr5 Mar 21, 2026
cb09880
test: regression coverage for logger migration, EventStream bounds, b…
jpr5 Mar 21, 2026
8540122
fix: address review — recorder logging, strict fail-fast, chaos valid…
jpr5 Mar 22, 2026
c694c9b
docs: fix endpoint label (Groq not Azure) and metrics port (4010 not …
jpr5 Mar 22, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
fix: structured logger for chaos/stream warnings; EventStream bounds;…
… bedrock SSE; body timeout

- chaos.ts: add optional logger param to resolveChaosConfig/evaluateChaos/applyChaos;
  replace all console.warn calls with logger?.warn
- stream-collapse.ts: logger param on collapseStreamingResponse; replace console.warn;
  add explicit case "bedrock" routing to collapseAnthropicSSE; add bounds check in
  decodeEventStreamFrames — return {frames, truncated:true} when totalLength extends
  past buffer, preventing out-of-bounds reads on malformed/truncated EventStream frames
- recorder.ts: pass defaults.logger to collapseStreamingResponse; add res.setTimeout
  body accumulation timeout (30s) to prevent unbounded memory growth on slow responses
- bedrock.ts: update module docstring to describe all four endpoint families
- all handlers: pass defaults.logger as final arg to all applyChaos call sites
  • Loading branch information
jpr5 committed Mar 21, 2026
commit 72eda7cbdbeb5f11e46c7144c207593d291db315
2 changes: 2 additions & 0 deletions src/bedrock-converse.ts
Original file line number Diff line number Diff line change
Expand Up @@ -283,6 +283,7 @@ export async function handleConverse(
body: completionReq,
},
defaults.registry,
defaults.logger,
)
)
return;
Expand Down Expand Up @@ -485,6 +486,7 @@ export async function handleConverseStream(
body: completionReq,
},
defaults.registry,
defaults.logger,
)
)
return;
Expand Down
23 changes: 16 additions & 7 deletions src/bedrock.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,20 @@
/**
* AWS Bedrock Claude endpoint support.
* AWS Bedrock Claude endpoint support — invoke and invoke-with-response-stream.
*
* Handles POST /model/{modelId}/invoke and /invoke-with-response-stream
* requests. Translates incoming Bedrock Claude format into the
* ChatCompletionRequest format used by the fixture router, and converts
* fixture responses back into the appropriate Bedrock response format
* (JSON for invoke, AWS Event Stream binary encoding for streaming).
* Handles four Bedrock endpoint families (split across two modules):
*
* See bedrock-converse.ts for /converse and /converse-stream support.
* This file (bedrock.ts):
* - POST /model/{modelId}/invoke — non-streaming invoke
* - POST /model/{modelId}/invoke-with-response-stream — binary EventStream streaming
*
* bedrock-converse.ts:
* - POST /model/{modelId}/converse — Converse API (non-streaming)
* - POST /model/{modelId}/converse-stream — Converse API (EventStream streaming)
*
* Translates incoming Bedrock Claude format into the ChatCompletionRequest
* format used by the fixture router, and converts fixture responses back into
* the appropriate Bedrock response format (JSON for invoke, AWS Event Stream
* binary encoding for streaming).
*/

import type * as http from "node:http";
Expand Down Expand Up @@ -322,6 +329,7 @@ export async function handleBedrock(
body: completionReq,
},
defaults.registry,
defaults.logger,
)
)
return;
Expand Down Expand Up @@ -638,6 +646,7 @@ export async function handleBedrockStream(
body: completionReq,
},
defaults.registry,
defaults.logger,
)
)
return;
Expand Down
20 changes: 12 additions & 8 deletions src/chaos.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import type * as http from "node:http";
import type { ChaosAction, ChaosConfig, ChatCompletionRequest, Fixture } from "./types.js";
import { writeErrorResponse } from "./sse-writer.js";
import type { Journal } from "./journal.js";
import type { Logger } from "./logger.js";
import type { MetricsRegistry } from "./metrics.js";

/**
Expand All @@ -21,6 +22,7 @@ function resolveChaosConfig(
fixture: Fixture | null,
serverDefaults?: ChaosConfig,
rawHeaders?: http.IncomingHttpHeaders,
logger?: Logger,
): ChaosConfig {
const base: ChaosConfig = { ...serverDefaults };

Expand All @@ -41,23 +43,23 @@ function resolveChaosConfig(
if (typeof dropHeader === "string") {
const val = parseFloat(dropHeader);
if (isNaN(val)) {
console.warn(`[chaos] x-llmock-chaos-drop: invalid value "${dropHeader}", ignoring`);
logger?.warn(`[chaos] x-llmock-chaos-drop: invalid value "${dropHeader}", ignoring`);
} else {
if (val < 0 || val > 1) {
console.warn(`[chaos] x-llmock-chaos-drop: value ${val} out of range [0,1], clamping`);
logger?.warn(`[chaos] x-llmock-chaos-drop: value ${val} out of range [0,1], clamping`);
}
base.dropRate = Math.min(1, Math.max(0, val));
}
}
if (typeof malformedHeader === "string") {
const val = parseFloat(malformedHeader);
if (isNaN(val)) {
console.warn(
logger?.warn(
`[chaos] x-llmock-chaos-malformed: invalid value "${malformedHeader}", ignoring`,
);
} else {
if (val < 0 || val > 1) {
console.warn(
logger?.warn(
`[chaos] x-llmock-chaos-malformed: value ${val} out of range [0,1], clamping`,
);
}
Expand All @@ -67,12 +69,12 @@ function resolveChaosConfig(
if (typeof disconnectHeader === "string") {
const val = parseFloat(disconnectHeader);
if (isNaN(val)) {
console.warn(
logger?.warn(
`[chaos] x-llmock-chaos-disconnect: invalid value "${disconnectHeader}", ignoring`,
);
} else {
if (val < 0 || val > 1) {
console.warn(
logger?.warn(
`[chaos] x-llmock-chaos-disconnect: value ${val} out of range [0,1], clamping`,
);
}
Expand Down Expand Up @@ -100,8 +102,9 @@ export function evaluateChaos(
fixture: Fixture | null,
serverDefaults?: ChaosConfig,
rawHeaders?: http.IncomingHttpHeaders,
logger?: Logger,
): ChaosAction | null {
const config = resolveChaosConfig(fixture, serverDefaults, rawHeaders);
const config = resolveChaosConfig(fixture, serverDefaults, rawHeaders, logger);

if (config.dropRate !== undefined && config.dropRate > 0 && Math.random() < config.dropRate) {
return "drop";
Expand Down Expand Up @@ -143,8 +146,9 @@ export function applyChaos(
journal: Journal,
context: ChaosJournalContext,
registry?: MetricsRegistry,
logger?: Logger,
): boolean {
const action = evaluateChaos(fixture, serverDefaults, rawHeaders);
const action = evaluateChaos(fixture, serverDefaults, rawHeaders, logger);
if (!action) return false;

if (registry) {
Expand Down
1 change: 1 addition & 0 deletions src/cohere.ts
Original file line number Diff line number Diff line change
Expand Up @@ -485,6 +485,7 @@ export async function handleCohere(
body: completionReq,
},
defaults.registry,
defaults.logger,
)
)
return;
Expand Down
1 change: 1 addition & 0 deletions src/embeddings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,7 @@ export async function handleEmbeddings(
body: syntheticReq,
},
defaults.registry,
defaults.logger,
)
)
return;
Expand Down
1 change: 1 addition & 0 deletions src/gemini.ts
Original file line number Diff line number Diff line change
Expand Up @@ -436,6 +436,7 @@ export async function handleGemini(
body: completionReq,
},
defaults.registry,
defaults.logger,
)
)
return;
Expand Down
1 change: 1 addition & 0 deletions src/messages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -484,6 +484,7 @@ export async function handleMessages(
body: completionReq,
},
defaults.registry,
defaults.logger,
)
)
return;
Expand Down
2 changes: 2 additions & 0 deletions src/ollama.ts
Original file line number Diff line number Diff line change
Expand Up @@ -362,6 +362,7 @@ export async function handleOllama(
body: completionReq,
},
defaults.registry,
defaults.logger,
)
)
return;
Expand Down Expand Up @@ -604,6 +605,7 @@ export async function handleOllamaGenerate(
body: completionReq,
},
defaults.registry,
defaults.logger,
)
)
return;
Expand Down
5 changes: 5 additions & 0 deletions src/recorder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,7 @@ export async function proxyAndRecord(
ctString,
providerKey,
isBinaryStream ? rawBuffer : upstreamBody,
defaults.logger,
);

let fixtureResponse: FixtureResponse;
Expand Down Expand Up @@ -214,6 +215,7 @@ function makeUpstreamRequest(
return new Promise((resolve, reject) => {
const transport = target.protocol === "https:" ? https : http;
const UPSTREAM_TIMEOUT_MS = 30_000;
const BODY_TIMEOUT_MS = 30_000;
const req = transport.request(
target,
{
Expand All @@ -225,6 +227,9 @@ function makeUpstreamRequest(
},
},
(res) => {
res.setTimeout(BODY_TIMEOUT_MS, () => {
req.destroy(new Error(`Upstream response timed out after ${BODY_TIMEOUT_MS / 1000}s`));
});
const chunks: Buffer[] = [];
res.on("data", (chunk: Buffer) => chunks.push(chunk));
res.on("error", reject);
Expand Down
1 change: 1 addition & 0 deletions src/responses.ts
Original file line number Diff line number Diff line change
Expand Up @@ -547,6 +547,7 @@ export async function handleResponses(
body: completionReq,
},
defaults.registry,
defaults.logger,
)
)
return;
Expand Down
1 change: 1 addition & 0 deletions src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,7 @@ async function handleCompletions(
body,
},
defaults.registry,
defaults.logger,
)
)
return;
Expand Down
11 changes: 10 additions & 1 deletion src/stream-collapse.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

import { crc32 } from "node:zlib";
import type { RecordProviderKey, ToolCall } from "./types.js";
import type { Logger } from "./logger.js";

// ---------------------------------------------------------------------------
// Result type shared by all collapse functions
Expand Down Expand Up @@ -438,6 +439,11 @@ function decodeEventStreamFrames(buf: Buffer): {
const totalLength = buf.readUInt32BE(offset);
const headersLength = buf.readUInt32BE(offset + 4);

// Validate bounds: ensure the full frame is within the buffer
if (totalLength < 12 || offset + totalLength > buf.length) {
return { frames, truncated: true };
}

// Validate prelude CRC
const preludeCrc = buf.readUInt32BE(offset + 8);
const computedPreludeCrc = crc32(buf.subarray(offset, offset + 8));
Expand Down Expand Up @@ -611,6 +617,7 @@ export function collapseStreamingResponse(
contentType: string,
providerKey: RecordProviderKey,
body: string | Buffer,
logger?: Logger,
): CollapseResult | null {
const ct = contentType.toLowerCase();

Expand All @@ -637,8 +644,10 @@ export function collapseStreamingResponse(
return collapseGeminiSSE(str);
case "cohere":
return collapseCohereSSE(str);
case "bedrock":
return collapseAnthropicSSE(str);
default:
console.warn(
logger?.warn(
`[stream-collapse] unknown SSE provider "${providerKey}", falling back to OpenAI SSE format`,
);
return collapseOpenAISSE(str);
Expand Down