@@ -47,12 +47,24 @@ struct GenerateContentIntegrationTests {
47
47
storage = Storage . storage ( )
48
48
}
49
49
50
- @Test ( arguments: InstanceConfig . allConfigs)
51
- func generateContent( _ config: InstanceConfig ) async throws {
50
+ @Test ( arguments: [
51
+ ( InstanceConfig . vertexAI_v1, ModelNames . gemini2FlashLite) ,
52
+ ( InstanceConfig . vertexAI_v1_staging, ModelNames . gemini2FlashLite) ,
53
+ ( InstanceConfig . vertexAI_v1beta, ModelNames . gemini2FlashLite) ,
54
+ ( InstanceConfig . vertexAI_v1beta_staging, ModelNames . gemini2FlashLite) ,
55
+ ( InstanceConfig . googleAI_v1beta, ModelNames . gemini2FlashLite) ,
56
+ ( InstanceConfig . googleAI_v1beta, ModelNames . gemma3_27B) ,
57
+ ( InstanceConfig . googleAI_v1beta_staging, ModelNames . gemini2FlashLite) ,
58
+ ( InstanceConfig . googleAI_v1beta_staging, ModelNames . gemma3_27B) ,
59
+ ( InstanceConfig . googleAI_v1_freeTier_bypassProxy, ModelNames . gemini2FlashLite) ,
60
+ ( InstanceConfig . googleAI_v1beta_freeTier_bypassProxy, ModelNames . gemini2FlashLite) ,
61
+ ( InstanceConfig . googleAI_v1beta_freeTier_bypassProxy, ModelNames . gemma3_27B) ,
62
+ ] )
63
+ func generateContent( _ config: InstanceConfig , modelName: String ) async throws {
52
64
let model = FirebaseAI . componentInstance ( config) . generativeModel (
53
- modelName: ModelNames . gemini2FlashLite ,
65
+ modelName: modelName ,
54
66
generationConfig: generationConfig,
55
- safetySettings: safetySettings
67
+ safetySettings: safetySettings,
56
68
)
57
69
let prompt = " Where is Google headquarters located? Answer with the city name only. "
58
70
@@ -62,17 +74,22 @@ struct GenerateContentIntegrationTests {
62
74
#expect( text == " Mountain View " )
63
75
64
76
let usageMetadata = try #require( response. usageMetadata)
65
- #expect( usageMetadata. promptTokenCount == 13 )
77
+ #expect( usageMetadata. promptTokenCount. isEqual ( to : 13 , accuracy : tokenCountAccuracy ) )
66
78
#expect( usageMetadata. candidatesTokenCount. isEqual ( to: 3 , accuracy: tokenCountAccuracy) )
67
79
#expect( usageMetadata. totalTokenCount. isEqual ( to: 16 , accuracy: tokenCountAccuracy) )
68
80
#expect( usageMetadata. promptTokensDetails. count == 1 )
69
81
let promptTokensDetails = try #require( usageMetadata. promptTokensDetails. first)
70
82
#expect( promptTokensDetails. modality == . text)
71
83
#expect( promptTokensDetails. tokenCount == usageMetadata. promptTokenCount)
72
- #expect( usageMetadata. candidatesTokensDetails. count == 1 )
73
- let candidatesTokensDetails = try #require( usageMetadata. candidatesTokensDetails. first)
74
- #expect( candidatesTokensDetails. modality == . text)
75
- #expect( candidatesTokensDetails. tokenCount == usageMetadata. candidatesTokenCount)
84
+ // The field `candidatesTokensDetails` is not included when using Gemma models.
85
+ if modelName == ModelNames . gemma3_27B {
86
+ #expect( usageMetadata. candidatesTokensDetails. isEmpty)
87
+ } else {
88
+ #expect( usageMetadata. candidatesTokensDetails. count == 1 )
89
+ let candidatesTokensDetails = try #require( usageMetadata. candidatesTokensDetails. first)
90
+ #expect( candidatesTokensDetails. modality == . text)
91
+ #expect( candidatesTokensDetails. tokenCount == usageMetadata. candidatesTokenCount)
92
+ }
76
93
}
77
94
78
95
@Test (
@@ -168,24 +185,35 @@ struct GenerateContentIntegrationTests {
168
185
169
186
// MARK: Streaming Tests
170
187
171
- @Test ( arguments: InstanceConfig . allConfigs)
172
- func generateContentStream( _ config: InstanceConfig ) async throws {
173
- let expectedText = """
174
- 1. Mercury
175
- 2. Venus
176
- 3. Earth
177
- 4. Mars
178
- 5. Jupiter
179
- 6. Saturn
180
- 7. Uranus
181
- 8. Neptune
182
- """
188
+ @Test ( arguments: [
189
+ ( InstanceConfig . vertexAI_v1, ModelNames . gemini2FlashLite) ,
190
+ ( InstanceConfig . vertexAI_v1_staging, ModelNames . gemini2FlashLite) ,
191
+ ( InstanceConfig . vertexAI_v1beta, ModelNames . gemini2FlashLite) ,
192
+ ( InstanceConfig . vertexAI_v1beta_staging, ModelNames . gemini2FlashLite) ,
193
+ ( InstanceConfig . googleAI_v1beta, ModelNames . gemini2FlashLite) ,
194
+ ( InstanceConfig . googleAI_v1beta, ModelNames . gemma3_27B) ,
195
+ ( InstanceConfig . googleAI_v1beta_staging, ModelNames . gemini2FlashLite) ,
196
+ ( InstanceConfig . googleAI_v1beta_staging, ModelNames . gemma3_27B) ,
197
+ ( InstanceConfig . googleAI_v1_freeTier_bypassProxy, ModelNames . gemini2FlashLite) ,
198
+ ( InstanceConfig . googleAI_v1beta_freeTier_bypassProxy, ModelNames . gemini2FlashLite) ,
199
+ ( InstanceConfig . googleAI_v1beta_freeTier_bypassProxy, ModelNames . gemma3_27B) ,
200
+ ] )
201
+ func generateContentStream( _ config: InstanceConfig , modelName: String ) async throws {
202
+ let expectedResponse = [
203
+ " Mercury " , " Venus " , " Earth " , " Mars " , " Jupiter " , " Saturn " , " Uranus " , " Neptune " ,
204
+ ]
183
205
let prompt = """
184
- What are the names of the planets in the solar system, ordered from closest to furthest from
185
- the sun? Answer with a Markdown numbered list of the names and no other text.
206
+ Generate a JSON array of strings. The array must contain the names of the planets in Earth's \
207
+ solar system, ordered from closest to furthest from the Sun.
208
+
209
+ Constraints:
210
+ - Output MUST be only the JSON array.
211
+ - Do NOT include any introductory or explanatory text.
212
+ - Do NOT wrap the JSON in Markdown code blocks (e.g., ```json ... ``` or ``` ... ```).
213
+ - The response must start with '[' and end with ']'.
186
214
"""
187
215
let model = FirebaseAI . componentInstance ( config) . generativeModel (
188
- modelName: ModelNames . gemini2FlashLite ,
216
+ modelName: modelName ,
189
217
generationConfig: generationConfig,
190
218
safetySettings: safetySettings
191
219
)
@@ -194,7 +222,13 @@ struct GenerateContentIntegrationTests {
194
222
let stream = try chat. sendMessageStream ( prompt)
195
223
var textValues = [ String] ( )
196
224
for try await value in stream {
197
- try textValues. append ( #require( value. text) )
225
+ if let text = value. text {
226
+ textValues. append ( text)
227
+ } else if let finishReason = value. candidates. first? . finishReason {
228
+ #expect( finishReason == . stop)
229
+ } else {
230
+ Issue . record ( " Expected a candidate with a `TextPart` or a `finishReason`; got \( value) . " )
231
+ }
198
232
}
199
233
200
234
let userHistory = try #require( chat. history. first)
@@ -206,11 +240,9 @@ struct GenerateContentIntegrationTests {
206
240
#expect( modelHistory. role == " model " )
207
241
#expect( modelHistory. parts. count == 1 )
208
242
let modelTextPart = try #require( modelHistory. parts. first as? TextPart )
209
- let modelText = modelTextPart. text. trimmingCharacters ( in: . whitespacesAndNewlines)
210
- #expect( modelText == expectedText)
211
- #expect( textValues. count > 1 )
212
- let text = textValues. joined ( ) . trimmingCharacters ( in: . whitespacesAndNewlines)
213
- #expect( text == expectedText)
243
+ let modelJSONData = try #require( modelTextPart. text. data ( using: . utf8) )
244
+ let response = try JSONDecoder ( ) . decode ( [ String ] . self, from: modelJSONData)
245
+ #expect( response == expectedResponse)
214
246
}
215
247
216
248
// MARK: - App Check Tests
0 commit comments