GenerativeModelGoogleAITests.swift 30 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717
  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 FirebaseAppCheckInterop
  15. import FirebaseAuthInterop
  16. import FirebaseCore
  17. import XCTest
  18. @testable import FirebaseAILogic
  19. @available(iOS 15.0, macOS 12.0, macCatalyst 15.0, tvOS 15.0, watchOS 8.0, *)
  20. final class GenerativeModelGoogleAITests: XCTestCase {
  21. let testPrompt = "What sorts of questions can I ask you?"
  22. let safetyRatingsNegligible: [SafetyRating] = [
  23. .init(
  24. category: .sexuallyExplicit,
  25. probability: .negligible,
  26. probabilityScore: 0.0,
  27. severity: SafetyRating.HarmSeverity(rawValue: "HARM_SEVERITY_UNSPECIFIED"),
  28. severityScore: 0.0,
  29. blocked: false
  30. ),
  31. .init(
  32. category: .hateSpeech,
  33. probability: .negligible,
  34. probabilityScore: 0.0,
  35. severity: SafetyRating.HarmSeverity(rawValue: "HARM_SEVERITY_UNSPECIFIED"),
  36. severityScore: 0.0,
  37. blocked: false
  38. ),
  39. .init(
  40. category: .harassment,
  41. probability: .negligible,
  42. probabilityScore: 0.0,
  43. severity: SafetyRating.HarmSeverity(rawValue: "HARM_SEVERITY_UNSPECIFIED"),
  44. severityScore: 0.0,
  45. blocked: false
  46. ),
  47. .init(
  48. category: .dangerousContent,
  49. probability: .negligible,
  50. probabilityScore: 0.0,
  51. severity: SafetyRating.HarmSeverity(rawValue: "HARM_SEVERITY_UNSPECIFIED"),
  52. severityScore: 0.0,
  53. blocked: false
  54. ),
  55. ].sorted()
  56. let testModelName = "test-model"
  57. let testModelResourceName = "projects/test-project-id/models/test-model"
  58. let apiConfig = FirebaseAI.defaultVertexAIAPIConfig
  59. let googleAISubdirectory = "mock-responses/googleai"
  60. var urlSession: URLSession!
  61. var model: GenerativeModel!
  62. override func setUp() async throws {
  63. let configuration = URLSessionConfiguration.default
  64. configuration.protocolClasses = [MockURLProtocol.self]
  65. urlSession = try XCTUnwrap(URLSession(configuration: configuration))
  66. model = GenerativeModel(
  67. modelName: testModelName,
  68. modelResourceName: testModelResourceName,
  69. firebaseInfo: GenerativeModelTestUtil.testFirebaseInfo(),
  70. apiConfig: apiConfig,
  71. tools: nil,
  72. requestOptions: RequestOptions(),
  73. urlSession: urlSession
  74. )
  75. }
  76. override func tearDown() {
  77. MockURLProtocol.requestHandler = nil
  78. }
  79. // MARK: - Generate Content
  80. func testGenerateContent_success_basicReplyLong() async throws {
  81. MockURLProtocol.requestHandler = try GenerativeModelTestUtil.httpRequestHandler(
  82. forResource: "unary-success-basic-reply-long",
  83. withExtension: "json",
  84. subdirectory: googleAISubdirectory
  85. )
  86. let response = try await model.generateContent(testPrompt)
  87. XCTAssertEqual(response.candidates.count, 1)
  88. let candidate = try XCTUnwrap(response.candidates.first)
  89. let finishReason = try XCTUnwrap(candidate.finishReason)
  90. XCTAssertEqual(finishReason, .stop)
  91. XCTAssertEqual(candidate.safetyRatings.count, 4)
  92. XCTAssertEqual(candidate.content.parts.count, 1)
  93. let part = try XCTUnwrap(candidate.content.parts.first)
  94. let partText = try XCTUnwrap(part as? TextPart).text
  95. XCTAssertTrue(partText.hasPrefix("Making professional-quality"))
  96. XCTAssertEqual(response.text, partText)
  97. XCTAssertEqual(response.functionCalls, [])
  98. XCTAssertEqual(response.inlineDataParts, [])
  99. }
  100. func testGenerateContent_success_basicReplyShort() async throws {
  101. MockURLProtocol.requestHandler = try GenerativeModelTestUtil.httpRequestHandler(
  102. forResource: "unary-success-basic-reply-short",
  103. withExtension: "json",
  104. subdirectory: googleAISubdirectory
  105. )
  106. let response = try await model.generateContent(testPrompt)
  107. XCTAssertEqual(response.candidates.count, 1)
  108. let candidate = try XCTUnwrap(response.candidates.first)
  109. let finishReason = try XCTUnwrap(candidate.finishReason)
  110. XCTAssertEqual(finishReason, .stop)
  111. XCTAssertEqual(candidate.safetyRatings.sorted(), safetyRatingsNegligible)
  112. XCTAssertEqual(candidate.content.parts.count, 1)
  113. let part = try XCTUnwrap(candidate.content.parts.first)
  114. let textPart = try XCTUnwrap(part as? TextPart)
  115. XCTAssertTrue(textPart.text.hasPrefix("Google's headquarters"))
  116. XCTAssertEqual(response.text, textPart.text)
  117. XCTAssertEqual(response.functionCalls, [])
  118. XCTAssertEqual(response.inlineDataParts, [])
  119. }
  120. func testGenerateContent_success_citations() async throws {
  121. MockURLProtocol.requestHandler = try GenerativeModelTestUtil.httpRequestHandler(
  122. forResource: "unary-success-citations",
  123. withExtension: "json",
  124. subdirectory: googleAISubdirectory
  125. )
  126. let response = try await model.generateContent(testPrompt)
  127. XCTAssertEqual(response.candidates.count, 1)
  128. let candidate = try XCTUnwrap(response.candidates.first)
  129. XCTAssertEqual(candidate.content.parts.count, 1)
  130. let text = try XCTUnwrap(response.text)
  131. XCTAssertTrue(text.hasPrefix("Okay, let's break down quantum mechanics."))
  132. let citationMetadata = try XCTUnwrap(candidate.citationMetadata)
  133. XCTAssertEqual(citationMetadata.citations.count, 4)
  134. let citationSource1 = try XCTUnwrap(citationMetadata.citations[0])
  135. XCTAssertEqual(citationSource1.uri, "https://www.example.com/some-citation-1")
  136. XCTAssertEqual(citationSource1.startIndex, 548)
  137. XCTAssertEqual(citationSource1.endIndex, 690)
  138. XCTAssertNil(citationSource1.title)
  139. XCTAssertEqual(citationSource1.license, "mit")
  140. XCTAssertNil(citationSource1.publicationDate)
  141. let citationSource2 = try XCTUnwrap(citationMetadata.citations[1])
  142. XCTAssertEqual(citationSource2.uri, "https://www.example.com/some-citation-1")
  143. XCTAssertEqual(citationSource2.startIndex, 1240)
  144. XCTAssertEqual(citationSource2.endIndex, 1407)
  145. XCTAssertNil(citationSource2.title, "some-citation-2")
  146. XCTAssertNil(citationSource2.license)
  147. XCTAssertNil(citationSource2.publicationDate)
  148. let citationSource3 = try XCTUnwrap(citationMetadata.citations[2])
  149. XCTAssertEqual(citationSource3.startIndex, 1942)
  150. XCTAssertEqual(citationSource3.endIndex, 2149)
  151. XCTAssertNil(citationSource3.uri)
  152. XCTAssertNil(citationSource3.license)
  153. XCTAssertNil(citationSource3.title)
  154. XCTAssertNil(citationSource3.publicationDate)
  155. let citationSource4 = try XCTUnwrap(citationMetadata.citations[3])
  156. XCTAssertEqual(citationSource4.startIndex, 2036)
  157. XCTAssertEqual(citationSource4.endIndex, 2175)
  158. XCTAssertNil(citationSource4.uri)
  159. XCTAssertNil(citationSource4.license)
  160. XCTAssertNil(citationSource4.title)
  161. XCTAssertNil(citationSource4.publicationDate)
  162. }
  163. func testGenerateContent_usageMetadata() async throws {
  164. MockURLProtocol.requestHandler = try GenerativeModelTestUtil.httpRequestHandler(
  165. forResource: "unary-success-basic-reply-short",
  166. withExtension: "json",
  167. subdirectory: googleAISubdirectory
  168. )
  169. let response = try await model.generateContent(testPrompt)
  170. let usageMetadata = try XCTUnwrap(response.usageMetadata)
  171. XCTAssertEqual(usageMetadata.promptTokenCount, 7)
  172. XCTAssertEqual(usageMetadata.promptTokensDetails.count, 1)
  173. XCTAssertEqual(usageMetadata.promptTokensDetails[0].modality, .text)
  174. XCTAssertEqual(usageMetadata.promptTokensDetails[0].tokenCount, 7)
  175. XCTAssertEqual(usageMetadata.candidatesTokenCount, 22)
  176. XCTAssertEqual(usageMetadata.candidatesTokensDetails.count, 1)
  177. XCTAssertEqual(usageMetadata.candidatesTokensDetails[0].modality, .text)
  178. XCTAssertEqual(usageMetadata.candidatesTokensDetails[0].tokenCount, 22)
  179. }
  180. func testGenerateContent_groundingMetadata() async throws {
  181. MockURLProtocol.requestHandler = try GenerativeModelTestUtil.httpRequestHandler(
  182. forResource: "unary-success-google-search-grounding",
  183. withExtension: "json",
  184. subdirectory: googleAISubdirectory
  185. )
  186. let response = try await model.generateContent(testPrompt)
  187. XCTAssertEqual(response.candidates.count, 1)
  188. let candidate = try XCTUnwrap(response.candidates.first)
  189. let groundingMetadata = try XCTUnwrap(candidate.groundingMetadata)
  190. XCTAssertEqual(groundingMetadata.webSearchQueries, ["current weather in London"])
  191. let searchEntryPoint = try XCTUnwrap(groundingMetadata.searchEntryPoint)
  192. XCTAssertFalse(searchEntryPoint.renderedContent.isEmpty)
  193. XCTAssertEqual(groundingMetadata.groundingChunks.count, 2)
  194. let firstChunk = try XCTUnwrap(groundingMetadata.groundingChunks.first?.web)
  195. XCTAssertEqual(firstChunk.title, "accuweather.com")
  196. XCTAssertNotNil(firstChunk.uri)
  197. XCTAssertNil(firstChunk.domain) // Domain is not supported by Google AI backend
  198. XCTAssertEqual(groundingMetadata.groundingSupports.count, 3)
  199. let firstSupport = try XCTUnwrap(groundingMetadata.groundingSupports.first)
  200. let segment = try XCTUnwrap(firstSupport.segment)
  201. XCTAssertEqual(segment.text, "The current weather in London, United Kingdom is cloudy.")
  202. XCTAssertEqual(segment.startIndex, 0)
  203. XCTAssertEqual(segment.partIndex, 0)
  204. XCTAssertEqual(segment.endIndex, 56)
  205. XCTAssertEqual(firstSupport.groundingChunkIndices, [0])
  206. }
  207. // This test case can be deleted once https://b.corp.google.com/issues/422779395 (internal) is
  208. // fixed.
  209. func testGenerateContent_groundingMetadata_emptyGroundingChunks() async throws {
  210. MockURLProtocol.requestHandler = try GenerativeModelTestUtil.httpRequestHandler(
  211. forResource: "unary-success-google-search-grounding-empty-grounding-chunks",
  212. withExtension: "json",
  213. subdirectory: googleAISubdirectory
  214. )
  215. let response = try await model.generateContent(testPrompt)
  216. XCTAssertEqual(response.candidates.count, 1)
  217. let candidate = try XCTUnwrap(response.candidates.first)
  218. let groundingMetadata = try XCTUnwrap(candidate.groundingMetadata)
  219. XCTAssertNotNil(groundingMetadata.searchEntryPoint)
  220. XCTAssertEqual(groundingMetadata.webSearchQueries, ["current weather London"])
  221. // Chunks exist, but contain no web information.
  222. XCTAssertEqual(groundingMetadata.groundingChunks.count, 2)
  223. XCTAssertNil(groundingMetadata.groundingChunks[0].web)
  224. XCTAssertNil(groundingMetadata.groundingChunks[1].web)
  225. XCTAssertEqual(groundingMetadata.groundingSupports.count, 1)
  226. let support = try XCTUnwrap(groundingMetadata.groundingSupports.first)
  227. XCTAssertEqual(support.groundingChunkIndices, [0])
  228. XCTAssertEqual(
  229. support.segment.text,
  230. "There is a 0% chance of rain and the humidity is around 41%."
  231. )
  232. }
  233. func testGenerateContent_success_thinking_thoughtSummary() async throws {
  234. MockURLProtocol.requestHandler = try GenerativeModelTestUtil.httpRequestHandler(
  235. forResource: "unary-success-thinking-reply-thought-summary",
  236. withExtension: "json",
  237. subdirectory: googleAISubdirectory
  238. )
  239. let response = try await model.generateContent(testPrompt)
  240. XCTAssertEqual(response.candidates.count, 1)
  241. let candidate = try XCTUnwrap(response.candidates.first)
  242. XCTAssertEqual(candidate.content.parts.count, 2)
  243. let thoughtPart = try XCTUnwrap(candidate.content.parts.first as? TextPart)
  244. XCTAssertTrue(thoughtPart.isThought)
  245. XCTAssertTrue(thoughtPart.text.hasPrefix("**Thinking About Google's Headquarters**"))
  246. XCTAssertEqual(thoughtPart.text, response.thoughtSummary)
  247. let textPart = try XCTUnwrap(candidate.content.parts.last as? TextPart)
  248. XCTAssertFalse(textPart.isThought)
  249. XCTAssertEqual(textPart.text, "Mountain View")
  250. XCTAssertEqual(textPart.text, response.text)
  251. }
  252. func testGenerateContent_success_thinking_functionCall_thoughtSummaryAndSignature() async throws {
  253. MockURLProtocol.requestHandler = try GenerativeModelTestUtil.httpRequestHandler(
  254. forResource: "unary-success-thinking-function-call-thought-summary-signature",
  255. withExtension: "json",
  256. subdirectory: googleAISubdirectory
  257. )
  258. let response = try await model.generateContent(testPrompt)
  259. XCTAssertEqual(response.candidates.count, 1)
  260. let candidate = try XCTUnwrap(response.candidates.first)
  261. XCTAssertEqual(candidate.finishReason, .stop)
  262. XCTAssertEqual(candidate.content.parts.count, 2)
  263. let thoughtPart = try XCTUnwrap(candidate.content.parts.first as? TextPart)
  264. XCTAssertTrue(thoughtPart.isThought)
  265. XCTAssertTrue(thoughtPart.text.hasPrefix("**Thinking Through the New Year's Eve Calculation**"))
  266. let functionCallPart = try XCTUnwrap(candidate.content.parts.last as? FunctionCallPart)
  267. XCTAssertFalse(functionCallPart.isThought)
  268. XCTAssertEqual(functionCallPart.name, "now")
  269. XCTAssertTrue(functionCallPart.args.isEmpty)
  270. let thoughtSignature = try XCTUnwrap(functionCallPart.thoughtSignature)
  271. XCTAssertTrue(thoughtSignature.hasPrefix("CtQOAVSoXO74PmYr9AFu"))
  272. }
  273. func testGenerateContent_success_codeExecution() async throws {
  274. MockURLProtocol.requestHandler = try GenerativeModelTestUtil.httpRequestHandler(
  275. forResource: "unary-success-code-execution",
  276. withExtension: "json",
  277. subdirectory: googleAISubdirectory
  278. )
  279. let response = try await model.generateContent(testPrompt)
  280. XCTAssertEqual(response.candidates.count, 1)
  281. let candidate = try XCTUnwrap(response.candidates.first)
  282. let parts = candidate.content.parts
  283. XCTAssertEqual(candidate.finishReason, .stop)
  284. XCTAssertEqual(parts.count, 3)
  285. let executableCodePart = try XCTUnwrap(parts[0] as? ExecutableCodePart)
  286. XCTAssertFalse(executableCodePart.isThought)
  287. XCTAssertEqual(executableCodePart.language, .python)
  288. XCTAssertTrue(executableCodePart.code.starts(with: "prime_numbers = [2, 3, 5, 7, 11]"))
  289. let codeExecutionResultPart = try XCTUnwrap(parts[1] as? CodeExecutionResultPart)
  290. XCTAssertFalse(codeExecutionResultPart.isThought)
  291. XCTAssertEqual(codeExecutionResultPart.outcome, .ok)
  292. XCTAssertEqual(codeExecutionResultPart.output, "sum_of_primes=28\n")
  293. let textPart = try XCTUnwrap(parts[2] as? TextPart)
  294. XCTAssertFalse(textPart.isThought)
  295. XCTAssertTrue(textPart.text.hasPrefix("The first 5 prime numbers are 2, 3, 5, 7, and 11."))
  296. let usageMetadata = try XCTUnwrap(response.usageMetadata)
  297. XCTAssertEqual(usageMetadata.toolUsePromptTokenCount, 160)
  298. }
  299. func testGenerateContent_success_urlContext() async throws {
  300. MockURLProtocol.requestHandler = try GenerativeModelTestUtil.httpRequestHandler(
  301. forResource: "unary-success-url-context",
  302. withExtension: "json",
  303. subdirectory: googleAISubdirectory
  304. )
  305. let response = try await model.generateContent(testPrompt)
  306. XCTAssertEqual(response.candidates.count, 1)
  307. let candidate = try XCTUnwrap(response.candidates.first)
  308. let urlContextMetadata = try XCTUnwrap(candidate.urlContextMetadata)
  309. XCTAssertEqual(urlContextMetadata.urlMetadata.count, 1)
  310. let urlMetadata = try XCTUnwrap(urlContextMetadata.urlMetadata.first)
  311. let retrievedURL = try XCTUnwrap(urlMetadata.retrievedURL)
  312. XCTAssertEqual(
  313. retrievedURL,
  314. URL(string: "https://berkshirehathaway.com")
  315. )
  316. XCTAssertEqual(urlMetadata.retrievalStatus, .success)
  317. let usageMetadata = try XCTUnwrap(response.usageMetadata)
  318. XCTAssertEqual(usageMetadata.toolUsePromptTokenCount, 424)
  319. }
  320. func testGenerateContent_success_urlContext_mixedValidity() async throws {
  321. MockURLProtocol.requestHandler = try GenerativeModelTestUtil.httpRequestHandler(
  322. forResource: "unary-success-url-context-mixed-validity",
  323. withExtension: "json",
  324. subdirectory: googleAISubdirectory
  325. )
  326. let response = try await model.generateContent(testPrompt)
  327. let candidate = try XCTUnwrap(response.candidates.first)
  328. let urlContextMetadata = try XCTUnwrap(candidate.urlContextMetadata)
  329. XCTAssertEqual(urlContextMetadata.urlMetadata.count, 3)
  330. let paywallURLMetadata = urlContextMetadata.urlMetadata[0]
  331. XCTAssertEqual(paywallURLMetadata.retrievalStatus, .error)
  332. let successURLMetadata = urlContextMetadata.urlMetadata[1]
  333. XCTAssertEqual(successURLMetadata.retrievalStatus, .success)
  334. let errorURLMetadata = urlContextMetadata.urlMetadata[2]
  335. XCTAssertEqual(errorURLMetadata.retrievalStatus, .error)
  336. }
  337. func testGenerateContent_failure_invalidAPIKey() async throws {
  338. let expectedStatusCode = 400
  339. MockURLProtocol.requestHandler = try GenerativeModelTestUtil.httpRequestHandler(
  340. forResource: "unary-failure-api-key",
  341. withExtension: "json",
  342. subdirectory: googleAISubdirectory,
  343. statusCode: expectedStatusCode
  344. )
  345. do {
  346. _ = try await model.generateContent(testPrompt)
  347. XCTFail("Should throw GenerateContentError.internalError; no error thrown.")
  348. } catch let GenerateContentError.internalError(error as BackendError) {
  349. XCTAssertEqual(error.httpResponseCode, 400)
  350. XCTAssertEqual(error.status, .invalidArgument)
  351. XCTAssertEqual(error.message, "API key not valid. Please pass a valid API key.")
  352. XCTAssertTrue(error.localizedDescription.contains(error.message))
  353. XCTAssertTrue(error.localizedDescription.contains(error.status.rawValue))
  354. XCTAssertTrue(error.localizedDescription.contains("\(error.httpResponseCode)"))
  355. let nsError = error as NSError
  356. XCTAssertEqual(nsError.domain, "\(Constants.baseErrorDomain).\(BackendError.self)")
  357. XCTAssertEqual(nsError.code, error.httpResponseCode)
  358. return
  359. } catch {
  360. XCTFail("Should throw GenerateContentError.internalError(RPCError); error thrown: \(error)")
  361. }
  362. }
  363. func testGenerateContent_failure_finishReasonSafety() async throws {
  364. MockURLProtocol.requestHandler = try GenerativeModelTestUtil.httpRequestHandler(
  365. forResource: "unary-failure-finish-reason-safety",
  366. withExtension: "json",
  367. subdirectory: googleAISubdirectory
  368. )
  369. do {
  370. _ = try await model.generateContent(testPrompt)
  371. XCTFail("Should throw")
  372. } catch let GenerateContentError.responseStoppedEarly(reason, response) {
  373. XCTAssertEqual(reason, .safety)
  374. XCTAssertEqual(response.text, "Safety error incoming in 5, 4, 3, 2...")
  375. } catch {
  376. XCTFail("Should throw a responseStoppedEarly")
  377. }
  378. }
  379. func testGenerateContent_failure_unknownModel() async throws {
  380. let expectedStatusCode = 404
  381. MockURLProtocol.requestHandler = try GenerativeModelTestUtil.httpRequestHandler(
  382. forResource: "unary-failure-unknown-model",
  383. withExtension: "json",
  384. subdirectory: googleAISubdirectory,
  385. statusCode: 404
  386. )
  387. do {
  388. _ = try await model.generateContent(testPrompt)
  389. XCTFail("Should throw GenerateContentError.internalError; no error thrown.")
  390. } catch let GenerateContentError.internalError(underlying: rpcError as BackendError) {
  391. XCTAssertEqual(rpcError.status, .notFound)
  392. XCTAssertEqual(rpcError.httpResponseCode, expectedStatusCode)
  393. XCTAssertTrue(rpcError.message.hasPrefix("models/gemini-5.0-flash is not found"))
  394. } catch {
  395. XCTFail("Should throw GenerateContentError.internalError; error thrown: \(error)")
  396. }
  397. }
  398. // MARK: - Generate Content (Streaming)
  399. func testGenerateContentStream_successBasicReplyLong() async throws {
  400. MockURLProtocol.requestHandler = try GenerativeModelTestUtil.httpRequestHandler(
  401. forResource: "streaming-success-basic-reply-long",
  402. withExtension: "txt",
  403. subdirectory: googleAISubdirectory
  404. )
  405. var responses = 0
  406. let stream = try model.generateContentStream("Hi")
  407. for try await content in stream {
  408. XCTAssertNotNil(content.text)
  409. responses += 1
  410. }
  411. XCTAssertEqual(responses, 36)
  412. }
  413. func testGenerateContentStream_successBasicReplyShort() async throws {
  414. MockURLProtocol.requestHandler = try GenerativeModelTestUtil.httpRequestHandler(
  415. forResource: "streaming-success-basic-reply-short",
  416. withExtension: "txt",
  417. subdirectory: googleAISubdirectory
  418. )
  419. var responses = 0
  420. let stream = try model.generateContentStream("Hi")
  421. for try await content in stream {
  422. XCTAssertNotNil(content.text)
  423. responses += 1
  424. }
  425. XCTAssertEqual(responses, 3)
  426. }
  427. func testGenerateContentStream_successWithCitations() async throws {
  428. MockURLProtocol.requestHandler = try GenerativeModelTestUtil.httpRequestHandler(
  429. forResource: "streaming-success-citations",
  430. withExtension: "txt",
  431. subdirectory: googleAISubdirectory
  432. )
  433. let stream = try model.generateContentStream("Hi")
  434. var citations = [Citation]()
  435. var responses = [GenerateContentResponse]()
  436. for try await content in stream {
  437. responses.append(content)
  438. XCTAssertNotNil(content.text)
  439. let candidate = try XCTUnwrap(content.candidates.first)
  440. if let sources = candidate.citationMetadata?.citations {
  441. citations.append(contentsOf: sources)
  442. }
  443. }
  444. let lastCandidate = try XCTUnwrap(responses.last?.candidates.first)
  445. XCTAssertEqual(lastCandidate.finishReason, .stop)
  446. XCTAssertEqual(citations.count, 1)
  447. let citation = try XCTUnwrap(citations.first)
  448. XCTAssertEqual(citation.startIndex, 111)
  449. XCTAssertEqual(citation.endIndex, 236)
  450. let citationURI = try XCTUnwrap(citation.uri)
  451. XCTAssertTrue(citationURI.starts(with: "https://www."))
  452. XCTAssertNil(citation.license)
  453. XCTAssertNil(citation.title)
  454. XCTAssertNil(citation.publicationDate)
  455. }
  456. func testGenerateContentStream_successWithThoughtSummary() async throws {
  457. MockURLProtocol.requestHandler = try GenerativeModelTestUtil.httpRequestHandler(
  458. forResource: "streaming-success-thinking-reply-thought-summary",
  459. withExtension: "txt",
  460. subdirectory: googleAISubdirectory
  461. )
  462. var thoughtSummary = ""
  463. var text = ""
  464. let stream = try model.generateContentStream("Hi")
  465. for try await response in stream {
  466. let candidate = try XCTUnwrap(response.candidates.first)
  467. XCTAssertEqual(candidate.content.parts.count, 1)
  468. let textPart = try XCTUnwrap(candidate.content.parts.first as? TextPart)
  469. if textPart.isThought {
  470. let newThought = try XCTUnwrap(response.thoughtSummary)
  471. XCTAssertEqual(textPart.text, newThought)
  472. thoughtSummary.append(newThought)
  473. } else {
  474. let newText = try XCTUnwrap(response.text)
  475. XCTAssertEqual(textPart.text, newText)
  476. text.append(newText)
  477. }
  478. }
  479. XCTAssertTrue(thoughtSummary.hasPrefix("**Exploring Sky Color**"))
  480. XCTAssertTrue(text.hasPrefix("The sky is blue because"))
  481. }
  482. func testGenerateContentStream_success_thinking_functionCall_thoughtSummary_signature() async throws {
  483. MockURLProtocol.requestHandler = try GenerativeModelTestUtil.httpRequestHandler(
  484. forResource: "streaming-success-thinking-function-call-thought-summary-signature",
  485. withExtension: "txt",
  486. subdirectory: googleAISubdirectory
  487. )
  488. var thoughtSummary = ""
  489. var functionCalls: [FunctionCallPart] = []
  490. let stream = try model.generateContentStream("Hi")
  491. for try await response in stream {
  492. let candidate = try XCTUnwrap(response.candidates.first)
  493. XCTAssertEqual(candidate.content.parts.count, 1)
  494. let part = try XCTUnwrap(candidate.content.parts.first)
  495. if part.isThought {
  496. let textPart = try XCTUnwrap(part as? TextPart)
  497. let newThought = try XCTUnwrap(response.thoughtSummary)
  498. XCTAssertEqual(textPart.text, newThought)
  499. thoughtSummary.append(newThought)
  500. } else {
  501. let functionCallPart = try XCTUnwrap(part as? FunctionCallPart)
  502. XCTAssertEqual(response.functionCalls.count, 1)
  503. let newFunctionCall = try XCTUnwrap(response.functionCalls.first)
  504. XCTAssertEqual(functionCallPart, newFunctionCall)
  505. functionCalls.append(newFunctionCall)
  506. }
  507. }
  508. XCTAssertTrue(thoughtSummary.hasPrefix("**Calculating the Days**"))
  509. XCTAssertEqual(functionCalls.count, 1)
  510. let functionCall = try XCTUnwrap(functionCalls.first)
  511. XCTAssertEqual(functionCall.name, "now")
  512. XCTAssertTrue(functionCall.args.isEmpty)
  513. let thoughtSignature = try XCTUnwrap(functionCall.thoughtSignature)
  514. XCTAssertTrue(thoughtSignature.hasPrefix("CiIBVKhc7vB+vaaq6rA"))
  515. }
  516. func testGenerateContentStream_success_ignoresEmptyParts() async throws {
  517. MockURLProtocol.requestHandler = try GenerativeModelTestUtil.httpRequestHandler(
  518. forResource: "streaming-success-empty-parts",
  519. withExtension: "txt",
  520. subdirectory: googleAISubdirectory
  521. )
  522. let stream = try model.generateContentStream("Hi")
  523. for try await response in stream {
  524. let candidate = try XCTUnwrap(response.candidates.first)
  525. XCTAssertGreaterThan(candidate.content.parts.count, 0)
  526. let text = response.text
  527. let inlineData = response.inlineDataParts.first
  528. XCTAssertTrue(text != nil || inlineData != nil, "Response did not contain text or data")
  529. }
  530. }
  531. func testGenerateContentStream_success_codeExecution() async throws {
  532. MockURLProtocol.requestHandler = try GenerativeModelTestUtil.httpRequestHandler(
  533. forResource: "streaming-success-code-execution",
  534. withExtension: "txt",
  535. subdirectory: googleAISubdirectory
  536. )
  537. var parts = [any Part]()
  538. let stream = try model.generateContentStream(testPrompt)
  539. for try await response in stream {
  540. if let responseParts = response.candidates.first?.content.parts {
  541. parts.append(contentsOf: responseParts)
  542. }
  543. }
  544. let thoughtParts = parts.filter { $0.isThought }
  545. XCTAssertEqual(thoughtParts.count, 0)
  546. let textParts = parts.filter { $0 is TextPart }
  547. XCTAssertGreaterThan(textParts.count, 0)
  548. let executableCodeParts = parts.compactMap { $0 as? ExecutableCodePart }
  549. XCTAssertEqual(executableCodeParts.count, 1)
  550. let executableCodePart = try XCTUnwrap(executableCodeParts.first)
  551. XCTAssertFalse(executableCodePart.isThought)
  552. XCTAssertEqual(executableCodePart.language, .python)
  553. XCTAssertTrue(executableCodePart.code.starts(with: "prime_numbers = [2, 3, 5, 7, 11]"))
  554. let codeExecutionResultParts = parts.compactMap { $0 as? CodeExecutionResultPart }
  555. XCTAssertEqual(codeExecutionResultParts.count, 1)
  556. let codeExecutionResultPart = try XCTUnwrap(codeExecutionResultParts.first)
  557. XCTAssertFalse(codeExecutionResultPart.isThought)
  558. XCTAssertEqual(codeExecutionResultPart.outcome, .ok)
  559. XCTAssertEqual(codeExecutionResultPart.output, "The sum of the first 5 prime numbers is: 28\n")
  560. }
  561. func testGenerateContentStream_failureInvalidAPIKey() async throws {
  562. MockURLProtocol.requestHandler = try GenerativeModelTestUtil.httpRequestHandler(
  563. forResource: "unary-failure-api-key",
  564. withExtension: "json",
  565. subdirectory: googleAISubdirectory
  566. )
  567. do {
  568. let stream = try model.generateContentStream("Hi")
  569. for try await _ in stream {
  570. XCTFail("No content is there, this shouldn't happen.")
  571. }
  572. } catch let GenerateContentError.internalError(error as BackendError) {
  573. XCTAssertEqual(error.httpResponseCode, 400)
  574. XCTAssertEqual(error.status, .invalidArgument)
  575. XCTAssertEqual(error.message, "API key not valid. Please pass a valid API key.")
  576. XCTAssertTrue(error.localizedDescription.contains(error.message))
  577. XCTAssertTrue(error.localizedDescription.contains(error.status.rawValue))
  578. XCTAssertTrue(error.localizedDescription.contains("\(error.httpResponseCode)"))
  579. let nsError = error as NSError
  580. XCTAssertEqual(nsError.domain, "\(Constants.baseErrorDomain).\(BackendError.self)")
  581. XCTAssertEqual(nsError.code, error.httpResponseCode)
  582. return
  583. }
  584. XCTFail("Should have caught an error.")
  585. }
  586. func testGenerateContentStream_failureFinishRecitation() async throws {
  587. MockURLProtocol.requestHandler = try GenerativeModelTestUtil.httpRequestHandler(
  588. forResource: "streaming-failure-recitation-no-content",
  589. withExtension: "txt",
  590. subdirectory: googleAISubdirectory
  591. )
  592. var responses = [GenerateContentResponse]()
  593. do {
  594. let stream = try model.generateContentStream("Hi")
  595. for try await response in stream {
  596. responses.append(response)
  597. }
  598. XCTFail("Expected a GenerateContentError.responseStoppedEarly error, but got no error.")
  599. } catch let GenerateContentError.responseStoppedEarly(reason, response) {
  600. XCTAssertEqual(reason, .recitation)
  601. let candidate = try XCTUnwrap(response.candidates.first)
  602. XCTAssertEqual(candidate.finishReason, reason)
  603. } catch {
  604. XCTFail("Expected a GenerateContentError.responseStoppedEarly error, but got error: \(error)")
  605. }
  606. XCTAssertEqual(responses.count, 8)
  607. let firstResponse = try XCTUnwrap(responses.first)
  608. XCTAssertEqual(firstResponse.text, "text1")
  609. let lastResponse = try XCTUnwrap(responses.last)
  610. XCTAssertEqual(lastResponse.text, "text8")
  611. }
  612. func testGenerateContentStream_success_urlContext() async throws {
  613. MockURLProtocol.requestHandler = try GenerativeModelTestUtil.httpRequestHandler(
  614. forResource: "streaming-success-url-context",
  615. withExtension: "txt",
  616. subdirectory: googleAISubdirectory
  617. )
  618. var responses = [GenerateContentResponse]()
  619. let stream = try model.generateContentStream(testPrompt)
  620. for try await response in stream {
  621. responses.append(response)
  622. }
  623. let firstResponse = try XCTUnwrap(responses.first)
  624. let candidate = try XCTUnwrap(firstResponse.candidates.first)
  625. let urlContextMetadata = try XCTUnwrap(candidate.urlContextMetadata)
  626. XCTAssertEqual(urlContextMetadata.urlMetadata.count, 1)
  627. let urlMetadata = try XCTUnwrap(urlContextMetadata.urlMetadata.first)
  628. let retrievedURL = try XCTUnwrap(urlMetadata.retrievedURL)
  629. XCTAssertEqual(retrievedURL, URL(string: "https://google.com"))
  630. XCTAssertEqual(urlMetadata.retrievalStatus, .success)
  631. }
  632. }