Writing aimock Test Fixtures
What aimock Is
aimock is a zero-dependency mock infrastructure for AI apps. Fixture-driven. Multi-provider (OpenAI, Anthropic, Gemini, Gemini Interactions, AWS Bedrock, Azure OpenAI, Vertex AI, Ollama, Cohere, OpenRouter). Multimedia endpoints (image generation, text-to-speech, audio transcription, video generation). MCP, A2A, AG-UI, and vector DB mocking. Runs a real HTTP server on a real port — works across processes, unlike MSW-style interceptors. WebSocket support for OpenAI Responses/Realtime and Gemini Live APIs. Record-and-replay for all endpoints including multimedia. Chaos testing and Prometheus metrics.
Core Mental Model
- Fixtures = match criteria + response
- First-match-wins — order matters
- All providers share one fixture pool (provider adapters normalize to
ChatCompletionRequest) - Fixtures are live — mutations after
start()take effect immediately - Sequential responses are supported via
sequenceIndex(match count tracked per fixture)
Match Field Reference
| Field | Type | Matches Against |
|---|---|---|
userMessage |
string |
Substring of last role: "user" message text |
userMessage |
RegExp |
Pattern test on last role: "user" message text |
systemMessage |
string |
Substring of the concatenated text of every role: "system" message in the request. Use to gate a fixture on host-supplied context (persona, agent-context entries) so changes to that context cause the fixture to fall through instead of returning a stale baked response |
systemMessage |
string[] |
Array of substrings — ALL must be present in the joined system text (AND semantics). Use when the gate must combine multiple non-adjacent tokens whose serialisation order isn't stable |
systemMessage |
RegExp |
Pattern test on the concatenated system-message text |
inputText |
string |
Substring of embedding input text (concatenated if multiple inputs) |
inputText |
RegExp |
Pattern test on embedding input text |
toolName |
string |
Exact match on any tool in request's tools[] array (by function.name) |
toolCallId |
string |
Exact match on tool_call_id of last role: "tool" message |
toolResultContains |
string |
Substring of the last tool message's text content, gated on that message being the request's LAST message (same rule as toolCallId). Discriminates resume paths that share a tool_call_id and differ only inside the tool-result payload (e.g. approve {"chosen_time": …} vs cancel {"cancelled": true}) |
model |
string |
Exact match on req.model |
model |
RegExp |
Pattern test on req.model |
responseFormat |
string |
Exact match on req.response_format.type ("json_object", "json_schema") |
sequenceIndex |
number |
Matches only when this fixture's match count equals the given index (0-based) |
turnIndex |
number |
Stateless conversation-depth matching. Counts role: "assistant" messages in the request; matches when that count equals the value. turnIndex: 0 = first turn (no prior assistant messages). Use instead of sequenceIndex for shared/deployed instances where stateful counters break under concurrency |
hasToolResult |
boolean |
Stateless tool-message presence matching, scoped to the CURRENT turn (messages after the last role: "user" message). true matches when a role: "tool" message appears after the last user message; false matches when none does. (If the request has no user message, the whole conversation is scanned.) Provider-consistent across all aimock handlers (OpenAI, Claude, Gemini, Bedrock, Ollama, Cohere) |
endpoint |
string |
Restrict to endpoint type: "chat", "image", "speech", "transcription", "video", "embedding" |
predicate |
(req: ChatCompletionRequest) => boolean |
Custom function — full access to request |
AND logic: all specified fields must match. Empty match {} = catch-all.
Multi-part content (e.g., [{type: "text", text: "hello"}]) is automatically extracted — userMessage matching works regardless of content format.
When to Use Each Multi-turn Matching Approach
| Approach | Stateless? | Best For |
|---|---|---|
turnIndex |
Yes | Shared/deployed instances; matches on conversation depth (count of assistant messages in request) |
hasToolResult |
Yes | Simplest option for 2-step tool flows — boolean: does the current turn (after the last user message) carry a tool result? |
sequenceIndex |
No | Single-client unit tests with repeated identical requests (server-side counter, breaks under concurrency) |
toolCallId |
Yes | Matching specific tool result IDs in the conversation history |
toolResultContains |
Yes | Same tool call id, different outcomes — match on the tool-result payload (approve vs cancel legs) |
Prefer stateless approaches (turnIndex, hasToolResult, toolResultContains) for shared aimock instances (deployed via Docker, used by multiple test runners). Use sequenceIndex only in isolated single-client unit tests where the counter won't be corrupted by concurrent requests.
Multi-turn fixture examples
// 2-step HITL with turnIndex
{"match": {"userMessage": "trip to mars", "turnIndex": 0}, "response": {"toolCalls": [{"id": "call_001", "name": "generate_steps", "arguments": "{}"}]}}
{"match": {"userMessage": "trip to mars", "turnIndex": 1}, "response": {"content": "Great choices! Proceeding."}}
// Same thing with hasToolResult (simpler for 2-step)
{"match": {"userMessage": "trip to mars", "hasToolResult": false}, "response": {"toolCalls": [{"id": "call_001", "name": "generate_steps", "arguments": "{}"}]}}
{"match": {"userMessage": "trip to mars", "hasToolResult": true}, "response": {"content": "Great choices!"}}
// HITL suspend tool where approve and cancel resume with the SAME tool call id —
// discriminate on the tool-result payload; put the cancel leg first (first match wins)
{"match": {"toolCallId": "call_001", "toolResultContains": "\"cancelled\""}, "response": {"content": "No problem — nothing was booked."}}
{"match": {"toolCallId": "call_001"}, "response": {"content": "Booked: Monday 9:00 AM confirmed."}}
Response Types
Text
{
content: "Hello!";
}
Tool Calls
// Preferred: object form (auto-stringified by the fixture loader)
{
toolCalls: [{ name: "get_weather", arguments: { city: "SF" } }];
}
// Also accepted: JSON string form (backward compatible)
{
toolCalls: [{ name: "get_weather", arguments: '{"city":"SF"}' }];
}
Both object and string forms are accepted for arguments. The fixture loader auto-stringifies objects via JSON.stringify(). Object form is preferred for readability.
Blocks (ordered text / tool-call streaming)
The optional blocks array expresses an explicit, ordered sequence of stream entries — something plain content + toolCalls cannot, since those imply text-then-tools. Each entry is either { "type": "text", "text": "..." } or { "type": "toolCall", "name": "...", "arguments": "...", "id"?: "..." }, streamed in array order. This enables tool-first ordering (a tool call before any text) and interleaved text/tool ordering.
// Tool-first: tool call streams before the text
{
blocks: [
{ type: "toolCall", name: "get_weather", arguments: { city: "SF" } },
{ type: "text", text: "Checking the weather for you…" },
];
}
When blocks is present it takes precedence over content/toolCalls for stream order; when absent, legacy behavior is unchanged. blocks-only fixtures are first-class — a response may be just { blocks: [...] } with no content and no toolCalls, and builders derive the aggregate content/tool_calls from the blocks. A toolCall block's arguments may be a JSON object or a string (objects auto-stringify), exactly like top-level toolCalls.
Replay caveat: block order is observable on some providers and not others — see the per-provider observability matrix.
Embedding
{
embedding: [0.1, 0.2, 0.3, -0.5, 0.8];
}
The embedding vector is returned for each input in the request. If no embedding fixture matches, deterministic embeddings are auto-generated from the input text hash — you only need fixtures when you want specific vectors.
Image
// Single image
{
image: {
url: "https://example.com/generated.png"
}
}
// Multiple images
{
images: [{ url: "https://example.com/1.png" }, { b64Json: "iVBOR..." }]
}
Use match: { endpoint: "image" } to prevent cross-matching with chat fixtures.
Speech (TTS)
{ audio: "base64-encoded-audio-data" }
// With explicit format (default: mp3)
{ audio: "base64-data", format: "opus" }
Transcription
// Simple
{ transcription: { text: "Hello world" } }
// Verbose with timestamps
{ transcription: { text: "Hello world", language: "en", duration: 2.5, words: [...], segments: [...] } }
Video
{ video: { id: "vid-1", status: "completed", url: "https://example.com/video.mp4" } }
Video uses async polling — POST /v1/videos creates, GET /v1/videos/{id} checks status.
Error
{ error: { message: "Rate limited", type: "rate_limit_error" }, status: 429 }
Chaos (Failure Injection)
The optional chaos field on a fixture enables probabilistic failure injection:
{
chaos?: {
dropRate?: number; // Probability (0-1) of returning a 500 error
malformedRate?: number; // Probability (0-1) of returning malformed JSON
disconnectRate?: number; // Probability (0-1) of disconnecting mid-stream
}
}
Rates are evaluated per-request. When triggered, the chaos failure replaces the normal response.
Common Patterns
Basic text fixture
mock.onMessage("hello", { content: "Hi there!" });
Tool call → tool result → final response (3-step agent loop)
The most common pattern. Fixture 1 triggers the tool call, fixture 2 handles the tool result.
// Step 1: User asks about weather → LLM calls tool
mock.onMessage("weather", {
toolCalls: [{ name: "get_weather", arguments: { city: "SF" } }],
});
// Step 2: Tool result comes back → LLM responds with text
mock.addFixture({
match: { predicate: (req) => req.messages.at(-1)?.role === "tool" },
response: { content: "It's 72°F in San Francisco." },
});
Why predicate, not userMessage? After a tool call, the client replays the same conversation with the tool result appended. The user message hasn't changed — userMessage: "weather" would match the SAME fixture again, creating an infinite loop.
Embedding fixture
// Match specific input text
mock.onEmbedding("search query", {
embedding: [0.1, 0.2, 0.3, 0.4, 0.5],
});
// Match with regex
mock.onEmbedding(/product.*description/, {
embedding: [0.9, -0.1, 0.5, 0.3, 0.2],
});
Structured output / JSON mode
// onJsonOutput auto-sets responseFormat: "json_object" and stringifies objects
mock.onJsonOutput("extract entities", {
entities: [
{ name: "Acme Corp", type: "company" },
{ name: "Jane Doe", type: "person" },
],
});
// Equivalent manual form:
mock.addFixture({
match: { userMessage: "extract entities", responseFormat: "json_object" },
response: { content: '{"entities":[...]}' },
});
Sequential responses (same match, different responses)
// First call returns tool call, second returns text
mock.on(
{ userMessage: "status", sequenceIndex: 0 },
{ toolCalls: [{ name: "check_status", arguments: {} }] },
);
mock.on({ userMessage: "status", sequenceIndex: 1 }, { content: "All systems operational." });
Match counts are tracked per fixture group. Use resetMatchCounts() between tests to reset counts while keeping loaded fixtures. reset() also clears the fixture pool, so avoid it between tests that share a loaded fixture set.
Streaming physics (realistic timing)
mock.onMessage(
"tell me a story",
{ content: "Once upon a time..." },
{
streamingProfile: {
ttft: 200, // 200ms before first token
tps: 30, // 30 tokens per second after that
jitter: 0.1, // ±10% random variance
},
},
);
Predicate-based routing (same user message, different context)
Common in supervisor/orchestrator patterns where the system prompt changes:
mock.addFixture({
match: {
predicate: (req) => {
const sys = req.messages.find((m) => m.role === "system")?.content ?? "";
return typeof sys === "string" && sys.includes("Flights found: false");
},
},
response: { toolCalls: [{ name: "search_flights", arguments: {} }] },
});
Catch-all (always add one)
Prevents unmatched requests from returning 404 and crashing the test:
mock.addFixture({
match: { predicate: () => true },
response: { content: "I understand. How can I help?" },
});
Tool result catch-all with prependFixture
Must go at the front so it matches before substring-based fixtures:
mock.prependFixture({
match: { predicate: (req) => req.messages.at(-1)?.role === "tool" },
response: { content: "Done!" },
});
Stream interruption simulation (v1.3.0+)
mock.onMessage(
"long response",
{ content: "This will be cut short..." },
{
truncateAfterChunks: 3, // Stop after 3 SSE chunks
disconnectAfterMs: 500, // Or disconnect after 500ms
},
);
Chaos testing (probabilistic failures)
mock.addFixture({
match: { userMessage: "flaky" },
response: { content: "Sometimes works!" },
chaos: { dropRate: 0.3 },
});
30% of requests matching this fixture will get a 500 error instead of the response. Can also use malformedRate (garbled JSON) or disconnectRate (connection dropped mid-stream).
Server-level chaos applies to ALL requests:
mock.setChaos({ dropRate: 0.1 }); // 10% of all requests fail
mock.clearChaos(); // Remove server-level chaos
Error injection (one-shot)
mock.nextRequestError(429, { message: "Rate limited", type: "rate_limit_error" });
// Next request gets 429, then fixture auto-removes itself
JSON fixture files
{
"fixtures": [
{
"match": { "userMessage": "hello" },
"response": { "content": "Hi!" }
},
{
"match": { "userMessage": "weather" },
"response": {
"toolCalls": [
{
"name": "get_weather",
"arguments": { "city": "SF", "units": "fahrenheit" }
}
]
}
},
{
"match": { "inputText": "search query" },
"response": { "embedding": [0.1, 0.2, 0.3] }
},
{
"match": { "userMessage": "status", "sequenceIndex": 0 },
"response": { "content": "First response" }
}
]
}
JSON auto-stringify: In JSON fixture files, arguments and content can be objects — the loader auto-stringifies them with JSON.stringify(). This also applies to a blocks entry's arguments — object form auto-stringifies just like top-level toolCalls. The escaped-string form ("{\"city\":\"SF\"}") still works but objects are preferred for readability.
JSON files cannot use RegExp or predicate — those are code-only features. streamingProfile is supported in JSON fixture files.
Load with mock.loadFixtureFile("./fixtures/greetings.json") or mock.loadFixtureDir("./fixtures/").
API Endpoints
All providers share the same fixture pool — write fixtures once, they work for any endpoint.
| Endpoint | Provider | Protocol |
|---|---|---|
POST /v1/chat/completions |
OpenAI | HTTP |
POST /v1/responses |
OpenAI | HTTP + WS |
POST /v1/messages |
Anthropic | HTTP |
POST /v1/embeddings |
OpenAI | HTTP |
POST /v1beta/models/{model}:{method} |
Google Gemini | HTTP |
POST /model/{modelId}/invoke |
AWS Bedrock | HTTP |
POST /openai/deployments/{id}/chat/completions |
Azure OpenAI | HTTP |
POST /openai/deployments/{id}/embeddings |
Azure OpenAI | HTTP |
GET /health |
— | HTTP |
GET /ready |
— | HTTP |
POST /model/{modelId}/invoke-with-response-stream |
AWS Bedrock | HTTP |
POST /model/{modelId}/converse |
AWS Bedrock | HTTP |
POST /model/{modelId}/converse-stream |
AWS Bedrock | HTTP |
POST /v1/projects/{p}/locations/{l}/publishers/google/models/{m}:generateContent |
Vertex AI | HTTP |
POST /v1/projects/{p}/locations/{l}/publishers/google/models/{m}:streamGenerateContent |
Vertex AI | HTTP |
POST /api/chat |
Ollama | HTTP |
POST /api/generate |
Ollama | HTTP |
GET /api/tags |
Ollama | HTTP |
POST /v2/chat |
Cohere | HTTP |
POST /api/v1/chat/completions |
OpenRouter | HTTP |
GET /api/v1/models · /api/v1/key · /api/v1/credits |
OpenRouter | HTTP |
GET /metrics |
— | HTTP |
GET /v1/models |
OpenAI-compat | HTTP |
WS /v1/responses |
OpenAI | WebSocket |
WS /v1/realtime |
OpenAI | WebSocket |
WS /ws/google.ai...BidiGenerateContent |
Gemini Live | WebSocket |
POST /v1/images/generations |
OpenAI | HTTP |
POST /v1beta/models/{model}:predict |
Gemini Imagen | HTTP |
POST /v1/audio/speech |
OpenAI | HTTP |
POST /v1/audio/transcriptions |
OpenAI | HTTP |
POST /v1/videos |
OpenAI | HTTP |
GET /v1/videos/{id} |
OpenAI | HTTP |
Response Template Overrides
Fixture responses can include optional override fields to control auto-generated envelope values. These are merged into the provider-specific response format (OpenAI, Claude, Gemini, Responses API).
| Field | Type | Default | Description |
|---|---|---|---|
id |
string | auto-generated | Override response ID (e.g., chatcmpl-custom) |
created |
number | Date.now()/1000 |
Override Unix timestamp |
model |
string | echoes request | Override model name in response |
usage |
object | zeroed | Override token counts: { prompt_tokens, completion_tokens, total_tokens }. OpenAI Chat includes usage in response body; Responses API uses response.usage. When omitted, auto-computed from content length |
finishReason |
string | "stop" / "tool_calls" |
Override finish reason. Mappings: stop -> end_turn (Claude), STOP (Gemini); tool_calls -> tool_use (Claude), FUNCTION_CALL (Gemini); length -> max_tokens (Claude), MAX_TOKENS (Gemini); content_filter -> SAFETY (Gemini), failed (Responses API) |
role |
string | "assistant" |
Override message role |
systemFingerprint |
string | (omitted) | Add system_fingerprint to response |
provider |
string | slug author | OpenRouter only: top-level serving-provider display name (default = the winning model slug's author). Override to assert who served the request |
nativeFinishReason |
string | mirrors finishReason |
OpenRouter only: the raw upstream native_finish_reason alongside the normalized finish_reason |
usage.cost |
number | (omitted) | OpenRouter only: per-request usage.cost (scriptable — powers budget-guard tests). When set, usage.cost_details is emitted too. Never fabricated when omitted |
usage.is_byok |
bool | (omitted) | OpenRouter only: emit usage.is_byok. Also usage.prompt_tokens_details, usage.completion_tokens_details — emitted only when set |
Example
mock.onMessage("hello", {
content: "Hi!",
model: "gpt-4-turbo-2024-04-09",
usage: { prompt_tokens: 10, completion_tokens: 5, total_tokens: 15 },
systemFingerprint: "fp_abc123",
});
In JSON fixtures
{
"match": { "userMessage": "hello" },
"response": {
"content": "Hi!",
"model": "gpt-4-turbo-2024-04-09",
"usage": { "prompt_tokens": 10, "completion_tokens": 5, "total_tokens": 15 },
"systemFingerprint": "fp_abc123"
}
}
These fields map correctly across all provider formats — for example, finishReason: "stop" becomes finish_reason: "stop" in OpenAI, stop_reason: "end_turn" in Claude, and finishReason: "STOP" in Gemini.
OpenRouter (chat / router)
A request whose path starts with /api/v1/ (point the OpenAI SDK at a baseURL ending /api/v1) is shaped as OpenRouter: gen- id, top-level provider, per-choice native_finish_reason, always-present system_fingerprint/service_tier (null by default), an always-present message.reasoning (null unless a fixture supplies reasoning and the model is reasoning-capable), and a rich usage. Requests on the plain /v1/... base are untouched OpenAI. Same fixture pool — the fields above are the only additions.
- Scriptable cost / provider / finish reason: set
provider,nativeFinishReason, andusage.coston the response (see the overrides table).cost/cost_detailsare emitted only when a fixture suppliescost— aimock never fabricates a cost. models[]fallback (router failover): when the request body carriesmodels: [m1, m2, ...], aimock walks[model, ...models]in order and serves the first fixture that returns a NON-error response. A429/503error fixture on a candidate simulates a RUNTIME provider failure and falls through to the next candidate; the winning slug is echoed back as the top-levelmodel(assert failover viaresponse.model). Model the primary's "failure" as a 429/503 — an unknown/invalid model is just a fixture miss (aimock does not replicate OpenRouter's up-front invalid-model 400).- Terminal (non-failover) error class —
fallthrough: false: real OpenRouter fails over inconsistently by error class (a403 budget-exceeded/ generic "provider returned error" is served as terminal and does NOT advance to the next candidate, while 429/503 usually do). Setfallthrough: falseon an error fixture to make it terminal: the fallback loop stops and serves that error even when a good candidate follows. Absent /truekeeps the default fall-through. Composes withprovider.allow_fallbacks: fall-through happens only when both allow it (if either says stop, the error is terminal). Use it to reproduce the exact provider error a dev's app must handle itself.{ error: { message: "budget exceeded" }, status: 403, fallthrough: false } - Keepalive: set the fixture option
openRouterProcessing: trueto emit one: OPENROUTER PROCESSINGSSE comment before the first data frame (opt-in, default off). - Error envelope: OpenRouter errors are
{ error: { message, code } }(numericcode== HTTP status), with optional free-formmetadata.
// primary is a runtime 429, fallback answers
mock.on(
{ model: "openai/gpt-4o", userMessage: "route" },
{ error: { message: "rate limited" }, status: 429 },
);
mock.on(
{ model: "anthropic/claude-3.5-sonnet", userMessage: "route" },
{
content: "served by the fallback",
provider: "Anthropic",
usage: { cost: 0.0021 },
},
);
// POST /api/v1/chat/completions { model: "openai/gpt-4o", models: [...], ... }
// → response.model === "anthropic/claude-3.5-sonnet"
Provider Support Matrix
| Feature | OpenAI Chat | OpenAI Responses | Claude | Gemini | Gemini Int. | Bedrock | Azure | Ollama | Cohere | OpenRouter |
|---|---|---|---|---|---|---|---|---|---|---|
| Text | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes |
| Tool Calls | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes |
| Content + Tool Calls | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes |
| Streaming | SSE | SSE | SSE | SSE | SSE | Binary | SSE | NDJSON | SSE | SSE |
| Reasoning | Yes | Yes | Yes | Yes | -- | Yes | Yes | -- | -- | Yes |
| Web Searches | -- | Yes | -- | -- | -- | -- | -- | -- | -- | -- |
| Response Overrides | Yes | Yes | Yes | Yes | Yes | -- | Yes | -- | -- | Yes |
Critical Gotchas
Order matters — first match wins. Specific fixtures before general ones. Use
prependFixture()to force priority.argumentsaccepts both objects and strings —"arguments": {"key":"value"}(preferred, auto-stringified) or"arguments": "{\"key\":\"value\"}"(legacy). The same applies tocontentfields that contain JSON. The fixture loader detectstypeof === "object"and callsJSON.stringify()automatically.Latency is per-chunk, not total —
latency: 100means 100ms between each SSE chunk, not 100ms total response time. Similarly,truncateAfterChunksanddisconnectAfterMsare for simulating stream interruptions (added in v1.3.0).streamingProfiletakes precedence overlatency— when both are set on a fixture,streamingProfilecontrols timing. Use one or the other.Tool result messages don't change the user message — after a tool call, the client sends the same conversation + tool result. Matching on
userMessagewill hit the SAME fixture again → infinite loop. Always usepredicatecheckingrole === "tool"for tool results. Note: a whole-conversationrole === "tool"check (e.g.req.messages.some((m) => m.role === "tool")) diverges fromhasToolResult's current-turn scoping in multi-turn flows — the built-inhasToolResultmatcher only looks after the last user message, so a later turn whose history carries an earlier tool result still readsfalse.clearFixtures()preserves the array reference — uses.length = 0, not reassignment. The running server reads the same array object.Journal records everything — including 404 "no match" responses. Use
mock.getLastRequest()to debug mismatches.All providers share fixtures — a fixture matching "hello" works whether the request comes via
/v1/chat/completions(OpenAI),/v1/messages(Anthropic), Gemini, Bedrock, or Azure endpoints.WebSocket uses the same fixture pool — no special setup needed for WebSocket-based APIs (OpenAI Responses WS, Realtime, Gemini Live).
Embeddings auto-generate if no fixture matches — deterministic vectors are generated from the input text hash. You don't need a catch-all for embedding requests.
Sequential response counts are tracked per fixture — use
resetMatchCounts()between tests to reset counts while keeping loaded fixtures;reset()also clears the fixture pool, so don't use it between tests that share a loaded fixture set. The count increments after each match of that fixture group (all fixtures sharing the same non-sequenceIndexmatch fields).Bedrock uses Anthropic Messages format internally — the adapter normalizes Bedrock requests to
ChatCompletionRequest, so the same fixtures work. Bedrock supports both non-streaming (/invoke,/converse) and streaming (/invoke-with-response-stream,/converse-stream) endpoints.Azure OpenAI routes through the same handlers —
/openai/deployments/{id}/chat/completionsmaps to the completions handler,/openai/deployments/{id}/embeddingsmaps to the embeddings handler. Fixtures work unchanged.Ollama defaults to streaming — opposite of OpenAI. Set
stream: falseexplicitly in the request for non-streaming responses.Ollama tool call
argumentsis an object, not a JSON string — unlike OpenAI whereargumentsis a JSON string, Ollama sends and expects a plain object.Bedrock streaming uses binary Event Stream format — not SSE. The
invoke-with-response-streamand `conver
…(truncated)