@@ -53,14 +53,14 @@ struct GenerateContentIntegrationTests {
5353 ( InstanceConfig . vertexAI_v1beta_global_appCheckLimitedUse, ModelNames . gemini2_5_FlashLite) ,
5454 ( InstanceConfig . googleAI_v1beta, ModelNames . gemini3_1_FlashLitePreview) ,
5555 ( InstanceConfig . googleAI_v1beta_appCheckLimitedUse, ModelNames . gemini3_1_FlashLitePreview) ,
56- ( InstanceConfig . googleAI_v1beta, ModelNames . gemma3_4B ) ,
57- ( InstanceConfig . googleAI_v1beta_freeTier, ModelNames . gemma3_4B ) ,
56+ ( InstanceConfig . googleAI_v1beta, ModelNames . gemma4_31B ) ,
57+ ( InstanceConfig . googleAI_v1beta_freeTier, ModelNames . gemma4_31B ) ,
5858 // Note: The following configs are commented out for easy one-off manual testing.
5959 // (InstanceConfig.googleAI_v1beta_freeTier, ModelNames.gemini2_5_FlashLite),
6060 // (InstanceConfig.googleAI_v1beta_staging, ModelNames.gemini2_5_FlashLite),
61- // (InstanceConfig.googleAI_v1beta_staging, ModelNames.gemma3_4B ),
61+ // (InstanceConfig.googleAI_v1beta_staging, ModelNames.gemma4_31B ),
6262 // (InstanceConfig.googleAI_v1beta_freeTier_bypassProxy, ModelNames.gemini2_5_FlashLite),
63- // (InstanceConfig.googleAI_v1beta_freeTier_bypassProxy, ModelNames.gemma3_4B ),
63+ // (InstanceConfig.googleAI_v1beta_freeTier_bypassProxy, ModelNames.gemma4_31B ),
6464 ] )
6565 func generateContent( _ config: InstanceConfig , modelName: String ) async throws {
6666 let model = FirebaseAI . componentInstance ( config) . generativeModel (
@@ -82,15 +82,13 @@ struct GenerateContentIntegrationTests {
8282 #expect( promptTokensDetails. modality == . text)
8383 #expect( promptTokensDetails. tokenCount == usageMetadata. promptTokenCount)
8484 // No thoughts in Flash Lite.
85- #expect( usageMetadata. thoughtsTokenCount == 0 )
86- // The fields `candidatesTokenCount` and `candidatesTokensDetails` are not included when using
87- // Gemma models.
88- if modelName. hasPrefix ( " gemini-3 " ) {
85+ if !modelName. contains ( " flash-lite " ) {
86+ #expect( usageMetadata. thoughtsTokenCount > 0 )
87+ }
88+ // The `candidatesTokensDetails` field is not included when using Gemini 3 or Gemma models.
89+ if modelName. hasPrefix ( " gemini-3 " ) || modelName. hasPrefix ( " gemma " ) {
8990 #expect( usageMetadata. candidatesTokenCount == 2 )
9091 #expect( usageMetadata. candidatesTokensDetails. isEmpty)
91- } else if modelName. hasPrefix ( " gemma " ) {
92- #expect( usageMetadata. candidatesTokenCount == 0 )
93- #expect( usageMetadata. candidatesTokensDetails. isEmpty)
9492 } else {
9593 #expect( usageMetadata. candidatesTokenCount. isEqual ( to: 3 , accuracy: tokenCountAccuracy) )
9694 #expect( usageMetadata. candidatesTokensDetails. count == 1 )
@@ -528,15 +526,15 @@ struct GenerateContentIntegrationTests {
528526 ) ,
529527 ( InstanceConfig . googleAI_v1beta, ModelNames . gemini2_5_FlashLite) ,
530528 ( InstanceConfig . googleAI_v1beta_appCheckLimitedUse, ModelNames . gemini2_5_FlashLite) ,
531- ( InstanceConfig . googleAI_v1beta, ModelNames . gemma3_4B ) ,
529+ ( InstanceConfig . googleAI_v1beta, ModelNames . gemma4_31B ) ,
532530 // Note: The following configs are commented out for easy one-off manual testing.
533531 // (InstanceConfig.vertexAI_v1beta_staging, ModelNames.gemini2_5_FlashLite),
534532 // (InstanceConfig.googleAI_v1beta_staging, ModelNames.gemini2_5_FlashLite),
535- // (InstanceConfig.googleAI_v1beta_staging, ModelNames.gemma3_4B ),
533+ // (InstanceConfig.googleAI_v1beta_staging, ModelNames.gemma4_31B ),
536534 // (InstanceConfig.googleAI_v1beta_freeTier_bypassProxy, ModelNames.gemini2_5_FlashLite),
537- // (InstanceConfig.googleAI_v1beta_freeTier_bypassProxy, ModelNames.gemma3_4B ),
535+ // (InstanceConfig.googleAI_v1beta_freeTier_bypassProxy, ModelNames.gemma4_31B ),
538536// (InstanceConfig.googleAI_v1beta_freeTier, ModelNames.gemini2_5_FlashLite),
539- // (InstanceConfig.googleAI_v1beta_freeTier, ModelNames.gemma3_4B ),
537+ // (InstanceConfig.googleAI_v1beta_freeTier, ModelNames.gemma4_31B ),
540538 ] )
541539 func generateContentStream( _ config: InstanceConfig , modelName: String ) async throws {
542540 let expectedResponse = [
@@ -566,27 +564,41 @@ struct GenerateContentIntegrationTests {
566564 textValues. append ( text)
567565 } else if let finishReason = value. candidates. first? . finishReason {
568566 #expect( finishReason == . stop)
567+ } else if let thoughtSummary = value. thoughtSummary {
568+ #expect( !thoughtSummary. isEmpty)
569569 } else {
570570 Issue . record ( " Expected a candidate with a `TextPart` or a `finishReason`; got \( value) . " )
571571 }
572572 }
573573
574+ // Tests the text derived from streaming directly
575+ let modelJSONData = try #require( textValues. joined ( ) . data ( using: . utf8) )
576+ let response = try JSONDecoder ( ) . decode ( [ String ] . self, from: modelJSONData)
577+ #expect( response == expectedResponse)
578+
574579 let userHistory = try #require( chat. history. first)
575580 #expect( userHistory. role == " user " )
576581 #expect( userHistory. parts. count == 1 )
577582 let promptTextPart = try #require( userHistory. parts. first as? TextPart )
578583 #expect( promptTextPart. text == prompt)
579584 let modelHistory = try #require( chat. history. last)
580585 #expect( modelHistory. role == " model " )
581- if modelName. hasPrefix ( " gemini-3.1- " ) {
582- #expect( modelHistory. parts. count == 2 )
583- } else {
584- #expect( modelHistory. parts. count == 1 )
586+ let textParts = modelHistory. parts. compactMap { $0 as? TextPart } . filter {
587+ !$0. isThoughtOrRelated ( )
585588 }
586- let modelTextPart = try #require( modelHistory. parts. first as? TextPart )
587- let modelJSONData = try #require( modelTextPart. text. data ( using: . utf8) )
588- let response = try JSONDecoder ( ) . decode ( [ String ] . self, from: modelJSONData)
589- #expect( response == expectedResponse)
589+ if textParts. count > 1 {
590+ Issue . record ( " Found multiple text parts: \( textParts) " )
591+ }
592+ #expect(
593+ textParts. count == 1 ,
594+ " The model should reply with exactly one (non thought) text response. "
595+ )
596+
597+ // Tests the text derived from the chat history
598+ let historyTextPart = try #require( textParts. first)
599+ let historyModelJSONData = try #require( historyTextPart. text. data ( using: . utf8) )
600+ let historyResponse = try JSONDecoder ( ) . decode ( [ String ] . self, from: historyModelJSONData)
601+ #expect( historyResponse == expectedResponse)
590602 }
591603
592604 @Test ( arguments: [
@@ -665,3 +677,16 @@ struct GenerateContentIntegrationTests {
665677 }
666678 }
667679}
680+
681+ extension TextPart {
682+ /// Whether this text part is a thought or thought related text part.
683+ ///
684+ /// In such cases, it can be ignored for display and testing purposes.
685+ ///
686+ /// We use this over just a standard `isThought` check so that we can
687+ /// catch cases where the gemini model sends a text part with empty text that just
688+ /// acts as the last thought of the model.
689+ func isThoughtOrRelated( ) -> Bool {
690+ return isThought || ( thoughtSignature != nil && text. isEmpty)
691+ }
692+ }
0 commit comments