Repository navigation
feat: raw SSE/JSON passthrough fixtures for LLM endpoints - #580
Merged
Merged
Conversation
…them (#549) Raw fixtures with an error status fail over on OpenRouter like typed error fixtures. A raw fixture that sets its own misbehavior is skipped by the misbehavior planner.
commit: |
Merged
jpr5
added a commit
that referenced
this pull request
Oct 27, 2026
Release v1.47.0. Merging this PR publishes through `publish-release.yml`: npm, the git tag, the GitHub Release and Slack. ## Changelog ### Added - Fixture `citations` serve sources on Responses, Chat Completions, Claude and Gemini (#550, #578) - Fixture `files` serve generated images on Gemini and OpenAI Responses (#550, #578) - Raw passthrough fixtures send provider SSE events or JSON exactly as written (#580) - Opt-in `nonStreamingTimingAllProviders` delays non-streaming replies on every provider (#579) ### Fixed - Client disconnects stop and journal every LLM reply; Docker image on Node 22.23.4 (#579, #576) The detailed notes from `[Unreleased]` are condensed into these bullets. The full detail is in the linked PRs. The drift-harness and CI changes (#583–#587) do not change what users see, so they are left out. ## Docs (separate commit) - The fixtures page gets a new **Citations and generated files** section: the fixture shape, the wire shape for each provider, the error codes, and override-table and provider-matrix rows. The Responses, Chat Completions, Claude and Gemini pages link to it. - What's New has three 1.47.0 cards: citations and generated images, raw passthrough, and stream realism (disconnects and timing on every provider). The two 1.46.0 cards are merged into one. The GPT-Live card is removed because it shipped in 1.43.0, more than a month ago. - The README has new feature bullets for citations and files, raw passthrough and client disconnects. The Streaming Physics bullet now mentions `nonStreamingTimingAllProviders`. - The competitive matrix has three new hand-reviewed rows: provider-native citations, model-generated images, and stopping on client disconnect. Each competitor cell was checked against a shallow clone of that competitor's source on 2026-10-26, and an HTML comment on each row records the evidence. The rows are added to `HAND_ROWS`, so the weekly scan never rewrites them. The homepage ND count in the test is now 56. ## Version carriers (`git grep -F 1.46.0`) `package.json`, `charts/aimock/Chart.yaml` appVersion, `.claude-plugin/plugin.json`, `.claude-plugin/marketplace.json` (`^1.47.0`), the `docs/index.html` banner, the `packages/aimock-pytest/README.md` default, and `_version.py` `AIMOCK_VERSION`. These are not bumped: the CHANGELOG history, the `version-note` labels for 1.46.0 features, and `pyproject.toml`. ## Local verification - format:check, lint, typecheck, build, api:report with no `etc/` diff, test:exports, the docs and matrix tests, and the full `pnpm test`. - The citations and files wire shapes were exercised against the built server for Chat, Responses, Claude and Gemini, including the 500 for `files` on Chat Completions. - Screenshots of What's New, the matrix and the fixtures section at 375, 430 and 1440 px.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #549
Summary
This PR adds raw passthrough fixtures. A fixture response of the form
{ raw: { wire, events?, json?, status?, headers?, done? } }sends provider events or JSON exactly as written. aimock adds only the framing of the named wire. No overrides, reasoning gating, usage synthesis, provider shaping or drift checks apply to a raw fixture.Per-wire framing. Each wire uses the same bytes that the typed handlers write for that wire:
event: <type>thendata: <json>):openai-responses,anthropic,cohere.data: <json>):openai-chat,gemini,gemini-interactions. Onopenai-chat, the stream ends withdata: [DONE]unlessraw.doneisfalse.ollama(/api/chat) andollama-generate(/api/generate). These are two wires because the frames of the two routes have different shapes.bedrock-invokewraps each event as achunkframe with base64bytes.bedrock-converseuses the single key of each event as the frame's event type and its value as the payload.That gives 10 wires:
openai-responses,openai-chat(including the OpenRouter and Azure routes),anthropic,gemini(including Vertex),gemini-interactions,cohere,ollama,ollama-generate,bedrock-invokeandbedrock-converse. The router offers a raw fixture only to the handler for itsraw.wire. The handler supplies the wire, never the request body. When a raw fixture matches a request in every way except its wire, and no other fixture matches, the server log names both wires instead of returning a silent 404.Stream vs JSON. A streaming request gets
eventsand a non-streaming request getsjson. If the body that the request needs is missing, aimock answers with the native 500 error of that wire and the codeaimock_raw_mode_unavailable.Status and headers.
statusdefaults to 200 and must be 200-599, except 204 and 304.headersreplace the defaults case-insensitively. Ifheadershas no Content-Type, a stream gets the wire's own type and a non-streaming reply getsapplication/json. aimock rejects headers that it must set itself, because it frames the body.Error codes. An invalid raw fixture gets a 500 in the wire's own error shape with the code
aimock_raw_fixture_invalid. Examples are a factory that returnsstatus: 99, or a factory that returns a raw fixture for another wire. On Gemini, Ollama and OpenRouter the code is prefixed to the message.Validation.
--validate-on-load,addFixturesFromJSONandPOST /__aimock/fixturesall check raw fixtures with one shared checker:eventsorjsonis present;statusand the header names and values are valid;doneis a boolean, and only onopenai-chat;customToolCallsandresponsesBlocksare not next toraw;Unknown keys inside
rawand an emptyeventsare warnings.chunkSizeandopenRouterProcessingon a raw fixture are warnings, because they are ignored. The same check runs again when a fixture is served, so fixtures added withon(),addFixture()or factories are checked too.Misbehavior skip. Model misbehavior faults skip raw fixtures. aimock skips a server-level fault, serves the raw reply, and records the fault as skipped in the journal. An
X-AIMock-Misbehaviorheader returns 501aimock_misbehavior_not_applicable. A raw fixture that sets its own misbehavior is an invalid fixture: load validation reports it, and when it is served it getsaimock_raw_fixture_invalid.Timing rules.
latency,streamingProfile,recordedTimingsandreplaySpeedpace streamed raw events in the same way as typed frames.truncateAfterChunks,disconnectAfterMsand chaos also apply, and each raw event counts as one chunk. A non-streaming raw reply followsnonStreamingTiminglike a typed fixture.nonStreamingTiming: truedelays the reply on OpenAI Chat Completions only.nonStreamingTimingAllProviders: truedelays it on every raw wire. aimock sizes the wait from the served JSON.Error replies. A raw reply with a status of 400 or more is never delayed, streaming or not. If the reply has a
json, a streaming request gets that JSON instead of events, because provider APIs answer errors that way. Error replies are recorded in the journal.WS Responses.
/v1/responsesover WebSocket answers a raw fixture with an error event with the codeaimock_raw_mode_unavailableand the message "raw fixtures are not yet supported on the WebSocket transport". Before this change, it answered with the generic unknown-type error.OpenRouter failover. In an OpenRouter
models[]fallback chain, anopenai-chatraw fixture with a status of 400 or more fails over to the next model. A typed error fixture with the same status does the same.Compatibility
Additive only. The feature is opt-in. A response is raw only if
rawis a non-null, non-array object and no other response type's marker key (content,toolCalls,error,json, and so on) is present. A typed fixture with a strayrawkey is still served, and validated, exactly as before.Typed fixtures unchanged. Existing typed fixtures produce output byte-identical to main: an offline probe compared 321 cases (status, headers except
date, and body) on the pre-review build, and the no-regression snapshot plus the feat: stop delivery on client disconnect across providers, and opt-in non-streaming timing for all providers #579 byte goldens pass at the final head. The no-regression suite (raw-no-regression.test.ts, with a snapshot) is green on main and on this branch.API report. The API report changes are additions only:
RawResponse,RawWireIdandWireId(WireIdwas already declared, and is now exported);RawResponseis added to theFixtureResponseandFixtureFileResponseunions;@internalwirefield onMatchOptions.isRawResponseis internal and not exported.Red-green evidence
Each wire was reproduced against a real aimock server, with and without
--validate-on-load, and with the real provider SDK tests. Before the change, every raw fixture request got a 500 with the bodyFixture response did not match any known type. After the change, the authored events or JSON are served with the wire's framing.text/event-stream/ 200application/jsontext/event-stream, endsdata: [DONE]/ 200application/jsontext/event-stream/ 200text/event-stream/ 200/api/chat500,/api/generate500, "did not match any known type"/api/chat200,/api/generate200,application/x-ndjsonapplication/vnd.amazon.eventstream/ 200 on bothThe split between
ollamaandollama-generatehas its own red-green. Before the split, the test "/api/generate gets the ollama-generate fixture in generate shape" failed (1 failed, 1 passed). After the split, it passes (2 passed).The full suite, typecheck, lint, format check, build, goldens/contract tests and the API report check all pass on this branch.
Not in this PR
record.raw(recording raw fixtures). Both are in spec PR3.Spec: raw SSE/JSON passthrough fixtures (Notion)