Skip to content

fix(openai): project aggregated citations to top-level field instead of leaking transformer_metadata - #2572

Merged
llc1123 merged 1 commit into
looplj:unstablefrom
remote-controlled-man:codex/axonhub-selfdisc3-citations-envelope
Oct 1, 2026
Merged

llc1123 merged 1 commit into
looplj:unstablefrom
remote-controlled-man:codex/axonhub-selfdisc3-citations-envelope

Conversation

@remote-controlled-man

@remote-controlled-man remote-controlled-man commented Sep 28, 2026 •

Copy link
Copy Markdown
Contributor

Problem

When chat completion streams carry provider citations (Perplexity /
OpenRouter style, top-level citations on each chunk),
AggregateStreamChunks collects them into
llm.Response.TransformerMetadata["citations"] and marshals the unified
llm.Response directly as the response body. The wire shape therefore
contains the internal envelope:

{"id":"chatcmpl-123",...,"transformer_metadata":{"citations":["https://..."]}}

and no top-level citations — while the streaming chunks themselves and
the direct non-streaming path (ResponseFromLLM, which extracts citations
from TransformerMetadata into the top-level field) both expose
top-level citations. The TransformerMetadata field is documented as
"ignored when serializing to JSON and is only used internally by
transformers"; the aggregator is the one place that marshals the unified
response verbatim, so the internal envelope leaks to clients.

Two caller-visible consequences:

  1. A non-streaming client on a stream-required channel
    (autoAggregateStream) receives citations in a shape no OpenAI client
    reads — the citations are effectively lost.
  2. The persisted request-log body for any streaming citation response
    records the internal envelope instead of the API shape; re-parsing it
    through ToLLMResponse (which reads top-level citations) loses them.

Fix

Keep collecting and deduplicating citations as today, then marshal a thin
client-facing wrapper that projects them back to the top-level
citations field. The internal envelope is never serialized. Streams
without citations produce byte-for-byte identical output.

Note on the existing test

TestAggregateStreamChunks_WithCitations previously asserted the leaked
shape (citations under transformer_metadata after a JSON round-trip).
It pinned the bug rather than a contract: the field's own documentation
says it is never serialized, and ResponseFromLLM is the codebase's
sanctioned unified→wire projection, which places citations top-level. The
test now asserts the corrected shape; its content assertions are
unchanged.

Testing

  • New aggregator_citations_test.go: citations spread across chunks are
    deduplicated, sorted, and emitted top-level; the body contains no
    transformer_metadata; content/finish_reason/usage intact. A second
    test guards that citation-free streams keep byte-identical shape. The
    main test fails on the base commit with the leaked envelope visible in
    the output.
  • go test ./transformer/openai/... — pass.
  • go test ./... (llm module) — pass except the pre-existing
    Windows-only
    TestWebSocketStreamReturnsErrorWhenReusedConnectionClosesBeforeEvent
    failure in openai/responses, which fails identically on the clean
    base and is unrelated to this change.
  • go vet ./... — clean.

Summary by CodeRabbit

  • Bug Fixes
    • Aggregated streaming responses now include deduplicated, sorted citations in a top-level citations field when citations are present.
    • Responses without citations omit the citations field.
    • Aggregation continues to preserve the response message, finish reason, and usage information, so adding citations does not change those parts of the response.

Copilot AI lite review requested due to automatic review settings September 28, 2026 02:00

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@coderabbitai

coderabbitai Bot commented Sep 28, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

Navigate logical layers of code changes, visualize relationships, and explore their blast radius.

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Advanced

Run ID: 79a4c317-58dc-4293-a5af-2a7156070d01

📥 Commits

Reviewing files that changed from the base of the PR and between f0e28a8 and 4af1197.

📒 Files selected for processing (1)
  • llm/transformer/openai/aggregator_citations_test.go

Included review availability: This review used your included allowance. Your plan provides up to 8 included reviews per hour; 6 remain after this review.


📝 Walkthrough

Walkthrough

The aggregator now serializes deduplicated, sorted citations in the top-level citations field. It omits that field when no citations are present. Tests verify citation responses, other aggregated fields, and the absence of transformer_metadata.

Changes

Citation serialization

Layer / File(s) Summary
Response serialization
llm/transformer/openai/aggregator.go, llm/transformer/openai/aggregator_citations_test.go, llm/transformer/openai/aggregator_test.go
The aggregator serializes citations through a response wrapper with an optional top-level citations field. Tests check sorting, deduplication, preservation of message, finish reason, and usage, and omission of transformer_metadata and empty citation fields.

Priority: ➖ Normal

Estimated code review effort: 2 (Simple) | ~10 minutes

Change: Bug fix

Merge Risk: ⚪ Minimal · up to 4af11

This change moves aggregated citations into the top-level response field and leaves citation-free output unchanged. Tests cover the new shape, so no merge-blocking risk is evident.

Security Architecture Review

Security architecture risk: 🔵 Low · up to 4af11

The change aligns aggregated citations with the existing response format without changing their source or granting new permissions. Risk is limited, but downstream consumers of stored response bodies were not fully verified.

Retained concerns
No architecture-level concerns identified.

Security review details

Security Blast Radius

  • observed — The verified exposure is provider-supplied citation strings in an aggregated response body. The inspected aggregator neither fetches citation URLs nor executes their contents. Downstream rendering, stored-body consumption, and deployment-wide tenant or environment exposure were not established.

Trust Boundaries and Controls

  • inferred — The changed projection does not introduce a new citation source, identity transition, permission decision, or privileged sink within the inspected path. Existing direct responses already expose citations in this wire position. These facts limit the introduced risk without proving that every downstream consumer is safe.

Resilience and Maintainability Implications

  • inferred — The changed citation state is invocation-local and is returned only as serialized bytes. It introduces no shared-state mutation, reservation, or recovery transition. Repeated aggregation retains deterministic citation ordering and deduplication; interruption or serialization failure creates no new shared cleanup obligation in this function.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 66.67% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 6 functions across 3 files. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly describes the main change: moving aggregated citations to the top-level field instead of serializing transformer_metadata. It is specific and concise.
  • Fix all pre-merge checks with AI
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create a new PR

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

…of leaking transformer_metadata

When chat completion streams carry provider citations (Perplexity /
OpenRouter style), AggregateStreamChunks collected them into
llm.Response.TransformerMetadata and marshaled the unified response
directly as the client body. The wire shape therefore contained the
internal transformer_metadata envelope — a field documented as never
serialized — and no top-level citations, while streaming chunks and the
direct non-streaming path (ResponseFromLLM) both expose top-level
citations. Non-streaming clients on stream-required channels and the
persisted request-log bodies received the wrong shape.

Marshal a thin client-facing wrapper that projects the collected,
deduplicated citations back to the top-level citations field and never
serializes the internal envelope. Streams without citations produce
byte-identical output.
@llc1123
llc1123 force-pushed the codex/axonhub-selfdisc3-citations-envelope branch from f0e28a8 to 4af1197 Compare October 1, 2026 09:12
@llc1123

llc1123 commented Oct 1, 2026

Copy link
Copy Markdown
Collaborator

@coderabbitai review

Please review current head 4af11979d0926c90c55d4ebde9fbb667b3e9aea9, rebased onto e26e52bb5d0398eb757ee80819aa636e84cbc3a0.

The contributor's unique aggregator fix is preserved. Regression inputs now exercise unsorted, duplicated citations, and the citation-free test asserts the exact unchanged response body. No shared response metadata tags or persistence readers were changed.

Validation:

  • The citation regression fails against the clean baseline because citations remain under transformer_metadata.
  • go test -race -shuffle=on -count=1 ./transformer/openai ./pipeline passes from the llm module.
  • A manual driver runs a non-streaming request through pipeline.Process, with a stream-required outbound transformer and a real local SSE HTTP server. It returns HTTP 200, application/json, stream=false, intact content and usage, and top-level sorted, deduplicated citations. The citation-free run omits both citation and metadata fields.
  • The commit preserves the contributor author and is signed by 粒粒橙 <[email protected]>, fingerprint SHA256:MCdIIoZUa7KUrSQN7S/m80ISdLxAE1D2f1c/Sr2Wb38.

Local build and lint commands were not run, per repository rules.

@coderabbitai

coderabbitai Bot commented Oct 1, 2026 •

Copy link
Copy Markdown
Contributor
⚠️ Action not completed

Already reviewed the last commit. Use @coderabbitai full review to rerun a review of the entire changeset.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@llc1123
llc1123 merged commit 00f0de8 into looplj:unstable Oct 1, 2026
6 checks passed
Edint386 pushed a commit to Edint386/axonhub that referenced this pull request Oct 9, 2026
…of leaking transformer_metadata (looplj#2572)

When chat completion streams carry provider citations (Perplexity /
OpenRouter style), AggregateStreamChunks collected them into
llm.Response.TransformerMetadata and marshaled the unified response
directly as the client body. The wire shape therefore contained the
internal transformer_metadata envelope — a field documented as never
serialized — and no top-level citations, while streaming chunks and the
direct non-streaming path (ResponseFromLLM) both expose top-level
citations. Non-streaming clients on stream-required channels and the
persisted request-log bodies received the wrong shape.

Marshal a thin client-facing wrapper that projects the collected,
deduplicated citations back to the top-level citations field and never
serializes the internal envelope. Streams without citations produce
byte-identical output.
@remote-controlled-man
remote-controlled-man deleted the codex/axonhub-selfdisc3-citations-envelope branch October 13, 2026 01:32
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.

3 participants