GenerateContentResponse.swift 27 KB

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