Skip to content

Commit f77a581

Browse files
test(gemini): add streaming metadata cassettes (0xPlaygrounds#1777)
1 parent 327e4d4 commit f77a581

5 files changed

Lines changed: 252 additions & 2 deletions

File tree

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
when:
2+
path: /v1beta/interactions
3+
method: POST
4+
query_param:
5+
- name: alt
6+
value: sse
7+
header:
8+
- name: accept
9+
value: text/event-stream
10+
- name: content-type
11+
value: application/json
12+
body: '{"generation_config":{"temperature":0.0},"input":[{"content":[{"text":"Reply with exactly: interaction metadata ok","type":"text"}],"role":"user"}],"model":"gemini-3-flash-preview","stream":true}'
13+
then:
14+
status: 200
15+
header:
16+
- name: content-type
17+
value: text/event-stream
18+
body: |+
19+
event: interaction.start
20+
data: {"event_type":"interaction.start","interaction":{"id":"v1_REDACTED_1","model":"gemini-3-flash-preview","object":"interaction","status":"in_progress"}}
21+
22+
event: interaction.status_update
23+
data: {"event_type":"interaction.status_update","interaction_id":"v1_REDACTED_1","status":"in_progress"}
24+
25+
event: content.start
26+
data: {"content":{"type":"thought"},"event_type":"content.start","index":0}
27+
28+
event: content.delta
29+
data: {"delta":{"signature":"signature_REDACTED_1","type":"thought_signature"},"event_type":"content.delta","index":0}
30+
31+
event: content.stop
32+
data: {"event_type":"content.stop","index":0}
33+
34+
event: content.start
35+
data: {"content":{"type":"text"},"event_type":"content.start","index":1}
36+
37+
event: content.delta
38+
data: {"delta":{"text":"interaction metadata ok","type":"text"},"event_type":"content.delta","index":1}
39+
40+
event: content.stop
41+
data: {"event_type":"content.stop","index":1}
42+
43+
event: interaction.complete
44+
data: {"event_type":"interaction.complete","interaction":{"created":"1970-01-01T00:00:00Z","id":"v1_REDACTED_1","model":"gemini-3-flash-preview","object":"interaction","role":"model","service_tier":"standard","status":"completed","updated":"1970-01-01T00:00:00Z","usage":{"input_tokens_by_modality":[{"modality":"text","tokens":8}],"total_cached_tokens":0,"total_input_tokens":8,"total_output_tokens":3,"total_thought_tokens":41,"total_tokens":52,"total_tool_use_tokens":0}}}
45+
46+
event: done
47+
data: [DONE]
48+
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
when:
2+
path: /v1beta/models/gemini-2.5-flash:streamGenerateContent
3+
method: POST
4+
query_param:
5+
- name: alt
6+
value: sse
7+
- name: key
8+
value: '[REDACTED]'
9+
header:
10+
- name: accept
11+
value: text/event-stream
12+
- name: content-type
13+
value: application/json
14+
body: '{"contents":[{"parts":[{"text":"Reply with exactly: final metadata ok","thought":false}],"role":"user"}],"generationConfig":null,"safetySettings":null,"systemInstruction":null,"toolConfig":null}'
15+
then:
16+
status: 200
17+
header:
18+
- name: content-type
19+
value: text/event-stream
20+
body: "data: {\"candidates\":[{\"content\":{\"parts\":[{\"text\":\"final metadata ok\"}],\"role\":\"model\"},\"finishReason\":\"STOP\",\"index\":0}],\"modelVersion\":\"gemini-2.5-flash\",\"responseId\":\"id_REDACTED_1\",\"usageMetadata\":{\"candidatesTokenCount\":3,\"promptTokenCount\":8,\"promptTokensDetails\":[{\"modality\":\"TEXT\",\"tokenCount\":8}],\"serviceTier\":\"standard\",\"thoughtsTokenCount\":32,\"totalTokenCount\":43}}\r\n\r\n"
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
when:
2+
path: /v1beta/models/gemini-2.5-flash:streamGenerateContent
3+
method: POST
4+
query_param:
5+
- name: alt
6+
value: sse
7+
- name: key
8+
value: '[REDACTED]'
9+
header:
10+
- name: accept
11+
value: text/event-stream
12+
- name: content-type
13+
value: application/json
14+
body: '{"contents":[{"parts":[{"text":"Reply with exactly: contentless final metadata ok","thought":false}],"role":"user"}],"generationConfig":null,"safetySettings":null,"systemInstruction":null,"toolConfig":null}'
15+
then:
16+
status: 200
17+
header:
18+
- name: content-type
19+
value: text/event-stream
20+
body: "data: {\"candidates\":[{\"content\":{\"parts\":[{\"text\":\"contentless final metadata ok\"}],\"role\":\"model\"},\"finishReason\":\"STOP\",\"index\":0}],\"modelVersion\":\"gemini-2.5-flash\",\"responseId\":\"id_REDACTED_1\",\"usageMetadata\":{\"candidatesTokenCount\":5,\"promptTokenCount\":10,\"promptTokensDetails\":[{\"modality\":\"TEXT\",\"tokenCount\":10}],\"serviceTier\":\"standard\",\"thoughtsTokenCount\":27,\"totalTokenCount\":42}}\r\n\r\n"

tests/providers/gemini/cassette/interactions_api.rs

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -240,3 +240,50 @@ async fn streaming_interaction() {
240240
)
241241
.await;
242242
}
243+
244+
#[tokio::test]
245+
async fn streaming_final_metadata_exposes_model_version() {
246+
super::super::support::with_gemini_interactions_cassette(
247+
"interactions_api/streaming_final_metadata_exposes_model_version",
248+
|client| async move {
249+
let model = client.completion_model("gemini-3-flash-preview");
250+
let request = model
251+
.completion_request("Reply with exactly: interaction metadata ok")
252+
.temperature(0.0)
253+
.build();
254+
let mut stream = model.stream(request).await.expect("stream should start");
255+
256+
let mut text = String::new();
257+
let mut final_model_version = None;
258+
let mut final_response_count = 0;
259+
let mut saw_usage = false;
260+
while let Some(chunk) = stream.next().await {
261+
match chunk.expect("stream chunk should succeed") {
262+
StreamedAssistantContent::Text(delta) => text.push_str(&delta.text),
263+
StreamedAssistantContent::Final(response) => {
264+
final_response_count += 1;
265+
saw_usage = response.token_usage().is_some();
266+
final_model_version = response.model_version.clone();
267+
}
268+
_ => {}
269+
}
270+
}
271+
272+
assert_nonempty_response(&text);
273+
assert_eq!(
274+
final_response_count, 1,
275+
"stream should yield exactly one final response"
276+
);
277+
assert_eq!(
278+
final_model_version.as_deref(),
279+
Some("gemini-3-flash-preview"),
280+
"expected Interactions stream final response to expose Interaction.model"
281+
);
282+
assert!(
283+
saw_usage,
284+
"expected final response to expose Interactions token usage"
285+
);
286+
},
287+
)
288+
.await;
289+
}

tests/providers/gemini/cassette/streaming.rs

Lines changed: 117 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
//! Gemini streaming coverage, including the migrated example path.
22
3+
use futures::StreamExt;
34
use rig::client::CompletionClient;
5+
use rig::completion::{CompletionModel, GetTokenUsage};
46
use rig::providers::gemini;
57
use rig::providers::gemini::completion::gemini_api_types::{
6-
AdditionalParameters, GenerationConfig, ThinkingConfig, ThinkingLevel,
8+
AdditionalParameters, FinishReason, GenerationConfig, ThinkingConfig, ThinkingLevel,
79
};
8-
use rig::streaming::StreamingPrompt;
10+
use rig::streaming::{StreamedAssistantContent, StreamingPrompt};
911

1012
use crate::support::{
1113
STREAMING_PREAMBLE, STREAMING_PROMPT, assert_nonempty_response, collect_stream_final_response,
@@ -76,3 +78,116 @@ async fn example_streaming_prompt() {
7678
)
7779
.await;
7880
}
81+
82+
#[tokio::test]
83+
async fn final_metadata_exposes_finish_reason_and_model_version() {
84+
super::super::support::with_gemini_cassette(
85+
"streaming/final_metadata_exposes_finish_reason_and_model_version",
86+
|client| async move {
87+
let model = client.completion_model(gemini::completion::GEMINI_2_5_FLASH);
88+
let request = model
89+
.completion_request("Reply with exactly: final metadata ok")
90+
.temperature(0.0)
91+
.build();
92+
let mut stream = model.stream(request).await.expect("stream should start");
93+
94+
let mut text = String::new();
95+
let mut final_response = None;
96+
let mut final_response_count = 0;
97+
while let Some(chunk) = stream.next().await {
98+
match chunk.expect("stream chunk should succeed") {
99+
StreamedAssistantContent::Text(delta) => text.push_str(&delta.text),
100+
StreamedAssistantContent::Final(response) => {
101+
final_response_count += 1;
102+
final_response = Some(response);
103+
}
104+
_ => {}
105+
}
106+
}
107+
108+
assert_nonempty_response(&text);
109+
assert_eq!(
110+
final_response_count, 1,
111+
"stream should yield exactly one final response"
112+
);
113+
let final_response = final_response.expect("stream should yield final metadata");
114+
assert!(
115+
matches!(final_response.finish_reason, Some(FinishReason::Stop)),
116+
"expected STOP finish reason, got {:?}",
117+
final_response.finish_reason
118+
);
119+
assert_eq!(
120+
final_response.model_version.as_deref(),
121+
Some(gemini::completion::GEMINI_2_5_FLASH),
122+
"expected resolved Gemini model version to be surfaced"
123+
);
124+
assert!(
125+
final_response.token_usage().is_some(),
126+
"expected final response to expose token usage"
127+
);
128+
},
129+
)
130+
.await;
131+
}
132+
133+
#[tokio::test]
134+
async fn final_metadata_handles_terminal_finish_reason_chunk() {
135+
super::super::support::with_gemini_cassette(
136+
"streaming/final_metadata_handles_terminal_finish_reason_chunk",
137+
|client| async move {
138+
let model = client.completion_model(gemini::completion::GEMINI_2_5_FLASH);
139+
let request = model
140+
.completion_request("Reply with exactly: contentless final metadata ok")
141+
.temperature(0.0)
142+
.build();
143+
let mut stream = model.stream(request).await.expect("stream should start");
144+
145+
let mut text = String::new();
146+
let mut final_response = None;
147+
let mut final_response_count = 0;
148+
while let Some(chunk) = stream.next().await {
149+
match chunk.expect("stream chunk should succeed") {
150+
StreamedAssistantContent::Text(delta) => text.push_str(&delta.text),
151+
StreamedAssistantContent::Final(response) => {
152+
final_response_count += 1;
153+
final_response = Some(response);
154+
}
155+
_ => {}
156+
}
157+
}
158+
159+
assert_eq!(text.trim(), "contentless final metadata ok");
160+
assert_eq!(
161+
final_response_count, 1,
162+
"terminal finish chunk should yield exactly one final response"
163+
);
164+
let final_response = final_response.expect("stream should yield final metadata");
165+
assert!(
166+
matches!(final_response.finish_reason, Some(FinishReason::Stop)),
167+
"expected STOP finish reason from contentless terminal chunk, got {:?}",
168+
final_response.finish_reason
169+
);
170+
assert_eq!(
171+
final_response.model_version.as_deref(),
172+
Some(gemini::completion::GEMINI_2_5_FLASH),
173+
"expected modelVersion from terminal chunks to be retained"
174+
);
175+
let usage = final_response
176+
.token_usage()
177+
.expect("expected final response to expose token usage");
178+
assert!(
179+
usage.input_tokens > 0,
180+
"expected positive input token usage, got {usage:?}"
181+
);
182+
assert!(
183+
usage.output_tokens > 0,
184+
"expected positive output token usage, got {usage:?}"
185+
);
186+
assert!(
187+
usage.total_tokens >= usage.input_tokens + usage.output_tokens,
188+
"expected total token usage to include input and output tokens, got {usage:?}"
189+
);
190+
},
191+
)
192+
.await;
193+
}

0 commit comments

Comments
 (0)