mirror of
https://github.com/fawney19/Aether.git
synced 2026-10-05 00:47:48 +08:00
Gemini runs `googleSearch` inside Google. The search leaves no client-visible tool call, and the evidence arrives only as `candidates[].groundingMetadata`. Every cross-format target dropped it wholesale, so a grounded answer reached OpenAI- and Claude-shaped clients as prose that names its sources with nothing structured behind it: no `annotations`, no `citations`, no `url_citation`. Callers that verify grounding — the common "did this model actually search?" check — saw a 200 with no evidence and had to treat the answer as ungrounded. Adapters now normalise `groundingMetadata` into neutral citations and each target renders its own family's standard shape: `url_citation` annotations for `openai:chat` and `openai:responses`, and `web_search_result_location` citations on the text block for `claude:messages`. Gemini reports segment bounds as UTF-8 byte offsets while both targets count characters, so the bounds are converted rather than copied. Streaming is covered too, since that is what grounded traffic actually uses. A new `CanonicalStreamEvent::Citations` carries the neutral list once the answer text is whole — the offsets index into the finished answer, so it rides just ahead of `Finish` rather than as a delta per chunk — and each client emitter renders it: `delta.annotations` chunks, `response.output_text.annotation.added` events (also kept on the finished message item so clients that only read `response.completed` see them), and `citations_delta` content block deltas. For reference, CLIProxyAPI projects grounding only in its antigravity→Claude translator, and only when the client declared a typed `web_search_*` tool; its OpenAI and plain Gemini translators have no grounding handling at all. The citation shape here matches theirs, but the coverage is deliberately wider: all three targets, streaming and non-streaming, with no dependency on a declared tool. Co-Authored-By: Claude Opus 5 <[email protected]>