Repository navigation
feat: citations and generated files on the remaining providers - #578
Merged
Merged
Conversation
commit: |
…ators Add the types the citations and files work builds on: FixtureUrlCitation and FixtureFileCitation (FixtureCitation is now their union), the Chat Completions annotation type, and the fixture-output errors UnsupportedGeneratedFileError and InvalidFixtureOutputError, all exported from the package root. Add the shared validators every wire uses: fixtureCitationsProblem and fixtureFilesProblem with the null, surrogate-pair and mixed URL/file field rules, fixtureCitations, fixtureFiles, resolveCitations, rejectGeneratedFiles and surrogateSafeChunks. The fixture errors join the coded-500 path. Responses skips file citations here until it builds file_citation annotations.
…and Responses Fixture citations index the joined text of all text blocks, in UTF-16 string offsets, the same way on every wire that reads them. - Chat Completions: url_citation annotations on the message, and one delta.annotations chunk before the finish chunk. Every builder cites the text it serves; refusal, content-filter and reasoning-only turns carry none. - Anthropic Messages: web_search_result_location citations on the owning text block, streamed as citations_delta events. Bedrock InvokeModel shares the builders and serves no citations. - Gemini: groundingMetadata with UTF-8 byte offsets into the cited part; a streamed chunk grounds its own text part, and text chunks never cut a surrogate pair. - Responses: url_citation and file_citation annotations on the owning output_text. A file citation on a block boundary stays at the end of the block before it. A citation on a turn with no text is rejected on every wire.
…e no wire shape exists - Responses: image/* fixture files become image_generation_call output items, streamed through the image_generation_call lifecycle events before the message. A non-image file is a coded 500. Refusal, empty-response, content-filter and reasoning-only faults withhold the images with the rest of the output. - Chat Completions and Anthropic Messages have no projection for generated files, so a fixture with files is a coded 500 (aimock_unsupported_generated_file) before any byte is written, on the normal path and as a safety net on the fault path. - Gemini validates fixture files and keeps them as inlineData parts before the text under stop-length-mid-tool.
… journal faults in order - The planner checks a fixture's citations and files on the original turn, before any fault rewrites it. A fixture the wire would answer with a coded 500 is skipped before fault selection, so it spends no times budget and no seed ordinal, and takes the normal path. - A fault that replaces, truncates or empties the reply text drops the fixture's citations; a fault that serves the text unchanged keeps them. - Anthropic Messages, Gemini, Responses and the Responses WebSocket journal an applied fault before building it, so a fixture error thrown while building leaves a 500 entry that keeps the fixture, and the fault is recorded only once the reply is built. - Chat Completions records the outcome only once the reply is built; a non-stream reply records it before its timing wait, and a client that hangs up during the wait is journaled as interrupted.
…reject validateFixtures now checks a fixture's files and citations with the same validators the wires use at serve time, and checks citation offsets against the text the wires serve: the ordered text blocks when present, else content. A turn with no text reports any non-empty citations as the wires do. A malformed value stays a warning, not a load error; each warning names the wires that answer a 500 for it and the error code they send. The end-to-end validation tests prove every wire answers the same coded 500 for each malformed value, and that validateFixtures warns with the same message.
…API report Add the CHANGELOG entry for citations and generated files on every provider that reads them, and regenerate the API report for the new exported types and errors.
jpr5
force-pushed
the
feat/citations-files-all-providers
branch
from
October 23, 2026 22:23
824fd81 to
9eaa9f1
Compare
jpr5
marked this pull request as ready for review
October 23, 2026 22:24
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 #547
Closes #548
#550 added citations on the OpenAI Responses API and generated files on Gemini. This PR covers the remaining providers, adds file citations on Responses, and makes malformed
citationsandfilesfail loudly instead of being served or dropped silently. It reuses the same fixture fields (citations,files) and the same defaults: the title falls back to the URL, and URL citation offsets default to the whole text.What each provider does now
citations(#547)message.annotationsgets{type: "url_citation", url_citation: {url, title, start_index, end_index}}, the shape ofChatCompletionMessage.Annotationin the openai SDK. When streaming, one chunk withdelta.annotationsis sent after the content and before the finish chunk. The SDK'sChatCompletionStreamcopies it intomessage.annotations.citations: [{type: "web_search_result_location", cited_text, url, title, encrypted_index}](CitationsWebSearchResultLocationin@anthropic-ai/sdk). The realencrypted_indexis opaque, so aimock fills in a stable base64 placeholder. When streaming, onecitations_deltaper citation comes after the text deltas and beforecontent_block_stop, in the same order as the streaming example in the Anthropic citations docs. On ablocksturn with several text blocks, each citation goes to the text block that holds its start.groundingMetadatawith onegroundingChunks[].web {uri, title}per citation andgroundingSupports[] {segment {partIndex, startIndex, endIndex, text}, groundingChunkIndices}. Segment offsets are UTF-8 byte offsets into the text of the part named bypartIndex, as theSegmentdocs in@google/genairequire. A citation that spans several text parts becomes one support per part. When streaming, each chunk that holds cited text carries the supports for its own text.url_citationannotations as in feat: scriptable citations (Responses) and model-generated files (Gemini) #550, now also split correctly across several text blocks. A citation withtype: "file"(fileId,filename, optionalindex, which defaults to the end of the text) becomes afile_citationannotation{type, file_id, filename, index}onoutput_text, streamed throughresponse.output_text.annotation.added. Chat Completions, Anthropic and Gemini have no wire shape for a file citation, so they skip it.files(#548)image_generation_calloutput item{type, id, status, result}, withresultholding the base64 bytes. The items come after the reasoning and web-search items and before the message and tool-call items, on text, tool-only, content+toolCalls andblocksturns. When streaming, each item getsoutput_item.added(result: null), thenresponse.image_generation_call.in_progress,.generatingand.completed, thenoutput_item.done. Onlyimage/*files can be carried this way; any other MIME type answers a 500 withaimock_unsupported_generated_file.inlineDataparts, as feat: scriptable citations (Responses) and model-generated files (Gemini) #550 added.filesis rejected before any byte is written. The response is a 500 withcode: "aimock_unsupported_generated_file"in the provider's error envelope. It uses the same journaling and error path asUnsupportedToolCallErrorforcustomToolCalls. Before this PR the files were dropped silently with a 200.Validation and error codes
Fixture
citationsandfilesare checked on every request that serves them on Chat Completions, Anthropic Messages, Gemini and the OpenAI Responses API. A malformed value answers a 500 before any content is written:aimock_invalid_fixture_citation:citationsis not an array, an entry is not an object,url(orfileId/filenamefor a file citation) is missing or not a non-empty string, an offset is not an integer, offsets fall outside the text (0 <= startIndex <= endIndex <= text length,0 <= index <= text length), an offset splits a UTF-16 surrogate pair, an entry mixes URL and file citation fields, or the turn has no text to cite (tool calls only,blockswith no text block, or empty content).aimock_invalid_fixture_file:filesis not an array, or an entry lacks a non-empty stringmimeTypeor a stringdata.aimock_unsupported_generated_file: any file on Chat Completions or Anthropic Messages, or a non-image/*file on the OpenAI Responses API.Each message names the field, says what was wrong, and says how to fix it. A
nullvalue ornulloptional field is treated as absent, so JSON fixtures that writenullfor an unset key keep working. The newInvalidFixtureOutputError(behind the twoaimock_invalid_fixture_*codes) andUnsupportedGeneratedFileErrorare exported from@copilotkit/aimock. Other wires (Bedrock, Ollama, Cohere, Gemini Interactions, Gemini Live, Realtime) do not read these keys and ignore them, as before.Behavior under misbehavior faults
The fixture is checked on its original turn before any fault applies, so the result is the same with or without
--misbehavior. Only an invalid misbehavior config answers its own 400 or 501 first.citationsorfiles, generated files on Chat Completions or Anthropic, a non-image/*file on Responses) is never faulted. It takes the normal path, is journaled as a 500 that keeps its fixture, and consumes no faulttimesbudget or seed ordinal.aimock_misbehavior_totaldoes not count it. This holds on HTTP and on the Responses WebSocket.citations, so no citation points into text the client never receives. A fault that keeps the text keeps the citations.Load warnings
Fixture load validation reports malformed
citationsandfilesas warnings, with the same problem text the 500 carries, plus which 500 the requests will answer. The fixture still loads, as in the release that added the keys. For example:Compatibility
ChatCompletionAnnotation,annotations?onChatCompletionMessageandSSEDelta,FixtureUrlCitation,FixtureFileCitation,InvalidFixtureOutputErrorandUnsupportedGeneratedFileError. The API reports for every entry point are updated because they carry the citation types transitively.FixtureCitationchanged from an interface to the unionFixtureUrlCitation | FixtureFileCitation. It has never been released: feat: scriptable citations (Responses) and model-generated files (Gemini) #550 merged after 1.46.0, and the type is not in the v1.46.0 tag.FixtureUrlCitationhas the same fields as the old interface, plus an optionaltype: "url".citationsorfilesproduce the same responses as before. I compared 16 responses (Chat Completions, Responses, Anthropic and Gemini, streaming and non-streaming, text and content+toolCalls fixtures) between base508dd9b9and this branch. After normalizing ids and timestamps they are byte-identical. The tests also assert that noannotationskey,citationskey,groundingMetadataorimage_generation_callappears when the field is absent.citationsorfilesthat used to be served with a 200 now answer a 500 on the four wires above. That is the intended fail-loud behavior, and the load warning names each such fixture.Local red-green proof (real server, real wire)
I started the CLI server (
npx tsx src/cli.ts -f fixtures.json) from base508dd9b9(RED) and from this branch (GREEN), and sent the same requests to each.Citations and files
RED (base): no citations on Chat Completions, Anthropic or Gemini, no
image_generation_call, and files ignored with a 200.GREEN (this branch), same requests:
File citations, validation and faults
RED (base): a file citation is served as a broken
url_citationwith no URL, malformed citations and files are served with a 200, a PDF on Responses is dropped silently, and a malformed fixture under a refusal fault is faulted.GREEN (this branch), same requests:
With
--validate-on-load, the base server prints no warning for these fixtures. This branch prints one warning per malformed fixture (fixtures 1, 2 and 4), as shown in the load warnings section above.Through the official SDK clients
Earlier on this branch I also ran the citation and file fixtures through openai 6.49.0 and @anthropic-ai/sdk 0.78.0 against the running server. Non-streaming and accumulated streaming results both carried the annotations and citations shown above. The Responses client reported output types
image_generation_call,messagewithresult: iVBORw0KGgo=. The Chat Completions and Anthropic clients raised a 500 withaimock_unsupported_generated_filefor thefilesfixture.Tests and quality gates
server.test.ts,messages.test.ts,gemini.test.ts,responses.test.ts), the builders' cited source text, validation and load warnings (fixture-citations-files-validation.test.ts), the fault matrix and fault journaling (misbehavior-citations.test.ts,misbehavior-fixture-output-validation.test.ts,misbehavior-fault-journal-order.test.ts,ws-responses-fault-journal-order.test.ts), and theInvalidFixtureOutputErrorexport.pnpm format:check,pnpm lint,pnpm typecheck,pnpm buildandpnpm api:reportare clean.pnpm test: 18539 tests pass.Deferred
These are out of scope for this PR and are tracked in our follow-ups list:
citationsandfiles. Each needs a decision between serving them and rejecting them like Chat Completions and Anthropic.files[].datais not checked for empty or non-base64 content.blocks, treat any thrown error as "cannot serve" without logging it. The fixture still answers its coded 500 on the normal path, andvalidateBlocksreports malformed blocks as a load error.neveronFixtureUrlCitation) and some additional test cases.