Created
April 9, 2025 17:46
-
-
Save AFutureD/fc95b06b92f6e61c8ba592aa49aa1ab7 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import Foundation | |
// MARK: - Main Request Body | |
/// Represents the request body for creating a chat completion. | |
public struct ChatCompletionRequest: Codable { | |
/// A list of messages comprising the conversation so far. | |
public let messages: [Message] | |
/// Model ID used to generate the response. | |
public let model: String // TODO: using enum | |
/// Parameters for audio output. Required when audio output is requested. | |
public let audio: AudioOutput? | |
/// Defaults to 0. Number between -2.0 and 2.0. Positive values penalize new tokens based on their existing frequency. | |
public let frequencyPenalty: Double? | |
/// Modify the likelihood of specified tokens appearing in the completion. Maps token IDs (strings) to bias values (-100 to 100). | |
public let logitBias: [String: Int]? // WTF? | |
/// Defaults to false. Whether to return log probabilities of the output tokens. | |
public let logprobs: Bool? | |
/// An upper bound for the number of tokens that can be generated for a completion. | |
public let maxCompletionTokens: Int? | |
/// Set of 16 key-value pairs that can be attached to the object. | |
public let metadata: [String: String]? | |
/// Output types requested (e.g., ["text", "audio"]). Defaults to ["text"]. | |
public let modalities: [String]? | |
/// How many chat completion choices to generate for each input message. Defaults to 1. | |
public let n: Int? | |
/// Whether to enable parallel function calling. Defaults to true. | |
public let parallelToolCalls: Bool? | |
/// Configuration for a Predicted Output to improve response times. | |
public let prediction: Prediction? | |
/// Number between -2.0 and 2.0. Positive values penalize new tokens based on whether they appear in the text so far. | |
public let presencePenalty: Double? | |
/// Constrains effort on reasoning for o-series models. (low, medium, high). Defaults to medium. | |
public let reasoningEffort: ReasoningEffort? | |
/// An object specifying the format that the model must output. | |
public let responseFormat: ResponseFormat? | |
/// If specified, attempts to sample deterministically. | |
public let seed: Int? | |
/// Latency tier to use for processing the request (auto, default). | |
public let serviceTier: ServiceTier? | |
/// Up to 4 sequences where the API will stop generating further tokens. Can be a single string or an array of strings. | |
public let stop: Stop? | |
/// Whether to store the output for model distillation or evals. Defaults to false. | |
public let store: Bool? | |
/// If set to true, streams response data using server-sent events. Defaults to false. | |
public let stream: Bool? | |
/// Options for streaming response. Only set when stream is true. | |
public let streamOptions: StreamOptions? | |
/// Sampling temperature (0 to 2). Higher values = more random, lower = more focused. Defaults to 1. | |
public let temperature: Double? | |
/// Controls which (if any) tool is called by the model. | |
public let toolChoice: ToolChoice? | |
/// A list of tools the model may call. Currently, only functions are supported. | |
public let tools: [Tool]? | |
/// Number of most likely tokens to return at each position (0-20). Requires logprobs=true. | |
public let topLogprobs: Int? | |
/// Nucleus sampling parameter (0 to 1). Considers tokens with top_p probability mass. Defaults to 1. | |
public let topP: Double? | |
/// A unique identifier representing your end-user. | |
public let user: String? | |
/// Options for the web search tool. | |
public let webSearchOptions: WebSearchOptions? | |
public init(messages: [Message], model: String, audio: AudioOutput? = nil, frequencyPenalty: Double? = nil, logitBias: [String : Int]? = nil, logprobs: Bool? = nil, maxCompletionTokens: Int? = nil, metadata: [String : String]? = nil, modalities: [String]? = nil, n: Int? = nil, parallelToolCalls: Bool? = nil, prediction: Prediction? = nil, presencePenalty: Double? = nil, reasoningEffort: ReasoningEffort? = nil, responseFormat: ResponseFormat? = nil, seed: Int? = nil, serviceTier: ServiceTier? = nil, stop: Stop? = nil, store: Bool? = nil, stream: Bool? = nil, streamOptions: StreamOptions? = nil, temperature: Double? = nil, toolChoice: ToolChoice? = nil, tools: [Tool]? = nil, topLogprobs: Int? = nil, topP: Double? = nil, user: String? = nil, webSearchOptions: WebSearchOptions? = nil) { | |
self.messages = messages | |
self.model = model | |
self.audio = audio | |
self.frequencyPenalty = frequencyPenalty | |
self.logitBias = logitBias | |
self.logprobs = logprobs | |
self.maxCompletionTokens = maxCompletionTokens | |
self.metadata = metadata | |
self.modalities = modalities | |
self.n = n | |
self.parallelToolCalls = parallelToolCalls | |
self.prediction = prediction | |
self.presencePenalty = presencePenalty | |
self.reasoningEffort = reasoningEffort | |
self.responseFormat = responseFormat | |
self.seed = seed | |
self.serviceTier = serviceTier | |
self.stop = stop | |
self.store = store | |
self.stream = stream | |
self.streamOptions = streamOptions | |
self.temperature = temperature | |
self.toolChoice = toolChoice | |
self.tools = tools | |
self.topLogprobs = topLogprobs | |
self.topP = topP | |
self.user = user | |
self.webSearchOptions = webSearchOptions | |
} | |
// Maps Swift camelCase properties to JSON snake_case keys | |
enum CodingKeys: String, CodingKey { | |
case messages | |
case model | |
case audio | |
case frequencyPenalty = "frequency_penalty" | |
case logitBias = "logit_bias" | |
case logprobs | |
case maxCompletionTokens = "max_completion_tokens" | |
case metadata | |
case modalities | |
case n | |
case parallelToolCalls = "parallel_tool_calls" | |
case prediction | |
case presencePenalty = "presence_penalty" | |
case reasoningEffort = "reasoning_effort" | |
case responseFormat = "response_format" | |
case seed | |
case serviceTier = "service_tier" | |
case stop | |
case store | |
case stream | |
case streamOptions = "stream_options" | |
case temperature | |
case toolChoice = "tool_choice" | |
case tools | |
case topLogprobs = "top_logprobs" | |
case topP = "top_p" | |
case user | |
case webSearchOptions = "web_search_options" | |
} | |
} | |
// MARK: - Message Types | |
/// Represents the role of the message author. | |
public enum MessageRole: String, Codable { | |
case developer | |
case system | |
case user | |
case assistant | |
case tool | |
} | |
/// Represents a single message in the conversation. Uses an enum to handle different message structures based on role. | |
public enum Message: Codable { | |
case developer(DeveloperMessage) | |
case system(SystemMessage) | |
case user(UserMessage) | |
case assistant(AssistantMessage) | |
case tool(ToolMessage) | |
// Custom Codable implementation to handle the different message types | |
public init(from decoder: Decoder) throws { | |
let container = try decoder.container(keyedBy: CodingKeys.self) | |
let role = try container.decode(MessageRole.self, forKey: .role) | |
switch role { | |
case .developer: | |
self = .developer(try DeveloperMessage(from: decoder)) | |
case .system: | |
self = .system(try SystemMessage(from: decoder)) | |
case .user: | |
self = .user(try UserMessage(from: decoder)) | |
case .assistant: | |
self = .assistant(try AssistantMessage(from: decoder)) | |
case .tool: | |
self = .tool(try ToolMessage(from: decoder)) | |
} | |
} | |
public func encode(to encoder: Encoder) throws { | |
var container = encoder.singleValueContainer() | |
switch self { | |
case .developer(let message): | |
try container.encode(message) | |
case .system(let message): | |
try container.encode(message) | |
case .user(let message): | |
try container.encode(message) | |
case .assistant(let message): | |
try container.encode(message) | |
case .tool(let message): | |
try container.encode(message) | |
} | |
} | |
// Used internally for decoding based on role | |
private enum CodingKeys: String, CodingKey { | |
case role | |
} | |
} | |
// --- Specific Message Structs --- | |
public struct DeveloperMessage: Codable { | |
public let role: MessageRole | |
public let content: String // Can technically be array, but docs imply string for developer | |
public let name: String? | |
public init(content: String, name: String? = nil) { | |
self.content = content | |
self.name = name | |
self.role = .developer | |
} | |
} | |
public struct SystemMessage: Codable { | |
public let role: MessageRole | |
public let content: String // Can technically be array, but docs imply string for system | |
public let name: String? | |
public init(content: String, name: String? = nil) { | |
self.content = content | |
self.name = name | |
self.role = .system | |
} | |
} | |
/// Represents content for a user message (either plain text or structured parts). | |
public enum UserMessageContent: Codable { | |
case text(String) | |
case parts([ContentPart]) | |
// Custom Codable | |
public init(from decoder: Decoder) throws { | |
let container = try decoder.singleValueContainer() | |
if let text = try? container.decode(String.self) { | |
self = .text(text) | |
} else if let parts = try? container.decode([ContentPart].self) { | |
self = .parts(parts) | |
} else { | |
throw DecodingError.typeMismatch(UserMessageContent.self, DecodingError.Context(codingPath: decoder.codingPath, debugDescription: "Content must be a String or an array of ContentPart")) | |
} | |
} | |
public func encode(to encoder: Encoder) throws { | |
var container = encoder.singleValueContainer() | |
switch self { | |
case .text(let text): | |
try container.encode(text) | |
case .parts(let parts): | |
try container.encode(parts) | |
} | |
} | |
} | |
public struct UserMessage: Codable { | |
public let role: MessageRole | |
public let content: UserMessageContent | |
public let name: String? | |
public init(content: UserMessageContent, name: String? = nil) { | |
self.content = content | |
self.name = name | |
self.role = .user | |
} | |
// Convenience initializer for simple text content | |
public init(text: String, name: String? = nil) { | |
self.content = .text(text) | |
self.name = name | |
self.role = .user | |
} | |
} | |
/// Represents content for an assistant message (optional text or refusal) | |
public enum AssistantMessageContent: Codable { | |
case text(String) | |
// case parts([AssistantContentPart]) // Doc says array of text or exactly one refusal part | |
// Let's simplify based on common usage: Optional text content. Tool calls/function calls handle non-text actions. | |
// If refusal part is needed, it might be handled differently (e.g., specific error state). | |
// For simplicity, let's assume optional text. If parts are strictly needed, need AssistantContentPart enum. | |
// Custom Codable to handle optional string content | |
public init(from decoder: Decoder) throws { | |
let container = try decoder.singleValueContainer() | |
// Attempt to decode as string, if fails, assume no text content (nil string equivalent) | |
if let text = try? container.decode(String.self) { | |
self = .text(text) | |
} else { | |
// This handles cases where content might be null or an empty object/array if not text | |
// Depending on API behavior, might need refinement | |
throw DecodingError.typeMismatch(AssistantMessageContent.self, DecodingError.Context(codingPath: decoder.codingPath, debugDescription: "Expected String content or structure indicating no text")) | |
} | |
} | |
public func encode(to encoder: Encoder) throws { | |
var container = encoder.singleValueContainer() | |
switch self { | |
case .text(let text): | |
try container.encode(text) | |
} | |
} | |
} | |
public struct AssistantAudio: Codable { | |
public let id: String | |
} | |
public enum AssistantContent: Codable { | |
case text(String) | |
case parts([ContentPart]) | |
// Custom Codable | |
public init(from decoder: Decoder) throws { | |
let container = try decoder.singleValueContainer() | |
if let text = try? container.decode(String.self) { | |
self = .text(text) | |
} else if let parts = try? container.decode([ContentPart].self) { | |
self = .parts(parts) | |
} else { | |
throw DecodingError.typeMismatch(UserMessageContent.self, DecodingError.Context(codingPath: decoder.codingPath, debugDescription: "Content must be a String or an array of ContentPart")) | |
} | |
} | |
public func encode(to encoder: Encoder) throws { | |
var container = encoder.singleValueContainer() | |
switch self { | |
case .text(let text): | |
try container.encode(text) | |
case .parts(let parts): | |
try container.encode(parts) | |
} | |
} | |
} | |
public struct AssistantMessage: Codable { | |
public let role: MessageRole | |
public let audio: AssistantAudio? | |
// The contents of the assistant message. Required unless tool_calls or function_call is specified. | |
public let content: AssistantContent? | |
public let name: String? | |
public let toolCalls: [ToolCall]? | |
enum CodingKeys: String, CodingKey { | |
case role, content, name, audio | |
case toolCalls = "tool_calls" | |
} | |
public init(role: MessageRole, audio: AssistantAudio?, content: AssistantContent?, name: String?, toolCalls: [ToolCall]?) { | |
self.role = .assistant | |
self.audio = audio | |
self.content = content | |
self.name = name | |
self.toolCalls = toolCalls | |
} | |
} | |
public enum ToolMessageContent: Codable { | |
case text(String) | |
case parts([ContentPart]) | |
// Custom Codable | |
public init(from decoder: Decoder) throws { | |
let container = try decoder.singleValueContainer() | |
if let text = try? container.decode(String.self) { | |
self = .text(text) | |
} else if let parts = try? container.decode([ContentPart].self) { | |
self = .parts(parts) | |
} else { | |
throw DecodingError.typeMismatch(UserMessageContent.self, DecodingError.Context(codingPath: decoder.codingPath, debugDescription: "Content must be a String or an array of ContentPart")) | |
} | |
} | |
public func encode(to encoder: Encoder) throws { | |
var container = encoder.singleValueContainer() | |
switch self { | |
case .text(let text): | |
try container.encode(text) | |
case .parts(let parts): | |
try container.encode(parts) | |
} | |
} | |
} | |
public struct ToolMessage: Codable { | |
public let role: MessageRole | |
public let content: ToolMessageContent // Can technically be array, assuming string for tool results | |
public let toolCallId: String | |
enum CodingKeys: String, CodingKey { | |
case role, content | |
case toolCallId = "tool_call_id" | |
} | |
public init(content: ToolMessageContent, toolCallId: String) { | |
self.content = content | |
self.toolCallId = toolCallId | |
self.role = .tool | |
} | |
} | |
// MARK: - Content Parts (for User Messages) | |
/// Represents different types of content parts within a user message. | |
public enum ContentPart: Codable { | |
case text(TextContentPart) | |
case image(ImageContentPart) | |
case audio(AudioContentPart) | |
case file(FileContentPart) | |
case refusal(RefusalContentPart) | |
// Custom Codable implementation | |
public init(from decoder: Decoder) throws { | |
let container = try decoder.container(keyedBy: CodingKeys.self) | |
let type = try container.decode(ContentType.self, forKey: .type) | |
switch type { | |
case .text: | |
self = .text(try TextContentPart(from: decoder)) | |
case .image_url: // Mapped from image type | |
self = .image(try ImageContentPart(from: decoder)) | |
case .input_audio: // Mapped from audio type | |
self = .audio(try AudioContentPart(from: decoder)) | |
case .file: | |
self = .file(try FileContentPart(from: decoder)) | |
case .refusal: | |
self = .refusal(try RefusalContentPart(from: decoder)) | |
} | |
} | |
public func encode(to encoder: Encoder) throws { | |
var container = encoder.singleValueContainer() | |
switch self { | |
case .text(let part): | |
try container.encode(part) | |
case .image(let part): | |
try container.encode(part) | |
case .audio(let part): | |
try container.encode(part) | |
case .file(let part): | |
try container.encode(part) | |
case .refusal(let part): | |
try container.encode(part) | |
} | |
} | |
// Used internally for decoding based on type | |
private enum CodingKeys: String, CodingKey { | |
case type | |
} | |
// Maps JSON type strings to Swift cases | |
private enum ContentType: String, Codable { | |
case text | |
case image_url // JSON uses image_url for image type | |
case input_audio // JSON uses input_audio for audio type | |
case file | |
case refusal | |
} | |
} | |
public struct TextContentPart: Codable { | |
public let type: String | |
public let text: String | |
public init(text: String) { | |
self.text = text | |
self.type = "text" // - Warning: Not Sure | |
} | |
} | |
public enum ImageDetail: String, Codable { | |
case auto, low, high | |
} | |
public enum ImageContent: Codable { | |
case url(String) | |
case base64(String) // TODO: maybe other format. `f"data:image/jpeg;base64,{base64_image}"` | |
public init(from decoder: any Decoder) throws { | |
fatalError() | |
} | |
public func encode(to encoder: any Encoder) throws { | |
var container = encoder.singleValueContainer() | |
switch self { | |
case .url(let a0): | |
try container.encode(a0) | |
case .base64(let a0): | |
try container.encode(a0) | |
} | |
} | |
} | |
public struct ImageURL: Codable { | |
public let url: ImageContent | |
public let detail: ImageDetail? | |
public init(url: ImageContent, detail: ImageDetail? = .auto) { | |
self.url = url | |
self.detail = detail | |
} | |
} | |
/// https://platform.openai.com/docs/guides/images?api-mode=chat&format=base64-encoded | |
public struct ImageContentPart: Codable { | |
public let type: String | |
public let imageUrl: ImageURL | |
enum CodingKeys: String, CodingKey { | |
case type | |
case imageUrl = "image_url" | |
} | |
public init(imageUrl: ImageURL) { | |
self.imageUrl = imageUrl | |
self.type = "image_url" | |
} | |
} | |
public enum AudioDataFormat: String, Codable { | |
case wav, mp3 // Add others if supported by API | |
} | |
public struct InputAudio: Codable { | |
public let data: String // Base64 encoded audio data | |
public let format: AudioDataFormat | |
public init(data: String, format: AudioDataFormat) { | |
self.data = data | |
self.format = format | |
} | |
} | |
/// https://platform.openai.com/docs/guides/audio | |
public struct AudioContentPart: Codable { | |
public let type: String | |
public let inputAudio: InputAudio | |
enum CodingKeys: String, CodingKey { | |
case type | |
case inputAudio = "input_audio" | |
} | |
public init(inputAudio: InputAudio) { | |
self.inputAudio = inputAudio | |
self.type = "input_audio" | |
} | |
} | |
/// https://platform.openai.com/docs/guides/pdf-files?api-mode=chat | |
public struct FileDetail: Codable { | |
public let fileId: String? | |
public let filename: String? | |
public let fileData: String? | |
enum CodingKeys: String, CodingKey { | |
case fileId = "file_id" | |
case filename | |
case fileData = "file_data" | |
} | |
public init(fileId: String?, filename: String?, fileData: String?) { | |
self.fileId = fileId | |
self.filename = filename | |
self.fileData = fileData | |
} | |
} | |
public struct FileContentPart: Codable { | |
public let type: String | |
public let file: FileDetail // Adjust FileDetail based on actual API spec | |
public init(file: FileDetail) { | |
self.file = file | |
self.type = "file" | |
} | |
} | |
public struct RefusalContentPart: Codable { | |
public let type: String | |
public let refusal: String | |
public init(refusal: String) { | |
self.refusal = refusal | |
self.type = "refusal" | |
} | |
} | |
// MARK: - Tool Calls (Assistant Message) | |
/// Represents a tool call made by the assistant. Currently only function calls are supported. | |
public struct ToolCall: Codable { | |
/// The ID of the tool call. | |
public let id: String | |
/// The type of the tool. Currently, only "function" is supported. | |
public let type: String | |
/// The function that the model called. | |
public let function: CalledFunction | |
public init(id: String, function: CalledFunction) { | |
self.id = id | |
self.function = function | |
self.type = "function" // Hardcoded as only 'function' is supported | |
} | |
} | |
/// Represents the function called by the model within a ToolCall. | |
public struct CalledFunction: Codable { | |
/// The name of the function to call. | |
public let name: String | |
/// The arguments to call the function with, as a JSON format string. | |
public let arguments: String // Model generates JSON string | |
public init(name: String, arguments: String) { | |
self.name = name | |
self.arguments = arguments | |
} | |
} | |
public struct NamedFunction: Codable { | |
public let name: String | |
public init(name: String) { | |
self.name = name | |
} | |
} | |
/// Deprecated: Represents the function call generated by the model (used in AssistantMessage). | |
@available(*, deprecated, message: "Use ToolCall/CalledFunction instead.") | |
public struct FunctionCall: Codable { | |
public let name: String | |
public let arguments: String // JSON string | |
public init(name: String, arguments: String) { | |
self.name = name | |
self.arguments = arguments | |
} | |
} | |
/// Deprecated: Describes a function available to the model. | |
@available(*, deprecated, message: "Use Tool/ToolFunction instead.") | |
public struct FunctionDescription: Codable { | |
public let name: String | |
public let description: String? | |
/// JSON Schema object describing parameters. Represented as [String: Any] for flexibility. | |
/// Consider using a dedicated JSON Schema library for more robustness. | |
public let parameters: [String: AnyCodable]? // Using AnyCodable wrapper | |
public init(name: String, description: String? = nil, parameters: [String: AnyCodable]? = nil) { | |
self.name = name | |
self.description = description | |
self.parameters = parameters | |
} | |
} | |
// MARK: - Tools | |
/// Represents a tool choice (string 'none', 'auto', 'required' or specific tool). | |
public enum ToolChoice: Codable { | |
case none | |
case auto | |
case required | |
case tool(SpecificToolChoice) | |
public init(from decoder: Decoder) throws { | |
let container = try decoder.singleValueContainer() | |
if let strValue = try? container.decode(String.self) { | |
switch strValue { | |
case "none": self = .none | |
case "auto": self = .auto | |
case "required": self = .required | |
default: throw DecodingError.dataCorruptedError(in: container, debugDescription: "Invalid string value for ToolChoice") | |
} | |
} else if let objValue = try? container.decode(SpecificToolChoice.self) { | |
self = .tool(objValue) | |
} else { | |
throw DecodingError.typeMismatch(ToolChoice.self, DecodingError.Context(codingPath: decoder.codingPath, debugDescription: "Expected 'none', 'auto', 'required', or a tool object")) | |
} | |
} | |
public func encode(to encoder: Encoder) throws { | |
var container = encoder.singleValueContainer() | |
switch self { | |
case .none: try container.encode("none") | |
case .auto: try container.encode("auto") | |
case .required: try container.encode("required") | |
case .tool(let toolChoice): try container.encode(toolChoice) | |
} | |
} | |
} | |
/// Represents a choice to call a specific tool (currently only function). | |
public struct SpecificToolChoice: Codable { | |
public let type: String | |
public let function: NamedFunction // Reusing NamedFunction structure | |
public init(function: NamedFunction) { | |
self.function = function | |
self.type = "function" // Hardcoded as only 'function' is supported | |
} | |
} | |
/// Represents a tool available to the model. Currently only functions are supported. | |
public struct Tool: Codable { | |
public let type: String | |
public let function: ToolFunction | |
public init(function: ToolFunction) { | |
self.function = function | |
self.type = "function" // Hardcoded as only 'function' is supported | |
} | |
} | |
/// Describes a function tool available to the model. | |
/// | |
/// https://platform.openai.com/docs/guides/function-calling?api-mode=chat | |
/// https://json-schema.org/understanding-json-schema/reference | |
public struct ToolFunction: Codable { | |
public let name: String | |
public let description: String? | |
public let parameters: [String: AnyCodable]? // TODO: using other thing | |
// https://platform.openai.com/docs/guides/structured-outputs?api-mode=responses | |
public let strict: Bool? | |
public init(name: String, description: String? = nil, parameters: [String : AnyCodable]? = nil, strict: Bool? = nil) { | |
self.name = name | |
self.description = description | |
self.parameters = parameters | |
self.strict = strict | |
} | |
} | |
// MARK: - Other Supporting Structures | |
/// Represents the format/voice for audio output. | |
public struct AudioOutput: Codable { | |
/// Output audio format (wav, mp3, flac, opus, pcm16). | |
public let format: AudioOutputFormat | |
/// Voice to use (alloy, ash, ballad, coral, echo, sage, shimmer). | |
public let voice: AudioVoice | |
public init(format: AudioOutputFormat, voice: AudioVoice) { | |
self.format = format | |
self.voice = voice | |
} | |
} | |
public enum AudioOutputFormat: String, Codable { | |
case wav, mp3, flac, opus, pcm16 | |
} | |
public enum AudioVoice: String, Codable { | |
case alloy, ash, ballad, coral, echo, sage, shimmer | |
} | |
/// Represents the prediction configuration. Currently only StaticContent shown. | |
public struct Prediction: Codable { | |
public let content: StaticPredictionContent | |
public let type: String | |
// Add other prediction types if they exist | |
public init(content: StaticPredictionContent) { | |
self.content = content | |
self.type = "content" | |
} | |
} | |
/// Static content for prediction. | |
public enum StaticPredictionContent: Codable { | |
case text(String) | |
case parts([ContentPart]) | |
// Custom Codable | |
public init(from decoder: Decoder) throws { | |
let container = try decoder.singleValueContainer() | |
if let text = try? container.decode(String.self) { | |
self = .text(text) | |
} else if let parts = try? container.decode([ContentPart].self) { | |
self = .parts(parts) | |
} else { | |
throw DecodingError.typeMismatch(UserMessageContent.self, DecodingError.Context(codingPath: decoder.codingPath, debugDescription: "Content must be a String or an array of ContentPart")) | |
} | |
} | |
public func encode(to encoder: Encoder) throws { | |
var container = encoder.singleValueContainer() | |
switch self { | |
case .text(let text): | |
try container.encode(text) | |
case .parts(let parts): | |
try container.encode(parts) | |
} | |
} | |
} | |
public enum ReasoningEffort: String, Codable { | |
case low, medium, high | |
} | |
/// Specifies the desired response format. | |
public enum ResponseFormat: Codable { | |
case text(TextResponseFormat) | |
case jsonSchema(JSONSchemaResponseFormat) | |
case jsonObject(JSONObjectResponseFormat) | |
// Custom Codable | |
public init(from decoder: Decoder) throws { | |
let container = try decoder.container(keyedBy: CodingKeys.self) | |
let type = try container.decode(FormatType.self, forKey: .type) | |
switch type { | |
case .text: | |
// Text format might just be implicit or have 'text' type but no other fields | |
// Let's assume a simple struct for consistency | |
self = .text(try TextResponseFormat(from: decoder)) | |
case .json_schema: | |
self = .jsonSchema(try JSONSchemaResponseFormat(from: decoder)) | |
case .json_object: | |
self = .jsonObject(try JSONObjectResponseFormat(from: decoder)) | |
} | |
} | |
public func encode(to encoder: Encoder) throws { | |
var container = encoder.singleValueContainer() | |
switch self { | |
case .text(let format): | |
try container.encode(format) | |
case .jsonSchema(let format): | |
try container.encode(format) | |
case .jsonObject(let format): | |
try container.encode(format) | |
} | |
} | |
private enum CodingKeys: String, CodingKey { | |
case type | |
} | |
private enum FormatType: String, Codable { | |
case text | |
case json_schema | |
case json_object | |
} | |
} | |
public struct TextResponseFormat: Codable { | |
public let type: String | |
public init() { | |
self.type = "text" | |
} | |
} | |
public struct JSONSchemaResponseFormat: Codable { | |
public let type: String = "json_schema" | |
public let jsonSchema: JSONSchemaDefinition | |
enum CodingKeys: String, CodingKey { | |
case type | |
case jsonSchema = "json_schema" | |
} | |
public init(jsonSchema: JSONSchemaDefinition) { | |
self.jsonSchema = jsonSchema | |
} | |
} | |
public struct JSONSchemaDefinition: Codable { | |
public let name: String | |
public let description: String? | |
public let schema: [String: AnyCodable]? // TODO: Using other thing. | |
public let strict: Bool? | |
public init(name: String, description: String? = nil, schema: [String : AnyCodable]? = nil, strict: Bool? = nil) { | |
self.name = name | |
self.description = description | |
self.schema = schema | |
self.strict = strict | |
} | |
} | |
public struct JSONObjectResponseFormat: Codable { | |
public let type: String | |
public init() { | |
self.type = "json_object" | |
} | |
} | |
public enum ServiceTier: String, Codable { | |
case auto, `default` | |
} | |
/// Represents the stop parameter, which can be a single string or an array of strings. | |
public enum Stop: Codable { | |
case single(String) | |
case multiple([String]) | |
public init(from decoder: Decoder) throws { | |
let container = try decoder.singleValueContainer() | |
if let strValue = try? container.decode(String.self) { | |
self = .single(strValue) | |
} else if let arrValue = try? container.decode([String].self) { | |
// Ensure array has 1 to 4 elements as per docs | |
guard (1...4).contains(arrValue.count) else { | |
throw DecodingError.dataCorruptedError(in: container, debugDescription: "Stop array must contain between 1 and 4 sequences.") | |
} | |
self = .multiple(arrValue) | |
} else { | |
throw DecodingError.typeMismatch(Stop.self, DecodingError.Context(codingPath: decoder.codingPath, debugDescription: "Expected a String or an array of Strings for stop sequences")) | |
} | |
} | |
public func encode(to encoder: Encoder) throws { | |
var container = encoder.singleValueContainer() | |
switch self { | |
case .single(let value): | |
try container.encode(value) | |
case .multiple(let values): | |
try container.encode(values) | |
} | |
} | |
} | |
/// Options for streaming responses. | |
public struct StreamOptions: Codable { | |
public let includeUsage: Bool? | |
enum CodingKeys: String, CodingKey { | |
case includeUsage = "include_usage" | |
} | |
public init(includeUsage: Bool? = nil) { | |
self.includeUsage = includeUsage | |
} | |
} | |
/// Options for web search tool. | |
public struct WebSearchOptions: Codable { | |
public let searchContextSize: SearchContextSize? | |
public let userLocation: UserLocation? | |
enum CodingKeys: String, CodingKey { | |
case searchContextSize = "search_context_size" | |
case userLocation = "user_location" | |
} | |
public init(searchContextSize: SearchContextSize? = .medium, userLocation: UserLocation? = nil) { | |
self.searchContextSize = searchContextSize | |
self.userLocation = userLocation | |
} | |
} | |
public enum SearchContextSize: String, Codable { | |
case low, medium, high | |
} | |
public struct UserLocation: Codable { | |
public let type: String | |
public let approximate: ApproximateLocation? // Only approximate shown in docs | |
public init(approximate: ApproximateLocation?) { | |
self.approximate = approximate | |
self.type = "approximate" | |
} | |
} | |
public struct ApproximateLocation: Codable { | |
public let city: String? // Free text input for the city of the user, | |
public let country: String? // https://en.wikipedia.org/wiki/ISO_3166-1 | |
public let region: String? // Free text input for the region of the user | |
public let timezone: String? // https://timeapi.io/documentation/iana-timezones | |
public init(city: String? = nil, country: String? = nil, region: String? = nil, timezone: String? = nil) { | |
self.city = city | |
self.country = country | |
self.region = region | |
self.timezone = timezone | |
} | |
} | |
// MARK: - AnyCodable Helper | |
/// A type-erased wrapper for encoding/decoding heterogeneous dictionary values ([String: Any]). | |
/// Use this for fields like 'parameters' or 'schema' which expect flexible JSON objects. | |
public struct AnyCodable: Codable { | |
public let value: Any | |
public init(_ value: Any) { | |
self.value = value | |
} | |
public init(from decoder: Decoder) throws { | |
let container = try decoder.singleValueContainer() | |
if let intValue = try? container.decode(Int.self) { | |
value = intValue | |
} else if let doubleValue = try? container.decode(Double.self) { | |
value = doubleValue | |
} else if let boolValue = try? container.decode(Bool.self) { | |
value = boolValue | |
} else if let stringValue = try? container.decode(String.self) { | |
value = stringValue | |
} else if let arrayValue = try? container.decode([AnyCodable].self) { | |
value = arrayValue.map { $0.value } | |
} else if let dictionaryValue = try? container.decode([String: AnyCodable].self) { | |
value = dictionaryValue.mapValues { $0.value } | |
} else if container.decodeNil() { | |
// Technically JSON null, represent as NSNull or a custom nil marker if needed | |
throw DecodingError.dataCorruptedError(in: container, debugDescription: "Cannot decode nil value into AnyCodable directly") | |
} | |
else { | |
throw DecodingError.dataCorruptedError(in: container, debugDescription: "Unsupported type encountered") | |
} | |
} | |
public func encode(to encoder: Encoder) throws { | |
var container = encoder.singleValueContainer() | |
if let intValue = value as? Int { | |
try container.encode(intValue) | |
} else if let doubleValue = value as? Double { | |
try container.encode(doubleValue) | |
} else if let boolValue = value as? Bool { | |
try container.encode(boolValue) | |
} else if let stringValue = value as? String { | |
try container.encode(stringValue) | |
} else if let arrayValue = value as? [Any] { | |
try container.encode(arrayValue.map { AnyCodable($0) }) | |
} else if let dictionaryValue = value as? [String: Any] { | |
try container.encode(dictionaryValue.mapValues { AnyCodable($0) }) | |
} else if value is NSNull { | |
try container.encodeNil() | |
} | |
else { | |
// Handle potential custom objects or throw error | |
throw EncodingError.invalidValue(value, EncodingError.Context(codingPath: container.codingPath, debugDescription: "Unsupported type for AnyCodable encoding")) | |
} | |
} | |
} | |
// Extend Dictionary to work smoothly with AnyCodable for parameters/schema | |
extension Dictionary where Key == String, Value == Any { | |
func mapToAnyCodable() -> [String: AnyCodable] { | |
return self.mapValues { AnyCodable($0) } | |
} | |
} | |
extension Dictionary where Key == String, Value == AnyCodable { | |
func mapToAny() -> [String: Any] { | |
return self.mapValues { $0.value } | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment