From 584b1e9a8561716a34b16b8729ca9b85a60f0f95 Mon Sep 17 00:00:00 2001 From: Andrew Heard Date: Fri, 19 Apr 2024 17:04:35 -0400 Subject: [PATCH] Remove public `Codable` (`Decodable` and/or `Encodable`) conformance (#142) --- Sources/GoogleAI/CountTokensRequest.swift | 21 +- Sources/GoogleAI/FunctionCalling.swift | 33 ++- .../GoogleAI/GenerateContentResponse.swift | 241 +++++++++--------- Sources/GoogleAI/GenerationConfig.swift | 7 +- Sources/GoogleAI/ModelContent.swift | 146 ++++++----- Sources/GoogleAI/Safety.swift | 108 ++++---- 6 files changed, 302 insertions(+), 254 deletions(-) diff --git a/Sources/GoogleAI/CountTokensRequest.swift b/Sources/GoogleAI/CountTokensRequest.swift index 0a58d40..de852ae 100644 --- a/Sources/GoogleAI/CountTokensRequest.swift +++ b/Sources/GoogleAI/CountTokensRequest.swift @@ -21,13 +21,6 @@ struct CountTokensRequest { let options: RequestOptions } -@available(iOS 15.0, macOS 11.0, macCatalyst 15.0, *) -extension CountTokensRequest: Encodable { - enum CodingKeys: CodingKey { - case contents - } -} - @available(iOS 15.0, macOS 11.0, macCatalyst 15.0, *) extension CountTokensRequest: GenerativeAIRequest { typealias Response = CountTokensResponse @@ -39,7 +32,19 @@ extension CountTokensRequest: GenerativeAIRequest { /// The model's response to a count tokens request. @available(iOS 15.0, macOS 11.0, macCatalyst 15.0, *) -public struct CountTokensResponse: Decodable { +public struct CountTokensResponse { /// The total number of tokens in the input given to the model as a prompt. public let totalTokens: Int } + +// MARK: - Codable Conformances + +@available(iOS 15.0, macOS 11.0, macCatalyst 15.0, *) +extension CountTokensRequest: Encodable { + enum CodingKeys: CodingKey { + case contents + } +} + +@available(iOS 15.0, macOS 11.0, macCatalyst 15.0, *) +extension CountTokensResponse: Decodable {} diff --git a/Sources/GoogleAI/FunctionCalling.swift b/Sources/GoogleAI/FunctionCalling.swift index 8860344..17c9611 100644 --- a/Sources/GoogleAI/FunctionCalling.swift +++ b/Sources/GoogleAI/FunctionCalling.swift @@ -15,7 +15,7 @@ import Foundation /// A predicted function call returned from the model. -public struct FunctionCall: Equatable, Encodable { +public struct FunctionCall: Equatable { /// The name of the function to call. public let name: String @@ -27,7 +27,7 @@ public struct FunctionCall: Equatable, Encodable { /// /// These types can be objects, but also primitives and arrays. Represents a select subset of an /// [OpenAPI 3.0 schema object](https://spec.openapis.org/oas/v3.0.3#schema). -public class Schema: Encodable { +public class Schema { /// The data type. let type: DataType @@ -98,7 +98,7 @@ public class Schema: Encodable { /// A data type. /// /// Contains the set of OpenAPI [data types](https://spec.openapis.org/oas/v3.0.3#data-types). -public enum DataType: String, Encodable { +public enum DataType: String { /// A `String` type. case string = "STRING" @@ -157,7 +157,7 @@ public struct FunctionDeclaration { /// /// A `Tool` is a piece of code that enables the system to interact with external systems to /// perform an action, or set of actions, outside of knowledge and scope of the model. -public struct Tool: Encodable { +public struct Tool { /// A list of `FunctionDeclarations` available to the model. let functionDeclarations: [FunctionDeclaration]? @@ -178,10 +178,10 @@ public struct Tool: Encodable { } /// Configuration for specifying function calling behavior. -public struct FunctionCallingConfig: Encodable { +public struct FunctionCallingConfig { /// Defines the execution behavior for function calling by defining the /// execution mode. - public enum Mode: String, Encodable { + public enum Mode: String { /// The default behavior for function calling. The model calls functions to answer queries at /// its discretion. case auto = "AUTO" @@ -213,8 +213,7 @@ public struct FunctionCallingConfig: Encodable { } /// Tool configuration for any `Tool` specified in the request. -@available(iOS 15.0, macOS 11.0, macCatalyst 15.0, *) -public struct ToolConfig: Encodable { +public struct ToolConfig { let functionCallingConfig: FunctionCallingConfig? public init(functionCallingConfig: FunctionCallingConfig? = nil) { @@ -227,7 +226,7 @@ public struct ToolConfig: Encodable { /// Contains a string representing the `FunctionDeclaration.name` and a structured JSON object /// containing any output from the function is used as context to the model. This should contain the /// result of a ``FunctionCall`` made based on model prediction. -public struct FunctionResponse: Equatable, Encodable { +public struct FunctionResponse: Equatable { /// The name of the function that was called. let name: String @@ -264,6 +263,8 @@ extension FunctionCall: Decodable { } } +extension FunctionCall: Encodable {} + extension FunctionDeclaration: Encodable { enum CodingKeys: String, CodingKey { case name @@ -278,3 +279,17 @@ extension FunctionDeclaration: Encodable { try container.encode(parameters, forKey: .parameters) } } + +extension Schema: Encodable {} + +extension DataType: Encodable {} + +extension Tool: Encodable {} + +extension FunctionCallingConfig: Encodable {} + +extension FunctionCallingConfig.Mode: Encodable {} + +extension ToolConfig: Encodable {} + +extension FunctionResponse: Encodable {} diff --git a/Sources/GoogleAI/GenerateContentResponse.swift b/Sources/GoogleAI/GenerateContentResponse.swift index e623d74..4b01522 100644 --- a/Sources/GoogleAI/GenerateContentResponse.swift +++ b/Sources/GoogleAI/GenerateContentResponse.swift @@ -57,38 +57,6 @@ public struct GenerateContentResponse { } } -@available(iOS 15.0, macOS 11.0, macCatalyst 15.0, *) -extension GenerateContentResponse: Decodable { - enum CodingKeys: CodingKey { - case candidates - case promptFeedback - } - - public init(from decoder: Decoder) throws { - let container = try decoder.container(keyedBy: CodingKeys.self) - - guard container.contains(CodingKeys.candidates) || container - .contains(CodingKeys.promptFeedback) else { - let context = DecodingError.Context( - codingPath: [], - debugDescription: "Failed to decode GenerateContentResponse;" + - " missing keys 'candidates' and 'promptFeedback'." - ) - throw DecodingError.dataCorrupted(context) - } - - if let candidates = try container.decodeIfPresent( - [CandidateResponse].self, - forKey: .candidates - ) { - self.candidates = candidates - } else { - candidates = [] - } - promptFeedback = try container.decodeIfPresent(PromptFeedback.self, forKey: .promptFeedback) - } -} - /// A struct representing a possible reply to a content generation prompt. Each content generation /// prompt may produce multiple candidate responses. @available(iOS 15.0, macOS 11.0, macCatalyst 15.0, *) @@ -116,58 +84,9 @@ public struct CandidateResponse { } } -@available(iOS 15.0, macOS 11.0, macCatalyst 15.0, *) -extension CandidateResponse: Decodable { - enum CodingKeys: CodingKey { - case content - case safetyRatings - case finishReason - case finishMessage - case citationMetadata - } - - /// Initializes a response from a decoder. Used for decoding server responses; not for public - /// use. - public init(from decoder: Decoder) throws { - let container = try decoder.container(keyedBy: CodingKeys.self) - - do { - if let content = try container.decodeIfPresent(ModelContent.self, forKey: .content) { - self.content = content - } else { - content = ModelContent(parts: []) - } - } catch { - // Check if `content` can be decoded as an empty dictionary to detect the `"content": {}` bug. - if let content = try? container.decode([String: String].self, forKey: .content), - content.isEmpty { - throw InvalidCandidateError.emptyContent(underlyingError: error) - } else { - throw InvalidCandidateError.malformedContent(underlyingError: error) - } - } - - if let safetyRatings = try container.decodeIfPresent( - [SafetyRating].self, - forKey: .safetyRatings - ) { - self.safetyRatings = safetyRatings - } else { - safetyRatings = [] - } - - finishReason = try container.decodeIfPresent(FinishReason.self, forKey: .finishReason) - - citationMetadata = try container.decodeIfPresent( - CitationMetadata.self, - forKey: .citationMetadata - ) - } -} - /// A collection of source attributions for a piece of content. @available(iOS 15.0, macOS 11.0, macCatalyst 15.0, *) -public struct CitationMetadata: Decodable { +public struct CitationMetadata { /// A list of individual cited sources and the parts of the content to which they apply. public let citationSources: [Citation] } @@ -213,27 +132,11 @@ public enum FinishReason: String { case other = "OTHER" } -@available(iOS 15.0, macOS 11.0, macCatalyst 15.0, *) -extension FinishReason: Decodable { - /// Do not explicitly use. Initializer required for Decodable conformance. - public init(from decoder: Decoder) throws { - let value = try decoder.singleValueContainer().decode(String.self) - guard let decodedFinishReason = FinishReason(rawValue: value) else { - Logging.default - .error("[GoogleGenerativeAI] Unrecognized FinishReason with value \"\(value)\".") - self = .unknown - return - } - - self = decodedFinishReason - } -} - /// A metadata struct containing any feedback the model had on the prompt it was provided. @available(iOS 15.0, macOS 11.0, macCatalyst 15.0, *) public struct PromptFeedback { /// A type describing possible reasons to block a prompt. - public enum BlockReason: String, Decodable { + public enum BlockReason: String { /// The block reason is unknown. case unknown = "UNKNOWN" @@ -245,19 +148,6 @@ public struct PromptFeedback { /// All other block reasons. case other = "OTHER" - - /// Do not explicitly use. Initializer required for Decodable conformance. - public init(from decoder: Decoder) throws { - let value = try decoder.singleValueContainer().decode(String.self) - guard let decodedBlockReason = BlockReason(rawValue: value) else { - Logging.default - .error("[GoogleGenerativeAI] Unrecognized BlockReason with value \"\(value)\".") - self = .unknown - return - } - - self = decodedBlockReason - } } /// The reason a prompt was blocked, if it was blocked. @@ -273,20 +163,69 @@ public struct PromptFeedback { } } +// MARK: - Codable Conformances + @available(iOS 15.0, macOS 11.0, macCatalyst 15.0, *) -extension PromptFeedback: Decodable { +extension GenerateContentResponse: Decodable { enum CodingKeys: CodingKey { - case blockReason + case candidates + case promptFeedback + } + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + + guard container.contains(CodingKeys.candidates) || container + .contains(CodingKeys.promptFeedback) else { + let context = DecodingError.Context( + codingPath: [], + debugDescription: "Failed to decode GenerateContentResponse;" + + " missing keys 'candidates' and 'promptFeedback'." + ) + throw DecodingError.dataCorrupted(context) + } + + if let candidates = try container.decodeIfPresent( + [CandidateResponse].self, + forKey: .candidates + ) { + self.candidates = candidates + } else { + candidates = [] + } + promptFeedback = try container.decodeIfPresent(PromptFeedback.self, forKey: .promptFeedback) + } +} + +@available(iOS 15.0, macOS 11.0, macCatalyst 15.0, *) +extension CandidateResponse: Decodable { + enum CodingKeys: CodingKey { + case content case safetyRatings + case finishReason + case finishMessage + case citationMetadata } - /// Do not explicitly use. Initializer required for Decodable conformance. public init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) - blockReason = try container.decodeIfPresent( - PromptFeedback.BlockReason.self, - forKey: .blockReason - ) + + do { + if let content = try container.decodeIfPresent(ModelContent.self, forKey: .content) { + self.content = content + } else { + content = ModelContent(parts: []) + } + } catch { + // Check if `content` can be decoded as an empty dictionary to detect the `"content": {}` bug. + if let content = try? container.decode([String: String].self, forKey: .content), + content.isEmpty { + throw InvalidCandidateError.emptyContent(underlyingError: error) + } else { + throw InvalidCandidateError.malformedContent(underlyingError: error) + } + } + if let safetyRatings = try container.decodeIfPresent( [SafetyRating].self, forKey: .safetyRatings @@ -295,10 +234,18 @@ extension PromptFeedback: Decodable { } else { safetyRatings = [] } + + finishReason = try container.decodeIfPresent(FinishReason.self, forKey: .finishReason) + + citationMetadata = try container.decodeIfPresent( + CitationMetadata.self, + forKey: .citationMetadata + ) } } -// MARK: - Codable Conformances +@available(iOS 15.0, macOS 11.0, macCatalyst 15.0, *) +extension CitationMetadata: Decodable {} @available(iOS 15.0, macOS 11.0, macCatalyst 15.0, *) extension Citation: Decodable { @@ -322,3 +269,57 @@ extension Citation: Decodable { } } } + +@available(iOS 15.0, macOS 11.0, macCatalyst 15.0, *) +extension FinishReason: Decodable { + public init(from decoder: Decoder) throws { + let value = try decoder.singleValueContainer().decode(String.self) + guard let decodedFinishReason = FinishReason(rawValue: value) else { + Logging.default + .error("[GoogleGenerativeAI] Unrecognized FinishReason with value \"\(value)\".") + self = .unknown + return + } + + self = decodedFinishReason + } +} + +@available(iOS 15.0, macOS 11.0, macCatalyst 15.0, *) +extension PromptFeedback.BlockReason: Decodable { + public init(from decoder: Decoder) throws { + let value = try decoder.singleValueContainer().decode(String.self) + guard let decodedBlockReason = PromptFeedback.BlockReason(rawValue: value) else { + Logging.default + .error("[GoogleGenerativeAI] Unrecognized BlockReason with value \"\(value)\".") + self = .unknown + return + } + + self = decodedBlockReason + } +} + +@available(iOS 15.0, macOS 11.0, macCatalyst 15.0, *) +extension PromptFeedback: Decodable { + enum CodingKeys: CodingKey { + case blockReason + case safetyRatings + } + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + blockReason = try container.decodeIfPresent( + PromptFeedback.BlockReason.self, + forKey: .blockReason + ) + if let safetyRatings = try container.decodeIfPresent( + [SafetyRating].self, + forKey: .safetyRatings + ) { + self.safetyRatings = safetyRatings + } else { + safetyRatings = [] + } + } +} diff --git a/Sources/GoogleAI/GenerationConfig.swift b/Sources/GoogleAI/GenerationConfig.swift index 2d1016c..18bf2bd 100644 --- a/Sources/GoogleAI/GenerationConfig.swift +++ b/Sources/GoogleAI/GenerationConfig.swift @@ -17,7 +17,7 @@ import Foundation /// A struct defining model parameters to be used when sending generative AI /// requests to the backend model. @available(iOS 15.0, macOS 11.0, macCatalyst 15.0, *) -public struct GenerationConfig: Encodable { +public struct GenerationConfig { /// A parameter controlling the degree of randomness in token selection. A /// temperature of zero is deterministic, always choosing the /// highest-probability response. Typical values are between 0 and 1 @@ -84,3 +84,8 @@ public struct GenerationConfig: Encodable { self.stopSequences = stopSequences } } + +// MARK: - Codable Conformances + +@available(iOS 15.0, macOS 11.0, macCatalyst 15.0, *) +extension GenerationConfig: Encodable {} diff --git a/Sources/GoogleAI/ModelContent.swift b/Sources/GoogleAI/ModelContent.swift index 9c96625..70b08a6 100644 --- a/Sources/GoogleAI/ModelContent.swift +++ b/Sources/GoogleAI/ModelContent.swift @@ -18,28 +18,10 @@ import Foundation /// request or response contains an `Array` of ``ModelContent``s, and each ``ModelContent`` value /// may comprise multiple heterogeneous ``ModelContent/Part``s. @available(iOS 15.0, macOS 11.0, macCatalyst 15.0, *) -public struct ModelContent: Codable, Equatable { +public struct ModelContent: Equatable { /// A discrete piece of data in a media format intepretable by an AI model. Within a single value /// of ``Part``, different data types may not mix. - public enum Part: Codable, Equatable { - enum CodingKeys: String, CodingKey { - case text - case inlineData - case fileData - case functionCall - case functionResponse - } - - enum InlineDataKeys: String, CodingKey { - case mimeType = "mime_type" - case bytes = "data" - } - - enum FileDataKeys: String, CodingKey { - case mimeType = "mime_type" - case url = "file_uri" - } - + public enum Part: Equatable { /// Text value. case text(String) @@ -77,56 +59,6 @@ public struct ModelContent: Codable, Equatable { return .data(mimetype: "image/png", data) } - // MARK: Codable Conformance - - public func encode(to encoder: Encoder) throws { - var container = encoder.container(keyedBy: ModelContent.Part.CodingKeys.self) - switch self { - case let .text(a0): - try container.encode(a0, forKey: .text) - case let .data(mimetype, bytes): - var inlineDataContainer = container.nestedContainer( - keyedBy: InlineDataKeys.self, - forKey: .inlineData - ) - try inlineDataContainer.encode(mimetype, forKey: .mimeType) - try inlineDataContainer.encode(bytes, forKey: .bytes) - case let .fileData(mimetype: mimetype, url): - var fileDataContainer = container.nestedContainer( - keyedBy: FileDataKeys.self, - forKey: .fileData - ) - try fileDataContainer.encode(mimetype, forKey: .mimeType) - try fileDataContainer.encode(url, forKey: .url) - case let .functionCall(functionCall): - try container.encode(functionCall, forKey: .functionCall) - case let .functionResponse(functionResponse): - try container.encode(functionResponse, forKey: .functionResponse) - } - } - - public init(from decoder: Decoder) throws { - let values = try decoder.container(keyedBy: CodingKeys.self) - if values.contains(.text) { - self = try .text(values.decode(String.self, forKey: .text)) - } else if values.contains(.inlineData) { - let dataContainer = try values.nestedContainer( - keyedBy: InlineDataKeys.self, - forKey: .inlineData - ) - let mimetype = try dataContainer.decode(String.self, forKey: .mimeType) - let bytes = try dataContainer.decode(Data.self, forKey: .bytes) - self = .data(mimetype: mimetype, bytes) - } else if values.contains(.functionCall) { - self = try .functionCall(values.decode(FunctionCall.self, forKey: .functionCall)) - } else { - throw DecodingError.dataCorrupted(.init( - codingPath: [CodingKeys.text, CodingKeys.inlineData], - debugDescription: "No text, inline data or function call was found." - )) - } - } - /// Returns the text contents of this ``Part``, if it contains text. public var text: String? { switch self { @@ -179,3 +111,77 @@ public struct ModelContent: Codable, Equatable { self.init(role: role, parts: content) } } + +// MARK: Codable Conformances + +@available(iOS 15.0, macOS 11.0, macCatalyst 15.0, *) +extension ModelContent: Codable {} + +@available(iOS 15.0, macOS 11.0, macCatalyst 15.0, *) +extension ModelContent.Part: Codable { + enum CodingKeys: String, CodingKey { + case text + case inlineData + case fileData + case functionCall + case functionResponse + } + + enum InlineDataKeys: String, CodingKey { + case mimeType = "mime_type" + case bytes = "data" + } + + enum FileDataKeys: String, CodingKey { + case mimeType = "mime_type" + case url = "file_uri" + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + switch self { + case let .text(a0): + try container.encode(a0, forKey: .text) + case let .data(mimetype, bytes): + var inlineDataContainer = container.nestedContainer( + keyedBy: InlineDataKeys.self, + forKey: .inlineData + ) + try inlineDataContainer.encode(mimetype, forKey: .mimeType) + try inlineDataContainer.encode(bytes, forKey: .bytes) + case let .fileData(mimetype: mimetype, url): + var fileDataContainer = container.nestedContainer( + keyedBy: FileDataKeys.self, + forKey: .fileData + ) + try fileDataContainer.encode(mimetype, forKey: .mimeType) + try fileDataContainer.encode(url, forKey: .url) + case let .functionCall(functionCall): + try container.encode(functionCall, forKey: .functionCall) + case let .functionResponse(functionResponse): + try container.encode(functionResponse, forKey: .functionResponse) + } + } + + public init(from decoder: Decoder) throws { + let values = try decoder.container(keyedBy: CodingKeys.self) + if values.contains(.text) { + self = try .text(values.decode(String.self, forKey: .text)) + } else if values.contains(.inlineData) { + let dataContainer = try values.nestedContainer( + keyedBy: InlineDataKeys.self, + forKey: .inlineData + ) + let mimetype = try dataContainer.decode(String.self, forKey: .mimeType) + let bytes = try dataContainer.decode(Data.self, forKey: .bytes) + self = .data(mimetype: mimetype, bytes) + } else if values.contains(.functionCall) { + self = try .functionCall(values.decode(FunctionCall.self, forKey: .functionCall)) + } else { + throw DecodingError.dataCorrupted(.init( + codingPath: [CodingKeys.text, CodingKeys.inlineData], + debugDescription: "No text, inline data or function call was found." + )) + } + } +} diff --git a/Sources/GoogleAI/Safety.swift b/Sources/GoogleAI/Safety.swift index d59511f..d2adc4a 100644 --- a/Sources/GoogleAI/Safety.swift +++ b/Sources/GoogleAI/Safety.swift @@ -18,7 +18,7 @@ import Foundation /// of this type may be assigned to a category for every model-generated response, not just /// responses that exceed a certain threshold. @available(iOS 15.0, macOS 11.0, macCatalyst 15.0, *) -public struct SafetyRating: Decodable, Equatable, Hashable { +public struct SafetyRating: Equatable, Hashable { /// The category describing the potential harm a piece of content may pose. See /// ``SafetySetting/HarmCategory`` for a list of possible values. public let category: SafetySetting.HarmCategory @@ -38,7 +38,7 @@ public struct SafetyRating: Decodable, Equatable, Hashable { /// The probability that a given model output falls under a harmful content category. This does /// not indicate the severity of harm for a piece of content. - public enum HarmProbability: String, Codable { + public enum HarmProbability: String { /// Unknown. A new server value that isn't recognized by the SDK. case unknown = "UNKNOWN" @@ -57,26 +57,12 @@ public struct SafetyRating: Decodable, Equatable, Hashable { /// The probability is high. The content described is very likely harmful. case high = "HIGH" - - /// Initializes a new `SafetyRating` from a decoder. - /// Not for external use. Initializer required for Decodable conformance. - public init(from decoder: Decoder) throws { - let value = try decoder.singleValueContainer().decode(String.self) - guard let decodedProbability = HarmProbability(rawValue: value) else { - Logging.default - .error("[GoogleGenerativeAI] Unrecognized HarmProbability with value \"\(value)\".") - self = .unknown - return - } - - self = decodedProbability - } } } /// Safety feedback for an entire request. @available(iOS 15.0, macOS 11.0, macCatalyst 15.0, *) -public struct SafetyFeedback: Decodable { +public struct SafetyFeedback { /// Safety rating evaluated from content. public let rating: SafetyRating @@ -93,10 +79,10 @@ public struct SafetyFeedback: Decodable { /// A type used to specify a threshold for harmful content, beyond which the model will return a /// fallback response instead of generated content. @available(iOS 15.0, macOS 11.0, macCatalyst 15.0, *) -public struct SafetySetting: Codable { +public struct SafetySetting { /// A type describing safety attributes, which include harmful categories and topics that can /// be considered sensitive. - public enum HarmCategory: String, Codable { + public enum HarmCategory: String { /// Unknown. A new server value that isn't recognized by the SDK. case unknown = "HARM_CATEGORY_UNKNOWN" @@ -114,23 +100,10 @@ public struct SafetySetting: Codable { /// Promotes or enables access to harmful goods, services, or activities. case dangerousContent = "HARM_CATEGORY_DANGEROUS_CONTENT" - - /// Do not explicitly use. Initializer required for Decodable conformance. - public init(from decoder: Decoder) throws { - let value = try decoder.singleValueContainer().decode(String.self) - guard let decodedCategory = HarmCategory(rawValue: value) else { - Logging.default - .error("[GoogleGenerativeAI] Unrecognized HarmCategory with value \"\(value)\".") - self = .unknown - return - } - - self = decodedCategory - } } /// Block at and beyond a specified ``SafetyRating/HarmProbability``. - public enum BlockThreshold: String, Codable { + public enum BlockThreshold: String { /// Unknown. A new server value that isn't recognized by the SDK. case unknown = "UNKNOWN" @@ -148,19 +121,6 @@ public struct SafetySetting: Codable { /// All content will be allowed. case blockNone = "BLOCK_NONE" - - /// Do not explicitly use. Initializer required for Decodable conformance. - public init(from decoder: Decoder) throws { - let value = try decoder.singleValueContainer().decode(String.self) - guard let decodedThreshold = BlockThreshold(rawValue: value) else { - Logging.default - .error("[GoogleGenerativeAI] Unrecognized BlockThreshold with value \"\(value)\".") - self = .unknown - return - } - - self = decodedThreshold - } } enum CodingKeys: String, CodingKey { @@ -180,3 +140,59 @@ public struct SafetySetting: Codable { self.threshold = threshold } } + +// MARK: - Codable Conformances + +@available(iOS 15.0, macOS 11.0, macCatalyst 15.0, *) +extension SafetyRating.HarmProbability: Codable { + public init(from decoder: Decoder) throws { + let value = try decoder.singleValueContainer().decode(String.self) + guard let decodedProbability = SafetyRating.HarmProbability(rawValue: value) else { + Logging.default + .error("[GoogleGenerativeAI] Unrecognized HarmProbability with value \"\(value)\".") + self = .unknown + return + } + + self = decodedProbability + } +} + +@available(iOS 15.0, macOS 11.0, macCatalyst 15.0, *) +extension SafetyRating: Decodable {} + +@available(iOS 15.0, macOS 11.0, macCatalyst 15.0, *) +extension SafetyFeedback: Decodable {} + +@available(iOS 15.0, macOS 11.0, macCatalyst 15.0, *) +extension SafetySetting.HarmCategory: Codable { + public init(from decoder: Decoder) throws { + let value = try decoder.singleValueContainer().decode(String.self) + guard let decodedCategory = SafetySetting.HarmCategory(rawValue: value) else { + Logging.default + .error("[GoogleGenerativeAI] Unrecognized HarmCategory with value \"\(value)\".") + self = .unknown + return + } + + self = decodedCategory + } +} + +@available(iOS 15.0, macOS 11.0, macCatalyst 15.0, *) +extension SafetySetting.BlockThreshold: Codable { + public init(from decoder: Decoder) throws { + let value = try decoder.singleValueContainer().decode(String.self) + guard let decodedThreshold = SafetySetting.BlockThreshold(rawValue: value) else { + Logging.default + .error("[GoogleGenerativeAI] Unrecognized BlockThreshold with value \"\(value)\".") + self = .unknown + return + } + + self = decodedThreshold + } +} + +@available(iOS 15.0, macOS 11.0, macCatalyst 15.0, *) +extension SafetySetting: Codable {}