GenerateContentIntegrationTests.swift 9.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240
  1. // Copyright 2025 Google LLC
  2. //
  3. // Licensed under the Apache License, Version 2.0 (the "License");
  4. // you may not use this file except in compliance with the License.
  5. // You may obtain a copy of the License at
  6. //
  7. // http://www.apache.org/licenses/LICENSE-2.0
  8. //
  9. // Unless required by applicable law or agreed to in writing, software
  10. // distributed under the License is distributed on an "AS IS" BASIS,
  11. // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  12. // See the License for the specific language governing permissions and
  13. // limitations under the License.
  14. import FirebaseAuth
  15. import FirebaseCore
  16. import FirebaseStorage
  17. import FirebaseVertexAI
  18. import Testing
  19. import VertexAITestApp
  20. #if canImport(UIKit)
  21. import UIKit
  22. #endif // canImport(UIKit)
  23. @testable import struct FirebaseVertexAI.BackendError
  24. @Suite(.serialized)
  25. struct GenerateContentIntegrationTests {
  26. // Set temperature, topP and topK to lowest allowed values to make responses more deterministic.
  27. let generationConfig = GenerationConfig(temperature: 0.0, topP: 0.0, topK: 1)
  28. let safetySettings = [
  29. SafetySetting(harmCategory: .harassment, threshold: .blockLowAndAbove),
  30. SafetySetting(harmCategory: .hateSpeech, threshold: .blockLowAndAbove),
  31. SafetySetting(harmCategory: .sexuallyExplicit, threshold: .blockLowAndAbove),
  32. SafetySetting(harmCategory: .dangerousContent, threshold: .blockLowAndAbove),
  33. SafetySetting(harmCategory: .civicIntegrity, threshold: .blockLowAndAbove),
  34. ]
  35. // Candidates and total token counts may differ slightly between runs due to whitespace tokens.
  36. let tokenCountAccuracy = 1
  37. let storage: Storage
  38. let userID1: String
  39. init() async throws {
  40. userID1 = try await TestHelpers.getUserID()
  41. storage = Storage.storage()
  42. }
  43. @Test(arguments: InstanceConfig.allConfigs)
  44. func generateContent(_ config: InstanceConfig) async throws {
  45. let model = VertexAI.componentInstance(config).generativeModel(
  46. modelName: ModelNames.gemini2FlashLite,
  47. generationConfig: generationConfig,
  48. safetySettings: safetySettings
  49. )
  50. let prompt = "Where is Google headquarters located? Answer with the city name only."
  51. let response = try await model.generateContent(prompt)
  52. let text = try #require(response.text).trimmingCharacters(in: .whitespacesAndNewlines)
  53. #expect(text == "Mountain View")
  54. let usageMetadata = try #require(response.usageMetadata)
  55. #expect(usageMetadata.promptTokenCount == 13)
  56. #expect(usageMetadata.candidatesTokenCount.isEqual(to: 3, accuracy: tokenCountAccuracy))
  57. #expect(usageMetadata.totalTokenCount.isEqual(to: 16, accuracy: tokenCountAccuracy))
  58. #expect(usageMetadata.promptTokensDetails.count == 1)
  59. let promptTokensDetails = try #require(usageMetadata.promptTokensDetails.first)
  60. #expect(promptTokensDetails.modality == .text)
  61. #expect(promptTokensDetails.tokenCount == usageMetadata.promptTokenCount)
  62. #expect(usageMetadata.candidatesTokensDetails.count == 1)
  63. let candidatesTokensDetails = try #require(usageMetadata.candidatesTokensDetails.first)
  64. #expect(candidatesTokensDetails.modality == .text)
  65. #expect(candidatesTokensDetails.tokenCount == usageMetadata.candidatesTokenCount)
  66. }
  67. @Test(
  68. "Generate an enum and provide a system instruction",
  69. /* System instructions are not supported on the v1 Developer API. */
  70. arguments: InstanceConfig.allConfigsExceptDeveloperV1
  71. )
  72. func generateContentEnum(_ config: InstanceConfig) async throws {
  73. let model = VertexAI.componentInstance(config).generativeModel(
  74. modelName: ModelNames.gemini2FlashLite,
  75. generationConfig: GenerationConfig(
  76. responseMIMEType: "text/x.enum", // Not supported on the v1 Developer API
  77. responseSchema: .enumeration(values: ["Red", "Green", "Blue"])
  78. ),
  79. safetySettings: safetySettings,
  80. tools: [], // Not supported on the v1 Developer API
  81. toolConfig: .init(functionCallingConfig: .none()), // Not supported on the v1 Developer API
  82. systemInstruction: ModelContent(role: "system", parts: "Always pick blue.")
  83. )
  84. let prompt = "What is your favourite colour?"
  85. let response = try await model.generateContent(prompt)
  86. let text = try #require(response.text).trimmingCharacters(in: .whitespacesAndNewlines)
  87. #expect(text == "Blue")
  88. let usageMetadata = try #require(response.usageMetadata)
  89. #expect(usageMetadata.promptTokenCount.isEqual(to: 15, accuracy: tokenCountAccuracy))
  90. #expect(usageMetadata.candidatesTokenCount.isEqual(to: 1, accuracy: tokenCountAccuracy))
  91. #expect(usageMetadata.totalTokenCount
  92. == usageMetadata.promptTokenCount + usageMetadata.candidatesTokenCount)
  93. #expect(usageMetadata.promptTokensDetails.count == 1)
  94. let promptTokensDetails = try #require(usageMetadata.promptTokensDetails.first)
  95. #expect(promptTokensDetails.modality == .text)
  96. #expect(promptTokensDetails.tokenCount == usageMetadata.promptTokenCount)
  97. #expect(usageMetadata.candidatesTokensDetails.count == 1)
  98. let candidatesTokensDetails = try #require(usageMetadata.candidatesTokensDetails.first)
  99. #expect(candidatesTokensDetails.modality == .text)
  100. #expect(candidatesTokensDetails.tokenCount == usageMetadata.candidatesTokenCount)
  101. }
  102. @Test(arguments: [
  103. InstanceConfig.vertexV1Beta,
  104. // TODO(andrewheard): Prod config temporarily disabled due to backend issue.
  105. // InstanceConfig.developerV1Beta,
  106. InstanceConfig.developerV1BetaStaging, // Remove after re-enabling `developerV1Beta` config.
  107. ])
  108. func generateImage(_ config: InstanceConfig) async throws {
  109. let generationConfig = GenerationConfig(
  110. temperature: 0.0,
  111. topP: 0.0,
  112. topK: 1,
  113. responseModalities: [.text, .image]
  114. )
  115. let model = VertexAI.componentInstance(config).generativeModel(
  116. modelName: ModelNames.gemini2FlashExperimental,
  117. generationConfig: generationConfig,
  118. safetySettings: safetySettings
  119. )
  120. let prompt = "Generate an image of a cute cartoon kitten playing with a ball of yarn."
  121. var response: GenerateContentResponse?
  122. try await withKnownIssue(
  123. "Backend may fail with a 503 - Service Unavailable error when overloaded",
  124. isIntermittent: true
  125. ) {
  126. response = try await model.generateContent(prompt)
  127. } matching: { issue in
  128. (issue.error as? BackendError).map { $0.httpResponseCode == 503 } ?? false
  129. }
  130. guard let response else { return }
  131. let candidate = try #require(response.candidates.first)
  132. let inlineDataPart = try #require(candidate.content.parts
  133. .first { $0 is InlineDataPart } as? InlineDataPart)
  134. #expect(inlineDataPart.mimeType == "image/png")
  135. #expect(inlineDataPart.data.count > 0)
  136. #if canImport(UIKit)
  137. let uiImage = try #require(UIImage(data: inlineDataPart.data))
  138. // Gemini 2.0 Flash Experimental returns images sized to fit within a 1024x1024 pixel box but
  139. // dimensions may vary depending on the aspect ratio.
  140. #expect(uiImage.size.width <= 1024)
  141. #expect(uiImage.size.width >= 500)
  142. #expect(uiImage.size.height <= 1024)
  143. #expect(uiImage.size.height >= 500)
  144. #endif // canImport(UIKit)
  145. }
  146. // MARK: Streaming Tests
  147. @Test(arguments: InstanceConfig.allConfigs)
  148. func generateContentStream(_ config: InstanceConfig) async throws {
  149. let expectedText = """
  150. 1. Mercury
  151. 2. Venus
  152. 3. Earth
  153. 4. Mars
  154. 5. Jupiter
  155. 6. Saturn
  156. 7. Uranus
  157. 8. Neptune
  158. """
  159. let prompt = """
  160. What are the names of the planets in the solar system, ordered from closest to furthest from
  161. the sun? Answer with a Markdown numbered list of the names and no other text.
  162. """
  163. let model = VertexAI.componentInstance(config).generativeModel(
  164. modelName: ModelNames.gemini2FlashLite,
  165. generationConfig: generationConfig,
  166. safetySettings: safetySettings
  167. )
  168. let chat = model.startChat()
  169. let stream = try chat.sendMessageStream(prompt)
  170. var textValues = [String]()
  171. for try await value in stream {
  172. try textValues.append(#require(value.text))
  173. }
  174. let userHistory = try #require(chat.history.first)
  175. #expect(userHistory.role == "user")
  176. #expect(userHistory.parts.count == 1)
  177. let promptTextPart = try #require(userHistory.parts.first as? TextPart)
  178. #expect(promptTextPart.text == prompt)
  179. let modelHistory = try #require(chat.history.last)
  180. #expect(modelHistory.role == "model")
  181. #expect(modelHistory.parts.count == 1)
  182. let modelTextPart = try #require(modelHistory.parts.first as? TextPart)
  183. let modelText = modelTextPart.text.trimmingCharacters(in: .whitespacesAndNewlines)
  184. #expect(modelText == expectedText)
  185. #expect(textValues.count > 1)
  186. let text = textValues.joined().trimmingCharacters(in: .whitespacesAndNewlines)
  187. #expect(text == expectedText)
  188. }
  189. // MARK: - App Check Tests
  190. @Test(arguments: [
  191. InstanceConfig.vertexV1AppCheckNotConfigured,
  192. InstanceConfig.vertexV1BetaAppCheckNotConfigured,
  193. // App Check is not supported on the Generative Language Developer API endpoint since it
  194. // bypasses the Vertex AI in Firebase proxy.
  195. ])
  196. func generateContent_appCheckNotConfigured_shouldFail(_ config: InstanceConfig) async throws {
  197. let model = VertexAI.componentInstance(config).generativeModel(
  198. modelName: ModelNames.gemini2Flash
  199. )
  200. let prompt = "Where is Google headquarters located? Answer with the city name only."
  201. try await #require {
  202. _ = try await model.generateContent(prompt)
  203. } throws: {
  204. guard let error = $0 as? GenerateContentError else {
  205. Issue.record("Expected a \(GenerateContentError.self); got \($0.self).")
  206. return false
  207. }
  208. guard case let .internalError(underlyingError) = error else {
  209. Issue.record("Expected a GenerateContentError.internalError(...); got \(error.self).")
  210. return false
  211. }
  212. return String(describing: underlyingError).contains("Firebase App Check token is invalid")
  213. }
  214. }
  215. }