Skip to content

Instantly share code, notes, and snippets.

@AFutureD
Created April 9, 2025 17:46
Show Gist options
  • Save AFutureD/fc95b06b92f6e61c8ba592aa49aa1ab7 to your computer and use it in GitHub Desktop.
Save AFutureD/fc95b06b92f6e61c8ba592aa49aa1ab7 to your computer and use it in GitHub Desktop.
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