GenerateContentResponse.swift 28 KB

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