Summary
BraintrustStream and wrap_stream_with_span only handle OpenAI Chat Completions streaming chunks (choices[].delta). Google Gemini's streamGenerateContent endpoint emits GenerateContentResponse objects with a completely different structure (candidates[].content.parts[]). All Gemini streaming chunks are silently discarded by the Err(_) => continue fallback in aggregate(), producing an empty aggregated result with no output, no usage metrics, and no TTFT.
This is distinct from #34 (non-streaming Gemini usageMetadata extraction), which covers the non-streaming case. This issue is specifically about the streaming aggregation path.
What is missing
Gemini's streamGenerateContent yields a sequence of GenerateContentResponse JSON objects. Each chunk has this structure:
{
"candidates": [{
"content": {
"parts": [{ "text": "Hello" }],
"role": "model"
},
"finishReason": "STOP"
}]
}
The final chunk includes usageMetadata:
{
"candidates": [...],
"usageMetadata": {
"promptTokenCount": 10,
"candidatesTokenCount": 25,
"totalTokenCount": 35,
"thoughtsTokenCount": 12,
"cachedContentTokenCount": 5
}
}
For tool/function calling, parts contain a functionCall object instead of text:
{
"candidates": [{
"content": {
"parts": [{ "functionCall": { "name": "get_weather", "args": { "location": "NYC" } } }],
"role": "model"
}
}]
}
For Gemini 2.5 thinking models, parts include a thought field:
{ "parts": [{ "text": "Let me think...", "thought": true }] }
Currently in this SDK, BraintrustStream::aggregate() (src/stream.rs) attempts to deserialize each raw chunk as StreamChunk { model, choices, usage }. Gemini chunks have no choices field, so serde_json::from_value fails and the chunk is skipped. This means:
- Text output from
candidates[].content.parts[].text is lost
- Function call output from
candidates[].content.parts[].functionCall is lost
- Thinking content from Gemini 2.5 thought parts is lost
- Usage metrics (
promptTokenCount, candidatesTokenCount, thoughtsTokenCount, cachedContentTokenCount) are never extracted
- Finish reason from
candidates[].finishReason is lost
- TTFT metric is not recorded (the content heuristic
value_has_content() checks for choices which Gemini chunks don't have)
Braintrust docs status
supported (in other language SDKs) — Braintrust documents full Gemini streaming support including token metrics, thinking model support, and function call tracing:
Status for the Rust SDK: not instrumented
Upstream sources
Relationship to existing issues
Local files inspected
src/stream.rs — StreamChunk struct only has model, choices, usage; Gemini chunks have candidates not choices; aggregate() skips all non-parseable chunks via Err(_) => continue; value_has_content() checks for choices array which Gemini chunks lack
src/extractors.rs — extract_openai_usage() and extract_anthropic_usage() both call value.get("usage"); Gemini uses usageMetadata so neither can extract Gemini stream usage
src/lib.rs — wrap_stream_with_span is the primary streaming instrumentation surface; no Gemini-specific path
Cargo.toml — no Google AI / Vertex AI dependencies
- Full codebase grep for
gemini, google, genai, usageMetadata, candidatesTokenCount, candidates — zero results
Summary
BraintrustStreamandwrap_stream_with_spanonly handle OpenAI Chat Completions streaming chunks (choices[].delta). Google Gemini'sstreamGenerateContentendpoint emitsGenerateContentResponseobjects with a completely different structure (candidates[].content.parts[]). All Gemini streaming chunks are silently discarded by theErr(_) => continuefallback inaggregate(), producing an empty aggregated result with no output, no usage metrics, and no TTFT.This is distinct from #34 (non-streaming Gemini
usageMetadataextraction), which covers the non-streaming case. This issue is specifically about the streaming aggregation path.What is missing
Gemini's
streamGenerateContentyields a sequence ofGenerateContentResponseJSON objects. Each chunk has this structure:{ "candidates": [{ "content": { "parts": [{ "text": "Hello" }], "role": "model" }, "finishReason": "STOP" }] }The final chunk includes
usageMetadata:{ "candidates": [...], "usageMetadata": { "promptTokenCount": 10, "candidatesTokenCount": 25, "totalTokenCount": 35, "thoughtsTokenCount": 12, "cachedContentTokenCount": 5 } }For tool/function calling, parts contain a
functionCallobject instead oftext:{ "candidates": [{ "content": { "parts": [{ "functionCall": { "name": "get_weather", "args": { "location": "NYC" } } }], "role": "model" } }] }For Gemini 2.5 thinking models, parts include a
thoughtfield:{ "parts": [{ "text": "Let me think...", "thought": true }] }Currently in this SDK,
BraintrustStream::aggregate()(src/stream.rs) attempts to deserialize each raw chunk asStreamChunk { model, choices, usage }. Gemini chunks have nochoicesfield, soserde_json::from_valuefails and the chunk is skipped. This means:candidates[].content.parts[].textis lostcandidates[].content.parts[].functionCallis lostpromptTokenCount,candidatesTokenCount,thoughtsTokenCount,cachedContentTokenCount) are never extractedcandidates[].finishReasonis lostvalue_has_content()checks forchoiceswhich Gemini chunks don't have)Braintrust docs status
supported (in other language SDKs) — Braintrust documents full Gemini streaming support including token metrics, thinking model support, and function call tracing:
wrapGoogleGenAI()(TypeScript),setup_genai()(Python), token metrics, thinking tokens, cached tokensStatus for the Rust SDK: not instrumented
Upstream sources
streamGenerateContentREST reference: https://ai.google.dev/api/generate-content#method:-models.streamgeneratecontentGenerateContentResponseschema (candidates, usageMetadata): https://ai.google.dev/api/generate-content#v1beta.GenerateContentResponseRelationship to existing issues
extract_openai_usage/extract_anthropic_usageparity for non-streaming Gemini responses. This issue covers the streaming aggregation path (BraintrustStream,wrap_stream_with_span) where chunks have a completely different structure.message_start,content_block_delta, etc.). This issue covers Gemini'sGenerateContentResponsechunk format — a different provider with a different streaming schema.Local files inspected
src/stream.rs—StreamChunkstruct only hasmodel,choices,usage; Gemini chunks havecandidatesnotchoices;aggregate()skips all non-parseable chunks viaErr(_) => continue;value_has_content()checks forchoicesarray which Gemini chunks lacksrc/extractors.rs—extract_openai_usage()andextract_anthropic_usage()both callvalue.get("usage"); Gemini usesusageMetadataso neither can extract Gemini stream usagesrc/lib.rs—wrap_stream_with_spanis the primary streaming instrumentation surface; no Gemini-specific pathCargo.toml— no Google AI / Vertex AI dependenciesgemini,google,genai,usageMetadata,candidatesTokenCount,candidates— zero results