GenerateContentResponse.swift 27 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712
  1. // Copyright 2023 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 Foundation
  15. /// The model's response to a generate content request.
  16. @available(iOS 15.0, macOS 12.0, macCatalyst 15.0, tvOS 15.0, watchOS 8.0, *)
  17. public struct GenerateContentResponse: Sendable {
  18. /// Token usage metadata for processing the generate content request.
  19. @available(iOS 15.0, macOS 12.0, macCatalyst 15.0, tvOS 15.0, watchOS 8.0, *)
  20. public struct UsageMetadata: Sendable {
  21. /// The number of tokens in the request prompt.
  22. public let promptTokenCount: Int
  23. /// The total number of tokens across the generated response candidates.
  24. public let candidatesTokenCount: Int
  25. /// The number of tokens used by the model's internal "thinking" process.
  26. ///
  27. /// For models that support thinking (like Gemini 2.5 Pro and Flash), this represents the actual
  28. /// number of tokens consumed for reasoning before the model generated a response. For models
  29. /// that do not support thinking, this value will be `0`.
  30. ///
  31. /// When thinking is used, this count will be less than or equal to the `thinkingBudget` set in
  32. /// the ``ThinkingConfig``.
  33. public let thoughtsTokenCount: Int
  34. /// The total number of tokens in both the request and response.
  35. public let totalTokenCount: Int
  36. /// The breakdown, by modality, of how many tokens are consumed by the prompt
  37. public let promptTokensDetails: [ModalityTokenCount]
  38. /// The breakdown, by modality, of how many tokens are consumed by the candidates
  39. public let candidatesTokensDetails: [ModalityTokenCount]
  40. }
  41. /// A list of candidate response content, ordered from best to worst.
  42. public let candidates: [Candidate]
  43. /// A value containing the safety ratings for the response, or, if the request was blocked, a
  44. /// reason for blocking the request.
  45. public let promptFeedback: PromptFeedback?
  46. /// Token usage metadata for processing the generate content request.
  47. public let usageMetadata: UsageMetadata?
  48. /// The response's content as text, if it exists.
  49. ///
  50. /// - Note: This does not include thought summaries; see ``thoughtSummary`` for more details.
  51. public var text: String? {
  52. return text(isThought: false)
  53. }
  54. /// A summary of the model's thinking process, if available.
  55. ///
  56. /// - Important: Thought summaries are only available when `includeThoughts` is enabled in the
  57. /// ``ThinkingConfig``. For more information, see the
  58. /// [Thinking](https://firebase.google.com/docs/ai-logic/thinking) documentation.
  59. public var thoughtSummary: String? {
  60. return text(isThought: true)
  61. }
  62. /// Returns function calls found in any `Part`s of the first candidate of the response, if any.
  63. public var functionCalls: [FunctionCallPart] {
  64. guard let candidate = candidates.first else {
  65. return []
  66. }
  67. return candidate.content.parts.compactMap { part in
  68. guard let functionCallPart = part as? FunctionCallPart, !part.isThought else {
  69. return nil
  70. }
  71. return functionCallPart
  72. }
  73. }
  74. /// Returns inline data parts found in any `Part`s of the first candidate of the response, if any.
  75. public var inlineDataParts: [InlineDataPart] {
  76. guard let candidate = candidates.first else {
  77. AILog.error(code: .generateContentResponseNoCandidates, """
  78. Could not get inline data parts because the response has no candidates. The accessor only \
  79. checks the first candidate.
  80. """)
  81. return []
  82. }
  83. return candidate.content.parts.compactMap { part in
  84. guard let inlineDataPart = part as? InlineDataPart, !part.isThought else {
  85. return nil
  86. }
  87. return inlineDataPart
  88. }
  89. }
  90. /// Initializer for SwiftUI previews or tests.
  91. public init(candidates: [Candidate], promptFeedback: PromptFeedback? = nil,
  92. usageMetadata: UsageMetadata? = nil) {
  93. self.candidates = candidates
  94. self.promptFeedback = promptFeedback
  95. self.usageMetadata = usageMetadata
  96. }
  97. func text(isThought: Bool) -> String? {
  98. guard let candidate = candidates.first else {
  99. AILog.error(
  100. code: .generateContentResponseNoCandidates,
  101. "Could not get text from a response that had no candidates."
  102. )
  103. return nil
  104. }
  105. let textValues: [String] = candidate.content.parts.compactMap { part in
  106. guard let textPart = part as? TextPart, part.isThought == isThought else {
  107. return nil
  108. }
  109. return textPart.text
  110. }
  111. guard textValues.count > 0 else {
  112. AILog.error(
  113. code: .generateContentResponseNoText,
  114. "Could not get a text part from the first candidate."
  115. )
  116. return nil
  117. }
  118. return textValues.joined(separator: " ")
  119. }
  120. }
  121. /// A struct representing a possible reply to a content generation prompt. Each content generation
  122. /// prompt may produce multiple candidate responses.
  123. @available(iOS 15.0, macOS 12.0, macCatalyst 15.0, tvOS 15.0, watchOS 8.0, *)
  124. public struct Candidate: Sendable {
  125. /// The response's content.
  126. public let content: ModelContent
  127. /// The safety rating of the response content.
  128. public let safetyRatings: [SafetyRating]
  129. /// The reason the model stopped generating content, if it exists; for example, if the model
  130. /// generated a predefined stop sequence.
  131. public let finishReason: FinishReason?
  132. /// Cited works in the model's response content, if it exists.
  133. public let citationMetadata: CitationMetadata?
  134. public let groundingMetadata: GroundingMetadata?
  135. /// Initializer for SwiftUI previews or tests.
  136. public init(content: ModelContent, safetyRatings: [SafetyRating], finishReason: FinishReason?,
  137. citationMetadata: CitationMetadata?, groundingMetadata: GroundingMetadata? = nil) {
  138. self.content = content
  139. self.safetyRatings = safetyRatings
  140. self.finishReason = finishReason
  141. self.citationMetadata = citationMetadata
  142. self.groundingMetadata = groundingMetadata
  143. }
  144. // Returns `true` if the candidate contains no information that a developer could use.
  145. var isEmpty: Bool {
  146. content.parts
  147. .isEmpty && finishReason == nil && citationMetadata == nil && groundingMetadata == nil
  148. }
  149. }
  150. /// A collection of source attributions for a piece of content.
  151. @available(iOS 15.0, macOS 12.0, macCatalyst 15.0, tvOS 15.0, watchOS 8.0, *)
  152. public struct CitationMetadata: Sendable {
  153. /// A list of individual cited sources and the parts of the content to which they apply.
  154. public let citations: [Citation]
  155. }
  156. /// A struct describing a source attribution.
  157. @available(iOS 15.0, macOS 12.0, macCatalyst 15.0, tvOS 15.0, watchOS 8.0, *)
  158. public struct Citation: Sendable, Equatable {
  159. /// The inclusive beginning of a sequence in a model response that derives from a cited source.
  160. public let startIndex: Int
  161. /// The exclusive end of a sequence in a model response that derives from a cited source.
  162. public let endIndex: Int
  163. /// A link to the cited source, if available.
  164. public let uri: String?
  165. /// The title of the cited source, if available.
  166. public let title: String?
  167. /// The license the cited source work is distributed under, if specified.
  168. public let license: String?
  169. /// The publication date of the cited source, if available.
  170. ///
  171. /// > Tip: `DateComponents` can be converted to a `Date` using the `date` computed property.
  172. public let publicationDate: DateComponents?
  173. init(startIndex: Int,
  174. endIndex: Int,
  175. uri: String? = nil,
  176. title: String? = nil,
  177. license: String? = nil,
  178. publicationDate: DateComponents? = nil) {
  179. self.startIndex = startIndex
  180. self.endIndex = endIndex
  181. self.uri = uri
  182. self.title = title
  183. self.license = license
  184. self.publicationDate = publicationDate
  185. }
  186. }
  187. /// A value enumerating possible reasons for a model to terminate a content generation request.
  188. @available(iOS 15.0, macOS 12.0, macCatalyst 15.0, tvOS 15.0, watchOS 8.0, *)
  189. public struct FinishReason: DecodableProtoEnum, Hashable, Sendable {
  190. enum Kind: String {
  191. case stop = "STOP"
  192. case maxTokens = "MAX_TOKENS"
  193. case safety = "SAFETY"
  194. case recitation = "RECITATION"
  195. case other = "OTHER"
  196. case blocklist = "BLOCKLIST"
  197. case prohibitedContent = "PROHIBITED_CONTENT"
  198. case spii = "SPII"
  199. case malformedFunctionCall = "MALFORMED_FUNCTION_CALL"
  200. }
  201. /// Natural stop point of the model or provided stop sequence.
  202. public static let stop = FinishReason(kind: .stop)
  203. /// The maximum number of tokens as specified in the request was reached.
  204. public static let maxTokens = FinishReason(kind: .maxTokens)
  205. /// The token generation was stopped because the response was flagged for safety reasons.
  206. ///
  207. /// > NOTE: When streaming, the ``Candidate/content`` will be empty if content filters blocked the
  208. /// > output.
  209. public static let safety = FinishReason(kind: .safety)
  210. /// The token generation was stopped because the response was flagged for unauthorized citations.
  211. public static let recitation = FinishReason(kind: .recitation)
  212. /// All other reasons that stopped token generation.
  213. public static let other = FinishReason(kind: .other)
  214. /// Token generation was stopped because the response contained forbidden terms.
  215. public static let blocklist = FinishReason(kind: .blocklist)
  216. /// Token generation was stopped because the response contained potentially prohibited content.
  217. public static let prohibitedContent = FinishReason(kind: .prohibitedContent)
  218. /// Token generation was stopped because of Sensitive Personally Identifiable Information (SPII).
  219. public static let spii = FinishReason(kind: .spii)
  220. /// Token generation was stopped because the function call generated by the model was invalid.
  221. public static let malformedFunctionCall = FinishReason(kind: .malformedFunctionCall)
  222. /// Returns the raw string representation of the `FinishReason` value.
  223. ///
  224. /// > Note: This value directly corresponds to the values in the [REST
  225. /// > API](https://cloud.google.com/vertex-ai/docs/reference/rest/v1beta1/GenerateContentResponse#FinishReason).
  226. public let rawValue: String
  227. static let unrecognizedValueMessageCode =
  228. AILog.MessageCode.generateContentResponseUnrecognizedFinishReason
  229. }
  230. /// A metadata struct containing any feedback the model had on the prompt it was provided.
  231. @available(iOS 15.0, macOS 12.0, macCatalyst 15.0, tvOS 15.0, watchOS 8.0, *)
  232. public struct PromptFeedback: Sendable {
  233. /// A type describing possible reasons to block a prompt.
  234. @available(iOS 15.0, macOS 12.0, macCatalyst 15.0, tvOS 15.0, watchOS 8.0, *)
  235. public struct BlockReason: DecodableProtoEnum, Hashable, Sendable {
  236. enum Kind: String {
  237. case safety = "SAFETY"
  238. case other = "OTHER"
  239. case blocklist = "BLOCKLIST"
  240. case prohibitedContent = "PROHIBITED_CONTENT"
  241. }
  242. /// The prompt was blocked because it was deemed unsafe.
  243. public static let safety = BlockReason(kind: .safety)
  244. /// All other block reasons.
  245. public static let other = BlockReason(kind: .other)
  246. /// The prompt was blocked because it contained terms from the terminology blocklist.
  247. public static let blocklist = BlockReason(kind: .blocklist)
  248. /// The prompt was blocked due to prohibited content.
  249. public static let prohibitedContent = BlockReason(kind: .prohibitedContent)
  250. /// Returns the raw string representation of the `BlockReason` value.
  251. ///
  252. /// > Note: This value directly corresponds to the values in the [REST
  253. /// > API](https://cloud.google.com/vertex-ai/docs/reference/rest/v1beta1/GenerateContentResponse#BlockedReason).
  254. public let rawValue: String
  255. static let unrecognizedValueMessageCode =
  256. AILog.MessageCode.generateContentResponseUnrecognizedBlockReason
  257. }
  258. /// The reason a prompt was blocked, if it was blocked.
  259. public let blockReason: BlockReason?
  260. /// A human-readable description of the ``blockReason``.
  261. public let blockReasonMessage: String?
  262. /// The safety ratings of the prompt.
  263. public let safetyRatings: [SafetyRating]
  264. /// Initializer for SwiftUI previews or tests.
  265. public init(blockReason: BlockReason?, blockReasonMessage: String? = nil,
  266. safetyRatings: [SafetyRating]) {
  267. self.blockReason = blockReason
  268. self.blockReasonMessage = blockReasonMessage
  269. self.safetyRatings = safetyRatings
  270. }
  271. }
  272. /// Metadata returned to the client when grounding is enabled.
  273. ///
  274. /// > Important: If using Grounding with Google Search, you are required to comply with the
  275. /// "Grounding with Google Search" usage requirements for your chosen API provider:
  276. /// [Gemini Developer API](https://ai.google.dev/gemini-api/terms#grounding-with-google-search)
  277. /// or Vertex AI Gemini API (see [Service Terms](https://cloud.google.com/terms/service-terms)
  278. /// section within the Service Specific Terms).
  279. @available(iOS 15.0, macOS 12.0, macCatalyst 15.0, tvOS 15.0, watchOS 8.0, *)
  280. public struct GroundingMetadata: Sendable, Equatable, Hashable {
  281. /// A list of web search queries that the model performed to gather the grounding information.
  282. /// These can be used to allow users to explore the search results themselves.
  283. public let webSearchQueries: [String]
  284. /// A list of ``GroundingChunk`` structs. Each chunk represents a piece of retrieved content
  285. /// (e.g., from a web page) that the model used to ground its response.
  286. public let groundingChunks: [GroundingChunk]
  287. /// A list of ``GroundingSupport`` structs. Each object details how specific segments of the
  288. /// model's response are supported by the `groundingChunks`.
  289. public let groundingSupports: [GroundingSupport]
  290. /// Google Search entry point for web searches.
  291. /// This contains an HTML/CSS snippet that **must** be embedded in an app to display a Google
  292. /// Search entry point for follow-up web searches related to the model's "Grounded Response".
  293. public let searchEntryPoint: SearchEntryPoint?
  294. /// A struct representing the Google Search entry point.
  295. @available(iOS 15.0, macOS 12.0, macCatalyst 15.0, tvOS 15.0, watchOS 8.0, *)
  296. public struct SearchEntryPoint: Sendable, Equatable, Hashable {
  297. /// An HTML/CSS snippet that can be embedded in your app.
  298. ///
  299. /// To ensure proper rendering, it's recommended to display this content within a `WKWebView`.
  300. public let renderedContent: String
  301. }
  302. /// Represents a chunk of retrieved data that supports a claim in the model's response. This is
  303. /// part of the grounding information provided when grounding is enabled.
  304. @available(iOS 15.0, macOS 12.0, macCatalyst 15.0, tvOS 15.0, watchOS 8.0, *)
  305. public struct GroundingChunk: Sendable, Equatable, Hashable {
  306. /// Contains details if the grounding chunk is from a web source.
  307. public let web: WebGroundingChunk?
  308. }
  309. /// A grounding chunk sourced from the web.
  310. @available(iOS 15.0, macOS 12.0, macCatalyst 15.0, tvOS 15.0, watchOS 8.0, *)
  311. public struct WebGroundingChunk: Sendable, Equatable, Hashable {
  312. /// The URI of the retrieved web page.
  313. public let uri: String?
  314. /// The title of the retrieved web page.
  315. public let title: String?
  316. /// The domain of the original URI from which the content was retrieved.
  317. ///
  318. /// This field is only populated when using the Vertex AI Gemini API.
  319. public let domain: String?
  320. }
  321. /// Provides information about how a specific segment of the model's response is supported by the
  322. /// retrieved grounding chunks.
  323. @available(iOS 15.0, macOS 12.0, macCatalyst 15.0, tvOS 15.0, watchOS 8.0, *)
  324. public struct GroundingSupport: Sendable, Equatable, Hashable {
  325. /// Specifies the segment of the model's response content that this grounding support pertains
  326. /// to.
  327. public let segment: Segment
  328. /// A list of indices that refer to specific ``GroundingChunk`` structs within the
  329. /// ``GroundingMetadata/groundingChunks`` array. These referenced chunks are the sources that
  330. /// support the claim made in the associated `segment` of the response. For example, an array
  331. /// `[1, 3, 4]`
  332. /// means that `groundingChunks[1]`, `groundingChunks[3]`, `groundingChunks[4]` are the
  333. /// retrieved content supporting this part of the response.
  334. public let groundingChunkIndices: [Int]
  335. struct Internal {
  336. let segment: Segment?
  337. let groundingChunkIndices: [Int]
  338. func toPublic() -> GroundingSupport? {
  339. if segment == nil {
  340. return nil
  341. }
  342. return GroundingSupport(
  343. segment: segment!,
  344. groundingChunkIndices: groundingChunkIndices
  345. )
  346. }
  347. }
  348. }
  349. }
  350. /// Represents a specific segment within a ``ModelContent`` struct, often used to pinpoint the
  351. /// exact location of text or data that grounding information refers to.
  352. @available(iOS 15.0, macOS 12.0, macCatalyst 15.0, tvOS 15.0, watchOS 8.0, *)
  353. public struct Segment: Sendable, Equatable, Hashable {
  354. /// The zero-based index of the ``Part`` object within the `parts` array of its parent
  355. /// ``ModelContent`` object. This identifies which part of the content the segment belongs to.
  356. public let partIndex: Int
  357. /// The zero-based start index of the segment within the specified ``Part``, measured in UTF-8
  358. /// bytes. This offset is inclusive, starting from 0 at the beginning of the part's content.
  359. public let startIndex: Int
  360. /// The zero-based end index of the segment within the specified ``Part``, measured in UTF-8
  361. /// bytes. This offset is exclusive, meaning the character at this index is not included in the
  362. /// segment.
  363. public let endIndex: Int
  364. /// The text corresponding to the segment from the response.
  365. public let text: String
  366. }
  367. // MARK: - Codable Conformances
  368. @available(iOS 15.0, macOS 12.0, macCatalyst 15.0, tvOS 15.0, watchOS 8.0, *)
  369. extension GenerateContentResponse: Decodable {
  370. enum CodingKeys: CodingKey {
  371. case candidates
  372. case promptFeedback
  373. case usageMetadata
  374. }
  375. public init(from decoder: Decoder) throws {
  376. let container = try decoder.container(keyedBy: CodingKeys.self)
  377. guard container.contains(CodingKeys.candidates) || container
  378. .contains(CodingKeys.promptFeedback) else {
  379. let context = DecodingError.Context(
  380. codingPath: [],
  381. debugDescription: "Failed to decode GenerateContentResponse;" +
  382. " missing keys 'candidates' and 'promptFeedback'."
  383. )
  384. throw DecodingError.dataCorrupted(context)
  385. }
  386. if let candidates = try container.decodeIfPresent(
  387. [Candidate].self,
  388. forKey: .candidates
  389. ) {
  390. self.candidates = candidates
  391. } else {
  392. candidates = []
  393. }
  394. promptFeedback = try container.decodeIfPresent(PromptFeedback.self, forKey: .promptFeedback)
  395. usageMetadata = try container.decodeIfPresent(UsageMetadata.self, forKey: .usageMetadata)
  396. }
  397. }
  398. @available(iOS 15.0, macOS 12.0, macCatalyst 15.0, tvOS 15.0, watchOS 8.0, *)
  399. extension GenerateContentResponse.UsageMetadata: Decodable {
  400. enum CodingKeys: CodingKey {
  401. case promptTokenCount
  402. case candidatesTokenCount
  403. case thoughtsTokenCount
  404. case totalTokenCount
  405. case promptTokensDetails
  406. case candidatesTokensDetails
  407. }
  408. public init(from decoder: any Decoder) throws {
  409. let container = try decoder.container(keyedBy: CodingKeys.self)
  410. promptTokenCount = try container.decodeIfPresent(Int.self, forKey: .promptTokenCount) ?? 0
  411. candidatesTokenCount =
  412. try container.decodeIfPresent(Int.self, forKey: .candidatesTokenCount) ?? 0
  413. thoughtsTokenCount = try container.decodeIfPresent(Int.self, forKey: .thoughtsTokenCount) ?? 0
  414. totalTokenCount = try container.decodeIfPresent(Int.self, forKey: .totalTokenCount) ?? 0
  415. promptTokensDetails =
  416. try container.decodeIfPresent([ModalityTokenCount].self, forKey: .promptTokensDetails) ?? []
  417. candidatesTokensDetails = try container.decodeIfPresent(
  418. [ModalityTokenCount].self,
  419. forKey: .candidatesTokensDetails
  420. ) ?? []
  421. }
  422. }
  423. @available(iOS 15.0, macOS 12.0, macCatalyst 15.0, tvOS 15.0, watchOS 8.0, *)
  424. extension Candidate: Decodable {
  425. enum CodingKeys: CodingKey {
  426. case content
  427. case safetyRatings
  428. case finishReason
  429. case citationMetadata
  430. case groundingMetadata
  431. }
  432. /// Initializes a response from a decoder. Used for decoding server responses; not for public
  433. /// use.
  434. public init(from decoder: Decoder) throws {
  435. let container = try decoder.container(keyedBy: CodingKeys.self)
  436. do {
  437. if let content = try container.decodeIfPresent(ModelContent.self, forKey: .content) {
  438. self.content = content
  439. } else {
  440. content = ModelContent(parts: [])
  441. }
  442. } catch {
  443. throw InvalidCandidateError.malformedContent(underlyingError: error)
  444. }
  445. if let safetyRatings = try container.decodeIfPresent(
  446. [SafetyRating].self, forKey: .safetyRatings
  447. ) {
  448. self.safetyRatings = safetyRatings.filter {
  449. // Due to a bug in the backend, the SDK may receive invalid `SafetyRating` values that do
  450. // not include a category or probability; these are filtered out of the safety ratings.
  451. $0.category != HarmCategory.unspecified
  452. && $0.probability != SafetyRating.HarmProbability.unspecified
  453. }
  454. } else {
  455. safetyRatings = []
  456. }
  457. finishReason = try container.decodeIfPresent(FinishReason.self, forKey: .finishReason)
  458. citationMetadata = try container.decodeIfPresent(
  459. CitationMetadata.self,
  460. forKey: .citationMetadata
  461. )
  462. groundingMetadata = try container.decodeIfPresent(
  463. GroundingMetadata.self,
  464. forKey: .groundingMetadata
  465. )
  466. }
  467. }
  468. @available(iOS 15.0, macOS 12.0, macCatalyst 15.0, tvOS 15.0, watchOS 8.0, *)
  469. extension CitationMetadata: Decodable {
  470. enum CodingKeys: CodingKey {
  471. case citations // Vertex AI
  472. case citationSources // Google AI
  473. }
  474. public init(from decoder: any Decoder) throws {
  475. let container = try decoder.container(keyedBy: CodingKeys.self)
  476. // Decode for Google API if `citationSources` key is present.
  477. if container.contains(.citationSources) {
  478. citations = try container.decode([Citation].self, forKey: .citationSources)
  479. } else { // Fallback to default Vertex AI decoding.
  480. citations = try container.decode([Citation].self, forKey: .citations)
  481. }
  482. }
  483. }
  484. @available(iOS 15.0, macOS 12.0, macCatalyst 15.0, tvOS 15.0, watchOS 8.0, *)
  485. extension Citation: Decodable {
  486. enum CodingKeys: CodingKey {
  487. case startIndex
  488. case endIndex
  489. case uri
  490. case title
  491. case license
  492. case publicationDate
  493. }
  494. public init(from decoder: any Decoder) throws {
  495. let container = try decoder.container(keyedBy: CodingKeys.self)
  496. startIndex = try container.decodeIfPresent(Int.self, forKey: .startIndex) ?? 0
  497. endIndex = try container.decode(Int.self, forKey: .endIndex)
  498. if let uri = try container.decodeIfPresent(String.self, forKey: .uri), !uri.isEmpty {
  499. self.uri = uri
  500. } else {
  501. uri = nil
  502. }
  503. if let title = try container.decodeIfPresent(String.self, forKey: .title), !title.isEmpty {
  504. self.title = title
  505. } else {
  506. title = nil
  507. }
  508. if let license = try container.decodeIfPresent(String.self, forKey: .license),
  509. !license.isEmpty {
  510. self.license = license
  511. } else {
  512. license = nil
  513. }
  514. if let publicationProtoDate = try container.decodeIfPresent(
  515. ProtoDate.self,
  516. forKey: .publicationDate
  517. ) {
  518. publicationDate = publicationProtoDate.dateComponents
  519. if let publicationDate, !publicationDate.isValidDate {
  520. AILog.warning(
  521. code: .decodedInvalidCitationPublicationDate,
  522. "Decoded an invalid citation publication date: \(publicationDate)"
  523. )
  524. }
  525. } else {
  526. publicationDate = nil
  527. }
  528. }
  529. }
  530. @available(iOS 15.0, macOS 12.0, macCatalyst 15.0, tvOS 15.0, watchOS 8.0, *)
  531. extension PromptFeedback: Decodable {
  532. enum CodingKeys: CodingKey {
  533. case blockReason
  534. case blockReasonMessage
  535. case safetyRatings
  536. }
  537. public init(from decoder: Decoder) throws {
  538. let container = try decoder.container(keyedBy: CodingKeys.self)
  539. blockReason = try container.decodeIfPresent(
  540. PromptFeedback.BlockReason.self,
  541. forKey: .blockReason
  542. )
  543. blockReasonMessage = try container.decodeIfPresent(String.self, forKey: .blockReasonMessage)
  544. if let safetyRatings = try container.decodeIfPresent(
  545. [SafetyRating].self,
  546. forKey: .safetyRatings
  547. ) {
  548. self.safetyRatings = safetyRatings
  549. } else {
  550. safetyRatings = []
  551. }
  552. }
  553. }
  554. @available(iOS 15.0, macOS 12.0, macCatalyst 15.0, tvOS 15.0, watchOS 8.0, *)
  555. extension GroundingMetadata: Decodable {
  556. enum CodingKeys: String, CodingKey {
  557. case webSearchQueries
  558. case groundingChunks
  559. case groundingSupports
  560. case searchEntryPoint
  561. }
  562. public init(from decoder: Decoder) throws {
  563. let container = try decoder.container(keyedBy: CodingKeys.self)
  564. webSearchQueries = try container.decodeIfPresent([String].self, forKey: .webSearchQueries) ?? []
  565. groundingChunks = try container.decodeIfPresent(
  566. [GroundingChunk].self,
  567. forKey: .groundingChunks
  568. ) ?? []
  569. groundingSupports = try container.decodeIfPresent(
  570. [GroundingSupport.Internal].self,
  571. forKey: .groundingSupports
  572. )?.compactMap { $0.toPublic() } ?? []
  573. searchEntryPoint = try container.decodeIfPresent(
  574. SearchEntryPoint.self,
  575. forKey: .searchEntryPoint
  576. )
  577. }
  578. }
  579. @available(iOS 15.0, macOS 12.0, macCatalyst 15.0, tvOS 15.0, watchOS 8.0, *)
  580. extension GroundingMetadata.SearchEntryPoint: Decodable {}
  581. @available(iOS 15.0, macOS 12.0, macCatalyst 15.0, tvOS 15.0, watchOS 8.0, *)
  582. extension GroundingMetadata.GroundingChunk: Decodable {}
  583. @available(iOS 15.0, macOS 12.0, macCatalyst 15.0, tvOS 15.0, watchOS 8.0, *)
  584. extension GroundingMetadata.WebGroundingChunk: Decodable {}
  585. @available(iOS 15.0, macOS 12.0, macCatalyst 15.0, tvOS 15.0, watchOS 8.0, *)
  586. extension GroundingMetadata.GroundingSupport.Internal: Decodable {
  587. enum CodingKeys: String, CodingKey {
  588. case segment
  589. case groundingChunkIndices
  590. }
  591. public init(from decoder: Decoder) throws {
  592. let container = try decoder.container(keyedBy: CodingKeys.self)
  593. segment = try container.decodeIfPresent(Segment.self, forKey: .segment)
  594. groundingChunkIndices = try container.decodeIfPresent(
  595. [Int].self,
  596. forKey: .groundingChunkIndices
  597. ) ?? []
  598. }
  599. }
  600. @available(iOS 15.0, macOS 12.0, macCatalyst 15.0, tvOS 15.0, watchOS 8.0, *)
  601. extension Segment: Decodable {
  602. enum CodingKeys: String, CodingKey {
  603. case partIndex
  604. case startIndex
  605. case endIndex
  606. case text
  607. }
  608. public init(from decoder: Decoder) throws {
  609. let container = try decoder.container(keyedBy: CodingKeys.self)
  610. partIndex = try container.decodeIfPresent(Int.self, forKey: .partIndex) ?? 0
  611. startIndex = try container.decodeIfPresent(Int.self, forKey: .startIndex) ?? 0
  612. endIndex = try container.decodeIfPresent(Int.self, forKey: .endIndex) ?? 0
  613. text = try container.decodeIfPresent(String.self, forKey: .text) ?? ""
  614. }
  615. }