├── .spi.yml ├── src ├── Support │ ├── helpers.swift │ ├── MultiPartData.swift │ └── APIClient.swift ├── Extensions │ ├── URL+appendOptionalQueryItems.swift │ ├── Collection+safeAccess.swift │ ├── Result+async.swift │ └── JSONEncoder+string.swift ├── Macros │ ├── SchemaMacro.swift │ ├── ArraySchemaMacro.swift │ ├── StringSchemaMacro.swift │ ├── NumberSchemaMacro.swift │ └── ToolMacro.swift ├── Protocol │ ├── Schemable.swift │ └── Toolable.swift ├── Models │ ├── APIConversation.swift │ ├── WebhookEvent.swift │ ├── File.swift │ ├── Message.swift │ ├── Model.swift │ ├── Config.swift │ ├── JSONSchema.swift │ ├── Request.swift │ ├── Input.swift │ ├── Response.swift │ └── Event.swift ├── ConversationsAPI.swift └── ResponsesAPI.swift ├── .gitignore ├── macros ├── Extensions │ ├── String+isPlaceholder.swift │ ├── DeclGroupSyntax+accessLevel.swift │ ├── MemberBlockItemListSyntax+declaresVariable.swift │ ├── PatternBindingSyntax+isStored.swift │ ├── ExtensionDeclSyntax+init.swift │ ├── AttributeListSyntax+arguments.swift │ ├── SyntaxProtocol+docString.swift │ └── TypeSyntax+typeInfo.swift ├── Plugin.swift ├── Macros │ ├── ArraySchemaMacro.swift │ ├── NumberSchemaMacro.swift │ ├── StringSchemaMacro.swift │ ├── SchemaMacro.swift │ └── ToolMacro.swift ├── Support │ ├── helpers.swift │ ├── DiagnosticError.swift │ └── DocString.swift └── Generators │ ├── SchemaGenerator.swift │ ├── EnumSchemaGenerator.swift │ └── StructSchemaGenerator.swift ├── LICENSE ├── Package.swift ├── .github └── workflows │ ├── docs.yml │ └── test.yml ├── README.md └── tests ├── SchemaMacroTests.swift └── ToolMacroTests.swift /.spi.yml: -------------------------------------------------------------------------------- 1 | version: 1 2 | builder: 3 | configs: 4 | - documentation_targets: [OpenAI] 5 | -------------------------------------------------------------------------------- /src/Support/helpers.swift: -------------------------------------------------------------------------------- 1 | func tap(_ value: T, _ closure: (inout T) -> Void) -> T { 2 | var value = value 3 | closure(&value) 4 | return value 5 | } 6 | -------------------------------------------------------------------------------- /src/Extensions/URL+appendOptionalQueryItems.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | extension URL { 4 | mutating func append(queryItems: [URLQueryItem?]) { 5 | append(queryItems: queryItems.compactMap { $0 }) 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/Extensions/Collection+safeAccess.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | extension Collection { 4 | subscript(safe index: Index) -> Element? { 5 | guard indices.contains(index) else { return nil } 6 | return self[index] 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .netrc 2 | /.build 3 | .DS_Store 4 | /Packages 5 | *.xcarchive 6 | xcuserdata/ 7 | DerivedData/ 8 | *.xcframework 9 | Package.resolved 10 | .swiftpm/configuration/registries.json 11 | .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata 12 | -------------------------------------------------------------------------------- /src/Extensions/Result+async.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | extension Result { 4 | init(catching body: () async throws(Failure) -> Success) async { 5 | do { 6 | let value = try await body() 7 | self = .success(value) 8 | } catch { self = .failure(error) } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /macros/Extensions/String+isPlaceholder.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | nonisolated(unsafe) let placeholderPattern = /\<\#[\w ]*\#\>/ 4 | 5 | extension String { 6 | var isPlaceholder: Bool { 7 | trimmingCharacters(in: .whitespacesAndNewlines).contains(placeholderPattern) 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /macros/Extensions/DeclGroupSyntax+accessLevel.swift: -------------------------------------------------------------------------------- 1 | import SwiftSyntax 2 | 3 | extension DeclGroupSyntax { 4 | var accessLabel: String? { 5 | modifiers.first { modifier in 6 | ["public", "internal", "package", "fileprivate", "private"].contains(modifier.name.text) 7 | }?.name.text 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /macros/Plugin.swift: -------------------------------------------------------------------------------- 1 | import SwiftSyntaxMacros 2 | import SwiftCompilerPlugin 3 | 4 | @main struct Plugin: CompilerPlugin { 5 | let providingMacros: [Macro.Type] = [ 6 | ToolMacro.self, 7 | SchemaMacro.self, 8 | ArraySchemaMacro.self, 9 | StringSchemaMacro.self, 10 | NumberSchemaMacro.self, 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /src/Macros/SchemaMacro.swift: -------------------------------------------------------------------------------- 1 | /// Automatically generate a JSON Schema for a Swift type. 2 | /// 3 | /// This macro conforms the type to the `Schemable` protocol 4 | @attached(extension, conformances: Schemable, names: named(schema)) 5 | public macro Schema() = #externalMacro( 6 | module: "Macros", type: "SchemaMacro" 7 | ) 8 | -------------------------------------------------------------------------------- /macros/Macros/ArraySchemaMacro.swift: -------------------------------------------------------------------------------- 1 | import SwiftSyntax 2 | import SwiftSyntaxMacros 3 | 4 | public struct ArraySchemaMacro: PeerMacro { 5 | public static func expansion( 6 | of _: AttributeSyntax, 7 | providingPeersOf _: some DeclSyntaxProtocol, 8 | in _: some MacroExpansionContext 9 | ) throws -> [DeclSyntax] { [] } 10 | } 11 | -------------------------------------------------------------------------------- /macros/Macros/NumberSchemaMacro.swift: -------------------------------------------------------------------------------- 1 | import SwiftSyntax 2 | import SwiftSyntaxMacros 3 | 4 | public struct NumberSchemaMacro: PeerMacro { 5 | public static func expansion( 6 | of _: AttributeSyntax, 7 | providingPeersOf _: some DeclSyntaxProtocol, 8 | in _: some MacroExpansionContext 9 | ) throws -> [DeclSyntax] { [] } 10 | } 11 | -------------------------------------------------------------------------------- /macros/Macros/StringSchemaMacro.swift: -------------------------------------------------------------------------------- 1 | import SwiftSyntax 2 | import SwiftSyntaxMacros 3 | 4 | public struct StringSchemaMacro: PeerMacro { 5 | public static func expansion( 6 | of _: AttributeSyntax, 7 | providingPeersOf _: some DeclSyntaxProtocol, 8 | in _: some MacroExpansionContext 9 | ) throws -> [DeclSyntax] { [] } 10 | } 11 | -------------------------------------------------------------------------------- /macros/Support/helpers.swift: -------------------------------------------------------------------------------- 1 | func tap(_ value: T, _ closure: (inout T) throws(E) -> Void) throws(E) -> T { 2 | var mutableValue = value 3 | try closure(&mutableValue) 4 | return mutableValue 5 | } 6 | 7 | extension Sequence { 8 | func map(tapping: (inout Element) -> Void) -> [Element] { 9 | map { element in tap(element, tapping) } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/Macros/ArraySchemaMacro.swift: -------------------------------------------------------------------------------- 1 | /// Customize the auto-generated JSON schema for an array property. 2 | /// 3 | /// - Parameter minItems: The minimum number of items in the array. 4 | /// - Parameter maxItems: The maximum number of items in the array. 5 | @attached(peer) 6 | public macro ArraySchema( 7 | minItems: Int? = nil, 8 | maxItems: Int? = nil 9 | ) = #externalMacro(module: "Macros", type: "ArraySchemaMacro") 10 | -------------------------------------------------------------------------------- /src/Extensions/JSONEncoder+string.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | extension JSONEncoder { 4 | func encodeToString(_ value: T) throws -> String { 5 | let data = try encode(value) 6 | 7 | guard let string = String(data: data, encoding: .utf8) else { 8 | throw EncodingError.invalidValue(value, EncodingError.Context(codingPath: [], debugDescription: "Unable to convert data to string")) 9 | } 10 | 11 | return string 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/Macros/StringSchemaMacro.swift: -------------------------------------------------------------------------------- 1 | /// Customize the auto-generated JSON schema for a string property. 2 | /// 3 | /// - Parameter pattern: A regular expression pattern that the string must match. 4 | /// - Parameter format: An optional format for the string, such as email or date. 5 | @attached(peer) 6 | public macro StringSchema( 7 | pattern: String? = nil, 8 | format: JSONSchema.StringFormat? = nil 9 | ) = #externalMacro(module: "Macros", type: "StringSchemaMacro") 10 | -------------------------------------------------------------------------------- /macros/Extensions/MemberBlockItemListSyntax+declaresVariable.swift: -------------------------------------------------------------------------------- 1 | import SwiftSyntax 2 | 3 | extension MemberBlockItemListSyntax { 4 | /// Checks if the member block contains a variable declaration with the specified name. 5 | func declaresVariable(named name: String) -> Bool { 6 | contains(where: { member in 7 | member.decl.as(VariableDeclSyntax.self)?.bindings.contains(where: { binding in 8 | binding.pattern.as(IdentifierPatternSyntax.self)?.identifier.text == name 9 | }) ?? false 10 | }) 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /macros/Extensions/PatternBindingSyntax+isStored.swift: -------------------------------------------------------------------------------- 1 | import SwiftSyntax 2 | 3 | extension PatternBindingSyntax { 4 | var isStored: Bool { 5 | switch accessorBlock?.accessors { 6 | case nil: return true 7 | case .getter: return false 8 | case let .accessors(accessors): 9 | for accessor in accessors { 10 | switch accessor.accessorSpecifier.tokenKind { 11 | case .keyword(.willSet), .keyword(.didSet): break 12 | default: return false 13 | } 14 | } 15 | return true 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /macros/Extensions/ExtensionDeclSyntax+init.swift: -------------------------------------------------------------------------------- 1 | import SwiftSyntax 2 | import SwiftSyntaxBuilder 3 | 4 | extension ExtensionDeclSyntax { 5 | init(extending extendedType: TypeSyntaxProtocol, inheritsTypes inheritedTypes: [TokenSyntax] = [], @MemberBlockItemListBuilder _ itemsBuilder: () throws -> MemberBlockItemListSyntax) rethrows { 6 | try self.init( 7 | extendedType: extendedType, 8 | inheritanceClause: InheritanceClauseSyntax { 9 | inheritedTypes.map { type in 10 | InheritedTypeSyntax(type: IdentifierTypeSyntax(name: type)) 11 | } 12 | } 13 | ) { 14 | try MemberBlockItemListSyntax(itemsBuilder: itemsBuilder) 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /macros/Generators/SchemaGenerator.swift: -------------------------------------------------------------------------------- 1 | import SwiftSyntax 2 | 3 | enum SchemaGenerator { 4 | static func `for`(_ declaration: some DeclGroupSyntax) throws -> VariableDeclSyntax { 5 | if let structDecl = declaration.as(StructDeclSyntax.self) { 6 | let generator = StructSchemaGenerator(fromStruct: structDecl) 7 | 8 | return try generator.makeSchema() 9 | } 10 | 11 | if let enumDecl = declaration.as(EnumDeclSyntax.self) { 12 | let generator = EnumSchemaGenerator(fromEnum: enumDecl) 13 | 14 | return try generator.makeSchema() 15 | } 16 | 17 | throw ReportableError(errorMessage: "The @Schema macro can only be applied to structs or enums.") 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /macros/Extensions/AttributeListSyntax+arguments.swift: -------------------------------------------------------------------------------- 1 | import SwiftSyntax 2 | 3 | extension AttributeListSyntax { 4 | func arguments(for attributeName: String) -> LabeledExprListSyntax? { 5 | guard let argumentList = compactMap({ $0.as(AttributeSyntax.self) }) 6 | .first(where: { 7 | if let attributeIdentifier = $0.attributeName.as(IdentifierTypeSyntax.self) { 8 | return attributeIdentifier.name.text == attributeName 9 | } 10 | 11 | return false 12 | })? 13 | .arguments? 14 | .as(LabeledExprListSyntax.self) else { return nil } 15 | 16 | return LabeledExprListSyntax(argumentList.map { 17 | $0.with(\.trailingComma, .commaToken()) 18 | }) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /macros/Macros/SchemaMacro.swift: -------------------------------------------------------------------------------- 1 | import SwiftSyntax 2 | import SwiftSyntaxMacros 3 | 4 | public struct SchemaMacro: ExtensionMacro { 5 | public static func expansion( 6 | of _: AttributeSyntax, 7 | attachedTo declaration: some DeclGroupSyntax, 8 | providingExtensionsOf type: some TypeSyntaxProtocol, 9 | conformingTo _: [TypeSyntax], 10 | in context: some MacroExpansionContext 11 | ) throws -> [ExtensionDeclSyntax] { 12 | return try ReportableError.report(in: context, for: declaration) { 13 | try [ 14 | ExtensionDeclSyntax(extending: type, inheritsTypes: ["Schemable"]) { 15 | try SchemaGenerator.for(declaration) 16 | }, 17 | ] 18 | } withDefault: { [] } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/Macros/NumberSchemaMacro.swift: -------------------------------------------------------------------------------- 1 | /// Customize the auto-generated JSON schema for a number or integer property. 2 | /// 3 | /// - Parameter multipleOf: The number must be a multiple of this value. 4 | /// - Parameter minimum: The number must be greater than or equal to this value. 5 | /// - Parameter exclusiveMinimum: The number must be greater than this value. 6 | /// - Parameter maximum: The number must be less than or equal to this value. 7 | /// - Parameter exclusiveMaximum: The number must be less than this value. 8 | @attached(peer) 9 | public macro NumberSchema( 10 | multipleOf: Int? = nil, 11 | minimum: Int? = nil, 12 | exclusiveMinimum: Int? = nil, 13 | maximum: Int? = nil, 14 | exclusiveMaximum: Int? = nil 15 | ) = #externalMacro(module: "Macros", type: "NumberSchemaMacro") 16 | -------------------------------------------------------------------------------- /macros/Support/DiagnosticError.swift: -------------------------------------------------------------------------------- 1 | import SwiftSyntax 2 | import SwiftDiagnostics 3 | import SwiftSyntaxMacros 4 | 5 | struct ReportableError: Error { 6 | private let fixIts: [FixIt] 7 | private let node: SyntaxProtocol? 8 | private let message: MacroExpansionErrorMessage 9 | 10 | init(node: SyntaxProtocol? = nil, errorMessage message: String, fixIts: [FixIt] = []) { 11 | self.node = node 12 | self.fixIts = fixIts 13 | self.message = MacroExpansionErrorMessage(message) 14 | } 15 | 16 | static func report(in context: some MacroExpansionContext, for node: SyntaxProtocol, _ body: () throws -> T, withDefault: () -> T) throws -> T { 17 | do { 18 | return try body() 19 | } catch let error as ReportableError { 20 | let errorNode = error.node ?? node 21 | context.diagnose(Diagnostic(node: errorNode, message: error.message, fixIts: error.fixIts)) 22 | return withDefault() 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/Macros/ToolMacro.swift: -------------------------------------------------------------------------------- 1 | /// Automatically generate a Tool from a struct with a `call` method. 2 | /// 3 | /// When declaring your tool, make sure you specify documentation for both 4 | /// the function (explaining what the tool does) and each parameter 5 | /// (explaining what the parameter is for), like so: 6 | /// 7 | /// ```swift 8 | /// @Tool 9 | /// struct GetWeather { 10 | /// /// Get the weather for a location. 11 | /// /// - Parameter location: The location to get the weather for. 12 | /// func call(location: String) -> String { 13 | /// "Sunny in \(location)" 14 | /// } 15 | /// } 16 | /// ``` 17 | /// 18 | /// This macro conforms the type to the `Toolable` protocol 19 | @attached( 20 | extension, 21 | conformances: Toolable, 22 | names: named(name), named(description), named(Arguments), named(Error), named(Output), named(call) 23 | ) 24 | public macro Tool() = #externalMacro( 25 | module: "Macros", type: "ToolMacro" 26 | ) 27 | -------------------------------------------------------------------------------- /src/Protocol/Schemable.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public protocol Schemable { 4 | static var schema: JSONSchema { get } 5 | } 6 | 7 | public extension Schemable { 8 | static func schema(description: String? = nil) -> JSONSchema { 9 | schema.withDescription(description ?? schema.description) 10 | } 11 | } 12 | 13 | extension UUID: Schemable { 14 | public static var schema: JSONSchema { 15 | .string(format: .uuid) 16 | } 17 | } 18 | 19 | /// > Note: This requires `JSONDecoder` to be configured with a date decoding strategy of `.iso8601`. 20 | extension Date: Schemable { 21 | public static var schema: JSONSchema { 22 | .string(format: .dateTime) 23 | } 24 | } 25 | 26 | public struct NullableVoid: Schemable, Encodable, Sendable { 27 | public static var schema: JSONSchema { 28 | .null(description: nil) 29 | } 30 | 31 | public init() {} 32 | 33 | public func encode(to encoder: Encoder) throws { 34 | var container = encoder.singleValueContainer() 35 | try container.encodeNil() 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/Models/APIConversation.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import MetaCodable 3 | import HelperCoders 4 | 5 | @Codable @CodingKeys(.snake_case) public struct APIConversation: Equatable, Hashable, Sendable { 6 | /// The time at which the conversation was created, measured in seconds since the Unix epoch. 7 | @CodedBy(Since1970DateCoder()) 8 | public var createdAt: Date 9 | 10 | /// The unique ID of the conversation. 11 | public var id: String 12 | 13 | /// Set of 16 key-value pairs that can be attached to an object. 14 | /// 15 | /// This can be useful for storing additional information about the object in a structured format, and querying for objects via API or the dashboard. 16 | /// 17 | /// Keys are strings with a maximum length of 64 characters, and values are strings with a maximum length of 512 characters. 18 | public var metadata: [String: String]? 19 | 20 | public init(id: String, createdAt: Date, metadata: [String: String]? = nil) { 21 | self.createdAt = createdAt 22 | self.id = id 23 | self.metadata = metadata 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /macros/Extensions/SyntaxProtocol+docString.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import SwiftSyntax 3 | 4 | extension SyntaxProtocol { 5 | var docString: String? { 6 | var docStringLines: [String] = [] 7 | 8 | for piece in leadingTrivia { 9 | switch piece { 10 | case let .docLineComment(comment): 11 | let line = String(comment.dropFirst(3)).trimmingCharacters(in: .whitespaces) // remove leading /// 12 | docStringLines.append(line) 13 | case let .docBlockComment(comment): 14 | let content = comment.dropFirst(3).dropLast(2) // remove /** and */ 15 | 16 | for line in content.split(separator: "\n") { 17 | // Remove leading asterisks and trim whitespace 18 | var trimmed = line.trimmingCharacters(in: .whitespaces) 19 | if !trimmed.isEmpty { 20 | if trimmed.hasPrefix("*") { trimmed = String(trimmed.dropFirst()) } // remove leading asterisk 21 | 22 | docStringLines.append(trimmed.trimmingCharacters(in: .whitespaces)) 23 | } 24 | } 25 | default: break 26 | } 27 | } 28 | 29 | return docStringLines.isEmpty ? nil : docStringLines.joined(separator: "\n") 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) Miguel Piedrafita 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/Models/WebhookEvent.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import MetaCodable 3 | import HelperCoders 4 | 5 | /// A webhook event corresponding to an update from the Responses API. 6 | @Codable @CodingKeys(.snake_case) public struct WebhookEvent: Equatable, Hashable, Sendable { 7 | /// Event data payload. 8 | public struct EventData: Equatable, Hashable, Codable, Sendable { 9 | /// The unique ID of the model response. 10 | public var id: String 11 | 12 | /// Creates a new `EventData` instance. 13 | /// 14 | /// - Parameter id: The unique ID of the model response. 15 | public init(id: String) { 16 | self.id = id 17 | } 18 | } 19 | 20 | /// The unique ID of the event. 21 | public var id: String 22 | 23 | /// The type of the event. 24 | public var type: String 25 | 26 | /// The date when the model response was completed. 27 | @CodedBy(Since1970DateCoder()) 28 | public var createdAt: Date 29 | 30 | /// Event data payload. 31 | public var data: EventData 32 | 33 | /// Creates a new `WebhookEvent` instance. 34 | /// 35 | /// - Parameter id: The unique ID of the event. 36 | /// - Parameter type: The type of the event. 37 | /// - Parameter createdAt: The date when the model response was completed. 38 | /// - Parameter data: Event data payload. 39 | public init(id: String, type: String, createdAt: Date, data: EventData) { 40 | self.id = id 41 | self.type = type 42 | self.data = data 43 | self.createdAt = createdAt 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/Support/MultiPartData.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | #if canImport(FoundationNetworking) 3 | import FoundationNetworking 4 | #endif 5 | 6 | final class FormData { 7 | enum Entry { 8 | case string(paramName: String, value: Any) 9 | 10 | case file(paramName: String, fileName: String, fileData: Data, contentType: String) 11 | 12 | var data: Data { 13 | var body = Data() 14 | 15 | switch self { 16 | case let .string(paramName, value): 17 | body.append("Content-Disposition: form-data; name=\"\(paramName)\"\r\n\r\n") 18 | body.append("\(value)\r\n") 19 | 20 | case let .file(paramName, fileName, fileData, contentType): 21 | body.append("Content-Disposition: form-data; name=\"\(paramName)\"; filename=\"\(fileName)\"\r\n") 22 | body.append("Content-Type: \(contentType)\r\n\r\n") 23 | body.append(fileData) 24 | body.append("\r\n") 25 | } 26 | 27 | return body 28 | } 29 | } 30 | 31 | let boundary: String 32 | let entries: [Entry] 33 | 34 | init(boundary: String, entries: [Entry]) { 35 | self.entries = entries 36 | self.boundary = boundary 37 | } 38 | 39 | var data: Data { 40 | var httpData = entries.map(\.data).reduce(Data()) { result, element in 41 | var result = result 42 | 43 | result.append("--\(boundary)\r\n") 44 | result.append(element) 45 | 46 | return result 47 | } 48 | 49 | httpData.append("--\(boundary)--\r\n") 50 | return httpData 51 | } 52 | 53 | var header: String { 54 | return "multipart/form-data; boundary=\(boundary)" 55 | } 56 | } 57 | 58 | extension URLRequest { 59 | mutating func attach(formData form: FormData) { 60 | httpBody = form.data 61 | addValue(form.header, forHTTPHeaderField: "Content-Type") 62 | } 63 | } 64 | 65 | fileprivate extension Data { 66 | mutating func append(_ string: String) { 67 | if let data = string.data(using: .utf8, allowLossyConversion: true) { 68 | append(data) 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version: 6.0 2 | 3 | import PackageDescription 4 | import CompilerPluginSupport 5 | 6 | let package = Package( 7 | name: "ResponsesAPI", 8 | platforms: [ 9 | .iOS(.v17), 10 | .tvOS(.v17), 11 | .macOS(.v14), 12 | .watchOS(.v10), 13 | .visionOS(.v1), 14 | .macCatalyst(.v17), 15 | ], 16 | products: [ 17 | .library(name: "ResponsesAPI", targets: ["ResponsesAPI"]), 18 | ], 19 | dependencies: [ 20 | .package(url: "https://github.com/SwiftyLab/MetaCodable.git", from: "1.0.0"), 21 | .package(url: "https://github.com/swiftlang/swift-docc-plugin", from: "1.1.0"), 22 | .package(url: "https://github.com/pointfreeco/swift-macro-testing", from: "0.6.0"), 23 | .package(url: "https://github.com/m1guelpf/EventSource.git", branch: "compiler-fix"), 24 | .package(url: "https://github.com/swiftlang/swift-syntax.git", "600.0.1"..<"603.0.0"), 25 | ], 26 | targets: [ 27 | .target( 28 | name: "ResponsesAPI", 29 | dependencies: [ 30 | "Macros", 31 | .product(name: "EventSource", package: "EventSource"), 32 | .product(name: "MetaCodable", package: "MetaCodable"), 33 | .product(name: "HelperCoders", package: "MetaCodable"), 34 | ], 35 | path: "./src" 36 | ), 37 | .macro( 38 | name: "Macros", 39 | dependencies: [ 40 | .product(name: "SwiftSyntax", package: "swift-syntax"), 41 | .product(name: "SwiftDiagnostics", package: "swift-syntax"), 42 | .product(name: "SwiftSyntaxMacros", package: "swift-syntax"), 43 | .product(name: "SwiftSyntaxBuilder", package: "swift-syntax"), 44 | .product(name: "SwiftCompilerPlugin", package: "swift-syntax"), 45 | ], 46 | path: "./macros" 47 | ), 48 | .testTarget( 49 | name: "Tests", 50 | dependencies: [ 51 | "ResponsesAPI", "Macros", 52 | .product(name: "MacroTesting", package: "swift-macro-testing"), 53 | .product(name: "SwiftSyntaxMacrosTestSupport", package: "swift-syntax"), 54 | ], 55 | path: "./tests" 56 | ), 57 | ] 58 | ) 59 | -------------------------------------------------------------------------------- /src/Protocol/Toolable.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public protocol Toolable: Equatable, Sendable { 4 | /// Arguments used to call the tool. 5 | associatedtype Arguments: Decodable & Schemable 6 | 7 | /// The output that the tool produces. 8 | associatedtype Output: Encodable 9 | 10 | /// The error type that the tool can throw. 11 | associatedtype Error: Swift.Error 12 | 13 | /// The name of the tool to call. 14 | var name: String { get } 15 | 16 | /// A description of the tool, used by the model to determine whether or not to call it. 17 | var description: String { get } 18 | 19 | /// Whether to enforce strict parameter validation. 20 | var strict: Bool { get } 21 | 22 | /// Initializes the tool. 23 | init() 24 | 25 | /// Runs the tool with the given parameters. 26 | func call(parameters: Self.Arguments) async throws(Self.Error) -> Self.Output 27 | } 28 | 29 | public extension Toolable { 30 | typealias Output = Void 31 | typealias Error = Swift.Error 32 | 33 | var strict: Bool { true } 34 | var description: String { "" } 35 | } 36 | 37 | package extension Toolable { 38 | func intoFunction() -> Tool.Function { 39 | guard case .object = Arguments.schema else { 40 | fatalError("Tool arguments must be a struct.") 41 | } 42 | 43 | return Tool.Function(name: name, description: description, parameters: Arguments.schema, strict: strict) 44 | } 45 | 46 | func respond(to functionCall: Item.FunctionCall) async throws -> Item.FunctionCallOutput? { 47 | let parameters = try decoder.decode(Arguments.self, from: Data(functionCall.arguments.utf8)) 48 | 49 | let output = try await call(parameters: parameters) 50 | 51 | if output is NullableVoid { return nil } 52 | 53 | if output is String { 54 | return Item.FunctionCallOutput(callId: functionCall.callId, output: .text(output as! String)) 55 | } 56 | 57 | if output is Input.Content { 58 | return Item.FunctionCallOutput(callId: functionCall.callId, output: output as! ModelInput.Content) 59 | } 60 | 61 | if output is Input.Content.ContentItem { 62 | return Item.FunctionCallOutput(callId: functionCall.callId, output: .list([output as! ModelInput.Content.ContentItem])) 63 | } 64 | 65 | return try Item.FunctionCallOutput( 66 | callId: functionCall.callId, 67 | output: .text(encoder.encodeToString(output)) 68 | ) 69 | } 70 | } 71 | 72 | fileprivate nonisolated let encoder = JSONEncoder() 73 | fileprivate nonisolated let decoder = JSONDecoder() 74 | -------------------------------------------------------------------------------- /.github/workflows/docs.yml: -------------------------------------------------------------------------------- 1 | name: Docs 2 | 3 | on: 4 | push: 5 | tags: ['*'] 6 | branches: [main] 7 | 8 | jobs: 9 | build: 10 | runs-on: macos-latest 11 | steps: 12 | - uses: swift-actions/setup-swift@v2 13 | 14 | - uses: actions/checkout@v4 15 | 16 | - name: Restore .build 17 | id: restore-build 18 | uses: actions/cache/restore@v4 19 | with: 20 | path: .build 21 | restore-keys: "swiftpm-docs-build-${{ runner.os }}-" 22 | key: "swiftpm-docs-build-${{ runner.os }}-${{ github.event.pull_request.base.sha || github.event.after }}" 23 | 24 | - name: Generate documentation 25 | run: | 26 | swift package --allow-writing-to-directory ./ResponsesAPI.doccarchive generate-documentation --target ResponsesAPI --disable-indexing --experimental-documentation-coverage --diagnostic-filter error --output-path ./ResponsesAPI.doccarchive 27 | tar -cf ResponsesAPI.doccarchive.tar ./ResponsesAPI.doccarchive/data ./ResponsesAPI.doccarchive/index ./ResponsesAPI.doccarchive/metadata.json ./ResponsesAPI.doccarchive/documentation-coverage.json 28 | 29 | - name: Cache .build 30 | if: steps.restore-build.outputs.cache-hit != 'true' 31 | uses: actions/cache/save@v4 32 | with: 33 | path: .build 34 | key: "swiftpm-docs-build-${{ runner.os }}-${{ github.event.pull_request.base.sha || github.event.after }}" 35 | 36 | - name: Update latest documentation 37 | id: github-pages 38 | if: github.ref_type != 'tag' 39 | uses: actions/upload-artifact@v4 40 | with: 41 | retention-days: 1 42 | name: github-pages 43 | if-no-files-found: error 44 | path: ./ResponsesAPI.doccarchive.tar 45 | 46 | - name: Attach to release 47 | if: github.ref_type == 'tag' 48 | uses: softprops/action-gh-release@v2 49 | with: 50 | files: ./ResponsesAPI.doccarchive.tar 51 | deploy: 52 | needs: build 53 | runs-on: ubuntu-latest 54 | if: github.ref_type != 'tag' 55 | permissions: 56 | pages: write 57 | id-token: write 58 | environment: 59 | name: github-pages 60 | url: ${{ steps.deployment.outputs.page_url }} 61 | steps: 62 | - name: Deploy to GitHub Pages 63 | id: deployment 64 | uses: actions/deploy-pages@v4 65 | -------------------------------------------------------------------------------- /macros/Extensions/TypeSyntax+typeInfo.swift: -------------------------------------------------------------------------------- 1 | import SwiftSyntax 2 | 3 | extension TypeSyntax { 4 | enum TypeInformation: Equatable { 5 | indirect enum SupportedPrimitive: RawRepresentable, Equatable { 6 | case int 7 | case bool 8 | case float 9 | case double 10 | case string 11 | case dictionary 12 | case array(TypeSyntax) 13 | case optional(TypeSyntax) 14 | 15 | var rawValue: String { 16 | switch self { 17 | case .int: return "Int" 18 | case .bool: return "Bool" 19 | case .array: return "Array" 20 | case .float: return "Float" 21 | case .double: return "Double" 22 | case .string: return "String" 23 | case .optional: return "Optional" 24 | case .dictionary: return "Dictionary" 25 | } 26 | } 27 | 28 | init?(rawValue: String) { 29 | switch rawValue { 30 | case "Int": self = .int 31 | case "Bool": self = .bool 32 | case "Float": self = .float 33 | case "Double": self = .double 34 | case "String": self = .string 35 | case "Dictionary": self = .dictionary 36 | default: return nil 37 | } 38 | } 39 | } 40 | 41 | case notSupported 42 | case schemable(String) 43 | case primitive(SupportedPrimitive) 44 | } 45 | 46 | var typeInfo: TypeInformation { 47 | let type = self.as(TypeSyntaxEnum.self) 48 | 49 | switch type { 50 | case .dictionaryType: return .primitive(.dictionary) 51 | case let .someOrAnyType(type): return type.constraint.typeInfo 52 | case let .arrayType(itemType): return .primitive(.array(itemType.element)) 53 | case let .optionalType(type): return .primitive(.optional(type.wrappedType)) 54 | case let .implicitlyUnwrappedOptionalType(type): return type.wrappedType.typeInfo 55 | case let .identifierType(type): 56 | if let generic = type.genericArgumentClause { 57 | if type.name.text == "Array" { 58 | let element = generic.arguments.first!.argument.as(TypeSyntax.self)! 59 | let arrayType = ArrayTypeSyntax(element: element) 60 | return TypeSyntax(arrayType).typeInfo 61 | } 62 | 63 | if type.name.text == "Dictionary" { 64 | return .primitive(.dictionary) 65 | } 66 | } 67 | 68 | if let primitive = TypeInformation.SupportedPrimitive(rawValue: type.name.text) { 69 | return .primitive(primitive) 70 | } 71 | 72 | return .schemable(type.name.text) 73 | case .packElementType, .metatypeType, .missingType, 74 | .suppressedType, .functionType, .classRestrictionType, 75 | .attributedType, .packExpansionType, .namedOpaqueReturnType, 76 | .tupleType, .memberType, .compositionType: return .notSupported 77 | } 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /macros/Support/DocString.swift: -------------------------------------------------------------------------------- 1 | import SwiftSyntax 2 | 3 | nonisolated(unsafe) let PARAMETERS_HEADER_REGEX = /(?m)^\s*-\s*Parameters:\s*$/ 4 | nonisolated(unsafe) let PARAMETER_REGEX = /- Parameter (?\w+): ?(?.*)/ 5 | nonisolated(unsafe) let PARAMETERS_ITEM_REGEX = /(?m)^\s*-\s*(?\w+)\s*:\s*(?.*)$/ 6 | 7 | struct DocString { 8 | let docString: String 9 | let properties: [String: String] 10 | 11 | var fullDocString: String { 12 | var docString = self.docString.trimmingCharacters(in: .whitespacesAndNewlines) 13 | 14 | for (key, value) in properties { 15 | docString.append("\n- Parameter \(key): \(value)") 16 | } 17 | 18 | return docString 19 | } 20 | 21 | var trivia: Trivia { 22 | fullDocString.split(separator: "\n").reduce(into: Trivia()) { trivia, line in 23 | if !trivia.isEmpty { trivia = trivia.appending(.newline) } 24 | 25 | trivia = trivia.merging(.docLineComment("/// \(line)")) 26 | } 27 | } 28 | 29 | static func parse(_ docString: String?) -> DocString? { 30 | guard let docString, !docString.isEmpty else { return nil } 31 | 32 | return parse(docString) 33 | } 34 | 35 | static func parse(_ docString: String) -> DocString { 36 | var docString = docString 37 | let properties = parseParameters(&docString) 38 | 39 | return DocString( 40 | docString: docString.trimmingCharacters(in: .whitespacesAndNewlines), 41 | properties: properties 42 | ) 43 | } 44 | 45 | func `for`(property: String) -> String? { 46 | properties[property] 47 | } 48 | 49 | func `for`(properties: String?...) -> String? { 50 | for property in properties { 51 | if let property, let value = self.properties[property] { 52 | return value 53 | } 54 | } 55 | 56 | return nil 57 | } 58 | 59 | func withComment(forProperty property: String, _ comment: String) -> DocString { 60 | var properties = self.properties 61 | properties[property] = comment 62 | 63 | return DocString(docString: docString, properties: properties) 64 | } 65 | 66 | private static func parseParameters(_ docString: inout String) -> [String: String] { 67 | var properties: [String: String] = [:] 68 | 69 | for match in docString.matches(of: PARAMETER_REGEX) { 70 | docString.removeSubrange(match.range.lowerBound..> $GITHUB_ENV 29 | echo "$SWIFTLY_BIN_DIR" >> $GITHUB_PATH 30 | echo "SWIFTLY_BIN_DIR=$SWIFTLY_BIN_DIR" >> $GITHUB_ENV 31 | echo "SWIFTLY_HOME_DIR=$SWIFTLY_HOME_DIR" >> $GITHUB_ENV 32 | 33 | export PATH=$SWIFTLY_BIN_DIR:$PATH 34 | 35 | mkdir -p $SWIFTLY_BIN_DIR 36 | mkdir -p $SWIFTLY_HOME_DIR 37 | 38 | if ${{ runner.os == 'Linux' }} ; then 39 | curl -O https://download.swift.org/swiftly/linux/swiftly-$(uname -m).tar.gz 40 | tar -xzf swiftly-$(uname -m).tar.gz -C $SWIFTLY_BIN_DIR 41 | rm swiftly-$(uname -m).tar.gz 42 | sudo apt-get update 43 | else 44 | curl -O https://download.swift.org/swiftly/darwin/swiftly.pkg 45 | installer -pkg swiftly.pkg -target CurrentUserHomeDirectory 46 | cp ~/.swiftly/bin/swiftly $SWIFTLY_BIN_DIR 47 | rm swiftly.pkg 48 | fi 49 | 50 | swiftly init --no-modify-profile --quiet-shell-followup --assume-yes --skip-install --verbose 51 | echo "swiftly version: $(swiftly --version)" >&2 52 | 53 | swiftly install --assume-yes --post-install-file "${HOME}/.swiftly-postinstall-cmds.sh" --use ${{ matrix.swift }} 54 | if [[ -f "${HOME}/.swiftly-postinstall-cmds.sh" ]]; then 55 | export DEBIAN_FRONTEND=noninteractive # prevent Debian/Ubuntu installs from hanging on tzdata 56 | if [[ "$(id -un)" == 'root' ]]; then 57 | . "${HOME}/.swiftly-postinstall-cmds.sh" 58 | else 59 | sudo bash -c ". '${HOME}/.swiftly-postinstall-cmds.sh'" 60 | fi 61 | rm "${HOME}/.swiftly-postinstall-cmds.sh" 62 | fi 63 | 64 | - uses: actions/checkout@v4 65 | 66 | - name: Restore .build 67 | id: restore-build 68 | uses: actions/cache/restore@v4 69 | with: 70 | path: .build 71 | restore-keys: "swiftpm-tests-build-${{ runner.os }}-${{ matrix.swift }}-" 72 | key: "swiftpm-tests-build-${{ runner.os }}-${{ matrix.swift }}-${{ github.event.pull_request.base.sha || github.event.after }}" 73 | 74 | - name: Build 75 | run: swift build --build-tests 76 | 77 | - name: Cache .build 78 | if: steps.restore-build.outputs.cache-hit != 'true' 79 | uses: actions/cache/save@v4 80 | with: 81 | path: .build 82 | key: "swiftpm-tests-build-${{ runner.os }}-${{ matrix.swift }}-${{ github.event.pull_request.base.sha || github.event.after }}" 83 | 84 | - name: Run tests 85 | run: swift test --skip-build --parallel 86 | -------------------------------------------------------------------------------- /src/Models/File.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import MetaCodable 3 | import HelperCoders 4 | 5 | @Codable @CodingKeys(.snake_case) public struct File: Equatable, Hashable, Sendable { 6 | public struct Upload: Equatable, Hashable, Sendable { 7 | /// The name of the file 8 | public var name: String 9 | /// The contents of the file 10 | public var contents: Data 11 | /// The mime type of the file 12 | public var contentType: String 13 | 14 | /// Creates a new file upload 15 | /// 16 | /// - Parameter name: The name of the file 17 | /// - Parameter contents: The contents of the file 18 | /// - Parameter contentType: The mime type of the file 19 | public init(name: String, contents: Data, contentType: String = "application/octet-stream") { 20 | self.name = name 21 | self.contents = contents 22 | self.contentType = contentType 23 | } 24 | } 25 | 26 | /// The intended purpose of the file. 27 | public enum Purpose: String, CaseIterable, Equatable, Hashable, Codable, Sendable { 28 | /// Used in the Assistants API 29 | case assistants 30 | /// Used in the Assistants API 31 | case assistantsOutput = "assistants_output" 32 | /// Used in the Batch API 33 | case batch 34 | /// Used for fine-tuning 35 | case fineTune = "fine-tune" 36 | /// Images used for vision fine-tuning 37 | case vision 38 | /// Flexible file type for any purpose 39 | case userData = "user_data" 40 | /// Used for eval data sets 41 | case evals 42 | } 43 | 44 | /// The current status of the file 45 | public enum Status: String, CaseIterable, Equatable, Codable, Sendable { 46 | case error 47 | case uploaded 48 | case processed 49 | } 50 | 51 | /// The file identifier, which can be referenced in the API endpoints. 52 | public var id: String 53 | 54 | /// The intended purpose of the file. 55 | public var purpose: Purpose 56 | 57 | /// The name of the file. 58 | public var filename: String 59 | 60 | /// The size of the file, in bytes. 61 | public var bytes: Int 62 | 63 | /// The `Date` when the file was created. 64 | @CodedBy(Since1970DateCoder()) 65 | public var createdAt: Date 66 | 67 | /// The `Date` when the file will expire. 68 | @CodedBy(Since1970DateCoder()) 69 | public var expiresAt: Date? 70 | 71 | /// The current status of the file 72 | public var status: Status 73 | 74 | /// Create a new `File` instance. 75 | /// 76 | /// - Parameter id: The file identifier, which can be referenced in the API endpoints. 77 | /// - Parameter purpose: The intended purpose of the file. 78 | /// - Parameter filename: The name of the file. 79 | /// - Parameter bytes: The size of the file, in bytes. 80 | /// - Parameter createdAt: The `Date` when the file was created. 81 | /// - Parameter expiresAt: The `Date` when the file will expire. 82 | /// - Parameter status: The current status of the file 83 | public init(id: String, purpose: Purpose, filename: String, bytes: Int, createdAt: Date, expiresAt: Date? = nil, status: Status) { 84 | self.id = id 85 | self.bytes = bytes 86 | self.status = status 87 | self.purpose = purpose 88 | self.filename = filename 89 | self.createdAt = createdAt 90 | self.expiresAt = expiresAt 91 | } 92 | } 93 | 94 | // MARK: - Creation helpers 95 | 96 | public extension File.Upload { 97 | /// Creates a file upload from a local file or URL. 98 | /// 99 | /// - Parameter url: The URL of the file to upload. 100 | /// - Parameter name: The name of the file. 101 | /// - Parameter contentType: The mime type of the file. 102 | static func url(_ url: URL, name: String? = nil, contentType: String = "application/octet-stream") async throws -> File.Upload { 103 | let name = name ?? url.lastPathComponent == "/" ? "unknown_file" : url.lastPathComponent 104 | 105 | return try File.Upload(name: name, contents: Data(contentsOf: url), contentType: contentType) 106 | } 107 | 108 | /// Creates a file upload from the given data. 109 | /// 110 | /// - Parameter name: The name of the file. 111 | /// - Parameter contents: The contents of the file. 112 | /// - Parameter contentType: The mime type of the file. 113 | static func file(name: String, contents: Data, contentType: String = "application/octet-stream") -> File.Upload { 114 | return File.Upload(name: name, contents: contents, contentType: contentType) 115 | } 116 | } 117 | 118 | // MARK: - Private helpers 119 | 120 | extension File.Upload { 121 | func toFormEntry(paramName: String = "file") -> FormData.Entry { 122 | .file(paramName: paramName, fileName: name, fileData: contents, contentType: contentType) 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /macros/Generators/EnumSchemaGenerator.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import SwiftSyntax 3 | import SwiftSyntaxMacros 4 | 5 | struct EnumSchemaGenerator { 6 | let name: TokenSyntax 7 | let docString: String? 8 | let attributes: AttributeListSyntax 9 | let declModifier: DeclModifierSyntax? 10 | let members: MemberBlockItemListSyntax 11 | 12 | init(fromEnum enumDecl: EnumDeclSyntax) { 13 | name = enumDecl.name.trimmed 14 | docString = enumDecl.docString 15 | attributes = enumDecl.attributes 16 | members = enumDecl.memberBlock.members 17 | declModifier = enumDecl.modifiers.first 18 | } 19 | 20 | func makeSchema() throws -> VariableDeclSyntax { 21 | let schemableCases = members 22 | .compactMap { $0.decl.as(EnumCaseDeclSyntax.self) } 23 | .flatMap { caseDecl in caseDecl.elements.map { (caseDecl, $0) } } 24 | .map(EnumCase.init) 25 | 26 | let casesWithoutAssociatedValues = schemableCases.filter { $0.associatedValues == nil } 27 | let casesWithAssociatedValues = schemableCases.filter { $0.associatedValues != nil } 28 | 29 | guard !casesWithAssociatedValues.isEmpty else { 30 | let expr = try buildSimpleEnum(withCases: casesWithoutAssociatedValues) 31 | 32 | return try VariableDeclSyntax(""" 33 | \(declModifier)static var schema: JSONSchema { 34 | \(raw: expr) 35 | } 36 | """) 37 | } 38 | 39 | var cases = try casesWithAssociatedValues.map { try $0.makeSchema() } 40 | if !casesWithoutAssociatedValues.isEmpty { cases = try [buildSimpleEnum(withCases: casesWithoutAssociatedValues, includeComment: false)] + cases } 41 | 42 | return try VariableDeclSyntax(""" 43 | \(declModifier)static var schema: JSONSchema { 44 | .anyOf(\(raw: ArrayElementListSyntax(expressions: cases)), description: \(literal: docString)) 45 | } 46 | """) 47 | } 48 | 49 | func buildSimpleEnum(withCases cases: [EnumCase], includeComment: Bool = true) throws -> ExprSyntax { 50 | var docString = includeComment ? self.docString : nil 51 | let caseComments = cases 52 | .filter { $0.docString != nil } 53 | .map { "- \($0.identifier.text): \($0.docString!)" } 54 | .joined(separator: "\n") 55 | 56 | if !caseComments.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty { 57 | docString = docString != nil ? "\(docString!)\n\n" : "" 58 | docString! += caseComments 59 | } 60 | 61 | return try ".enum(cases: [\(raw: ArrayElementListSyntax(expressions: cases.map { try $0.makeSchema() }))], description: \(literal: docString))" 62 | } 63 | } 64 | 65 | extension EnumSchemaGenerator { 66 | struct EnumCase { 67 | let docString: String? 68 | let identifier: TokenSyntax 69 | let declaration: SyntaxProtocol 70 | let associatedValues: EnumCaseParameterListSyntax? 71 | 72 | init(enumCaseDecl: EnumCaseDeclSyntax, caseElement: EnumCaseElementSyntax) { 73 | declaration = enumCaseDecl 74 | docString = enumCaseDecl.docString 75 | identifier = caseElement.name.trimmed 76 | associatedValues = caseElement.parameterClause?.parameters 77 | } 78 | 79 | func makeSchema() throws -> ExprSyntax { 80 | guard let associatedValues else { return "\(literal: identifier.text)" } 81 | var docString = self.docString 82 | var propertyDocStrings: [String: String] = [:] 83 | 84 | if docString != nil { 85 | let regex = /- Parameter (?\w*): ?(?.*)/ 86 | 87 | for match in docString!.matches(of: regex) { 88 | docString!.removeSubrange(match.range.lowerBound.. DictionaryElementSyntax? in 98 | let key = property.firstName?.text ?? "_\(i)" 99 | 100 | let parameter = StructSchemaGenerator.StructMember( 101 | declaration: declaration, 102 | type: property.type, 103 | identifier: TokenSyntax(stringLiteral: key), 104 | docString: propertyDocStrings[key] 105 | ) 106 | 107 | guard let schema = try parameter.makeSchema() else { return nil } 108 | return DictionaryElementSyntax( 109 | key: ExprSyntax(literal: key), 110 | value: schema 111 | ) 112 | } 113 | ) 114 | 115 | return """ 116 | .object(properties: [\(literal: identifier.text): .object(properties: [\(raw: properties)])], description: \(literal: docString)) 117 | """ 118 | } 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /src/Models/Message.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public typealias ModelInput = Input 4 | 5 | public enum Message: Equatable, Hashable, Sendable { 6 | /// The role of a message. 7 | public enum Role: String, CaseIterable, Equatable, Hashable, Codable, Sendable { 8 | case user 9 | case system 10 | case assistant 11 | case developer 12 | } 13 | 14 | /// The status of a message. 15 | public enum Status: String, CaseIterable, Equatable, Codable, Sendable { 16 | case completed 17 | case incomplete 18 | case inProgress = "in_progress" 19 | } 20 | 21 | /// The content of a message. 22 | public enum MessageContent: Equatable, Hashable, Sendable { 23 | /// Text, image, or audio input to the model, used to generate a response. Can also contain previous assistant responses. 24 | case input(ModelInput.Content) 25 | 26 | /// The content of the output message. 27 | case output([Item.Output.Content]) 28 | } 29 | 30 | /// A message input to the model with a role indicating instruction following hierarchy. 31 | /// 32 | /// Instructions given with the `developer` or `system` role take precedence over instructions given with the `user` role. 33 | /// 34 | /// Messages with the `assistant` role are presumed to have been generated by the model in previous interactions. 35 | public struct Input: Equatable, Hashable, Codable, Sendable { 36 | /// The role of the message input. 37 | public var role: Role 38 | 39 | /// The status of the message. Populated when the message is returned via API. 40 | public var status: Status? 41 | 42 | /// Text, image, or audio input to the model, used to generate a response. Can also contain previous assistant responses. 43 | public var content: ModelInput.Content 44 | 45 | /// The text content of the input. 46 | public var text: String? { 47 | return content.text 48 | } 49 | 50 | public init(role: Role = .user, content: ModelInput.Content, status: Status? = nil) { 51 | self.role = role 52 | self.status = status 53 | self.content = content 54 | } 55 | } 56 | 57 | /// An output message from the model. 58 | public struct Output: Equatable, Hashable, Codable, Sendable { 59 | /// The content of the output message. 60 | public var content: [Item.Output.Content] 61 | 62 | /// The unique ID of the output message. 63 | public var id: String 64 | 65 | /// The role of the output message. Always `assistant`. 66 | public var role: Role 67 | 68 | /// The status of the message output. 69 | public var status: Status 70 | 71 | /// The text content of the output. 72 | /// 73 | /// > Note: Annotations are not included when using this property. 74 | public var text: String { 75 | return content.map(\.text).joined() 76 | } 77 | 78 | /// Creates a new output message. 79 | /// 80 | /// - Parameter content: The content of the output message. 81 | /// - Parameter id: The unique ID of the output message. 82 | /// - Parameter role: The role of the output message. Always `assistant`. 83 | /// - Parameter status: The status of the message output. 84 | public init(content: [Item.Output.Content] = [], id: String, role: Role = .assistant, status: Status) { 85 | self.id = id 86 | self.role = role 87 | self.status = status 88 | self.content = content 89 | } 90 | } 91 | 92 | /// An input message to the model. 93 | case input(Input) 94 | 95 | /// An output message from the model. 96 | case output(Output) 97 | 98 | /// The role of a message. 99 | public var role: Role { 100 | switch self { 101 | case let .input(input): input.role 102 | case let .output(output): output.role 103 | } 104 | } 105 | 106 | /// The content of the message. 107 | public var content: MessageContent { 108 | switch self { 109 | case let .input(input): .input(input.content) 110 | case let .output(output): .output(output.content) 111 | } 112 | } 113 | 114 | /// The status of the message. 115 | public var status: Status? { 116 | switch self { 117 | case let .input(input): input.status 118 | case let .output(output): output.status 119 | } 120 | } 121 | 122 | /// The unique ID of the message, if available. 123 | public var id: String? { 124 | switch self { 125 | case .input: nil 126 | case let .output(output): output.id 127 | } 128 | } 129 | 130 | /// The text content of the input. 131 | /// 132 | /// > Note: This property does not include reasoning text. 133 | public var text: String? { 134 | switch self { 135 | case let .input(message): message.text 136 | case let .output(message): message.text 137 | } 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /src/Support/APIClient.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import EventSource 3 | #if canImport(FoundationNetworking) 4 | import FoundationNetworking 5 | #endif 6 | 7 | public struct APIClient: Sendable { 8 | public enum Error: Swift.Error { 9 | /// The provided request is invalid. 10 | case invalidRequest(URLRequest) 11 | 12 | /// The response was not a 200 or 400 status 13 | case invalidResponse(URLResponse) 14 | } 15 | 16 | private let request: URLRequest 17 | private let eventSource = EventSource(mode: .dataOnly) 18 | private let encoder = tap(JSONEncoder()) { $0.dateEncodingStrategy = .iso8601 } 19 | private let decoder = tap(JSONDecoder()) { $0.dateDecodingStrategy = .iso8601 } 20 | 21 | /// Creates a new `APIClient` instance using the provided `URLRequest`. 22 | /// 23 | /// - Parameter request: The `URLRequest` to use for the API. 24 | init(connectingTo request: URLRequest) throws(Error) { 25 | guard let url = request.url else { throw Error.invalidRequest(request) } 26 | 27 | var request = request 28 | if url.lastPathComponent != "/" { 29 | request.url = url.appendingPathComponent("/") 30 | } 31 | 32 | self.request = request 33 | } 34 | 35 | /// Creates a new `ResponsesAPI` instance using OpenAI API credentials. 36 | /// 37 | /// - Parameter authToken: The OpenAI API key to use for authentication. 38 | /// - Parameter organizationId: The [organization](https://platform.openai.com/docs/guides/production-best-practices#setting-up-your-organization) associated with the request. 39 | /// - Parameter projectId: The project associated with the request. 40 | init(authToken: String, organizationId: String? = nil, projectId: String? = nil) { 41 | var request = URLRequest(url: URL(string: "https://api.openai.com/")!) 42 | 43 | request.addValue("Bearer \(authToken)", forHTTPHeaderField: "Authorization") 44 | if let projectId { request.addValue(projectId, forHTTPHeaderField: "OpenAI-Project") } 45 | if let organizationId { request.addValue(organizationId, forHTTPHeaderField: "OpenAI-Organization") } 46 | 47 | self.request = request 48 | } 49 | 50 | func send(expecting _: R.Type, configuring requestBuilder: (inout URLRequest, JSONEncoder) throws -> Void) async throws -> R { 51 | return try decoder.decode(R.self, from: await send(configuring: requestBuilder)) 52 | } 53 | 54 | func send(configuring requestBuilder: (inout URLRequest, JSONEncoder) throws -> Void) async throws -> Data { 55 | var req = request 56 | try requestBuilder(&req, encoder) 57 | 58 | return try await send(request: req) 59 | } 60 | 61 | func stream(expecting _: T.Type, configuring requestBuilder: (inout URLRequest, JSONEncoder) throws -> Void) async throws -> AsyncThrowingStream { 62 | var req = request 63 | try requestBuilder(&req, encoder) 64 | 65 | return try await sseStream(of: T.self, request: req) 66 | } 67 | 68 | /// Sends an URLRequest and returns the response data. 69 | /// 70 | /// - Throws: If the request fails to send or has a non-200 status code. 71 | private func send(request: URLRequest) async throws -> Data { 72 | let (data, res) = try await URLSession.shared.data(for: request) 73 | 74 | guard let res = res as? HTTPURLResponse else { throw Error.invalidResponse(res) } 75 | guard res.statusCode != 200 else { return data } 76 | 77 | if let response = try? decoder.decode(Response.ErrorResponse.self, from: data) { 78 | throw response.error 79 | } 80 | 81 | throw Error.invalidResponse(res) 82 | } 83 | 84 | private func sseStream(of _: T.Type, request: URLRequest) async throws -> AsyncThrowingStream { 85 | let (stream, continuation) = AsyncThrowingStream.makeStream(of: T.self) 86 | 87 | let task = Task { 88 | defer { continuation.finish() } 89 | 90 | let dataTask = eventSource.dataTask(for: request) 91 | defer { dataTask.cancel(urlSession: URLSession.shared) } 92 | 93 | for await event in dataTask.events() { 94 | guard case let .event(event) = event, let data = event.data?.data(using: .utf8) else { continue } 95 | 96 | continuation.yield(with: Result { try decoder.decode(T.self, from: data) }) 97 | 98 | try Task.checkCancellation() 99 | } 100 | } 101 | 102 | continuation.onTermination = { _ in 103 | task.cancel() 104 | } 105 | 106 | return stream 107 | } 108 | 109 | /// A hacky parser for Server-Sent Events lines. 110 | /// 111 | /// It looks for a line that starts with `data:`, then tries to decode the message as the given type. 112 | private func parseSSELine(_ line: String, as _: T.Type = T.self) -> Result? { 113 | let components = line.split(separator: ":", maxSplits: 1, omittingEmptySubsequences: true) 114 | guard components.count == 2, components[0] == "data" else { return nil } 115 | 116 | let message = components[1].trimmingCharacters(in: .whitespacesAndNewlines) 117 | 118 | return Result { try decoder.decode(T.self, from: Data(message.utf8)) } 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /src/Models/Model.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /// The model to use for generating a response. 4 | public enum Model: Equatable, Hashable, Sendable { 5 | case o1 6 | case o1Pro 7 | case o1Mini 8 | case o3 9 | case o3Pro 10 | case o3DeepResearch 11 | case o3Mini 12 | case o4Mini 13 | case o4MiniDeepResearch 14 | case codexMini 15 | case gpt5 16 | case gpt5Pro 17 | case gpt5Mini 18 | case gpt5Nano 19 | case gpt5Codex 20 | case gpt5CodexMini 21 | case gpt51 22 | case gpt51Mini 23 | case gpt51Codex 24 | case gpt51CodexMini 25 | case gpt52 26 | case gpt52Pro 27 | case gpt4_5Preview 28 | case gpt4o 29 | case chatGPT4o 30 | case gpt4oMini 31 | case gpt4Turbo 32 | case gpt4 33 | case gpt4_1 34 | case gpt4_1Mini 35 | case gpt4_1Nano 36 | case gpt3_5Turbo 37 | case computerUsePreview 38 | case other(String) 39 | 40 | public var rawValue: String { 41 | switch self { 42 | case .o1: "o1" 43 | case .o3: "o3" 44 | case .gpt5: "gpt-5" 45 | case .gpt5Pro: "gpt-5-pro" 46 | case .gpt5Mini: "gpt-5-mini" 47 | case .gpt5Nano: "gpt-5-nano" 48 | case .gpt51: "gpt-5.1" 49 | case .gpt51Mini: "gpt-5.1-mini" 50 | case .gpt51Codex: "gpt-5.1-codex" 51 | case .gpt51CodexMini: "gpt-5.1-codex-mini" 52 | case .gpt52: "gpt-5.2" 53 | case .gpt52Pro: "gpt-5.2-pro" 54 | case .gpt4: "gpt-4" 55 | case .o3Pro: "o3-pro" 56 | case .o1Pro: "o1-pro" 57 | case .gpt4o: "gpt-4o" 58 | case .o1Mini: "o1-mini" 59 | case .o3Mini: "o3-mini" 60 | case .o4Mini: "o4-mini" 61 | case .gpt4_1: "gpt-4.1" 62 | case .codexMini: "codex-mini" 63 | case let .other(value): value 64 | case .gpt5Codex: "gpt-5-codex" 65 | case .gpt5CodexMini: "gpt-5-codex-mini" 66 | case .gpt4oMini: "gpt-4o-mini" 67 | case .gpt4Turbo: "gpt-4o-turbo" 68 | case .gpt4_1Nano: "gpt-4.1-nano" 69 | case .gpt4_1Mini: "gpt-4.1-mini" 70 | case .gpt3_5Turbo: "gpt-3.5-turbo" 71 | case .chatGPT4o: "chatgpt-4o-latest" 72 | case .gpt4_5Preview: "gpt-4.5-preview" 73 | case .o3DeepResearch: "o3-deep-research" 74 | case .computerUsePreview: "computer-use-preview" 75 | case .o4MiniDeepResearch: "o4-mini-deep-research" 76 | } 77 | } 78 | 79 | /// Creates a new `Model` instance from a string. 80 | public init(_ model: String) { 81 | switch model { 82 | case "o1": self = .o1 83 | case "o3": self = .o3 84 | case "gpt-5": self = .gpt5 85 | case "gpt-4": self = .gpt4 86 | case "gpt-4o": self = .gpt4o 87 | case "o1-pro": self = .o1Pro 88 | case "o3-pro": self = .o3Pro 89 | case "gpt-5.1": self = .gpt51 90 | case "gpt-5.2": self = .gpt52 91 | case "o1-mini": self = .o1Mini 92 | case "o3-mini": self = .o3Mini 93 | case "o4-mini": self = .o4Mini 94 | case "gpt-4.1": self = .gpt4_1 95 | case "gpt-5-pro": self = .gpt5Pro 96 | case "gpt-5-mini": self = .gpt5Mini 97 | case "gpt-5-nano": self = .gpt5Nano 98 | case "codex-mini": self = .codexMini 99 | case "gpt-5.2-pro": self = .gpt52Pro 100 | case "gpt-5-codex": self = .gpt5Codex 101 | case "gpt-4o-mini": self = .gpt4oMini 102 | case "gpt-5.1-mini": self = .gpt51Mini 103 | case "gpt-4o-turbo": self = .gpt4Turbo 104 | case "gpt-4.1-nano": self = .gpt4_1Nano 105 | case "gpt-4.1-mini": self = .gpt4_1Mini 106 | case "gpt-5.1-codex": self = .gpt51Codex 107 | case "gpt-3.5-turbo": self = .gpt3_5Turbo 108 | case "chatgpt-4o-latest": self = .chatGPT4o 109 | case "gpt-4.5-preview": self = .gpt4_5Preview 110 | case "gpt-5-codex-mini": self = .gpt5CodexMini 111 | case "o3-deep-research": self = .o3DeepResearch 112 | case "gpt-5.1-codex-mini": self = .gpt51CodexMini 113 | case "computer-use-preview": self = .computerUsePreview 114 | case "o4-mini-deep-research": self = .o4MiniDeepResearch 115 | default: self = .other(model) 116 | } 117 | } 118 | } 119 | 120 | public extension Model { 121 | enum Image: Equatable, Hashable, Sendable { 122 | case gptImage 123 | case gptImageMini 124 | case other(String) 125 | 126 | public var rawValue: String { 127 | switch self { 128 | case .gptImage: "gpt-image-1" 129 | case .gptImageMini: "gpt-image-1-mini" 130 | case let .other(value): value 131 | } 132 | } 133 | 134 | /// Creates a new `Model.Image` instance from a string. 135 | public init(_ model: String) { 136 | switch model { 137 | case "gpt-image-1": self = .gptImage 138 | case "gpt-image-1-mini": self = .gptImageMini 139 | default: self = .other(model) 140 | } 141 | } 142 | } 143 | } 144 | 145 | extension Model: RawRepresentable, Codable { 146 | public func encode(to encoder: any Encoder) throws { 147 | try rawValue.encode(to: encoder) 148 | } 149 | 150 | public init(from decoder: any Decoder) throws { 151 | try self.init(String(from: decoder)) 152 | } 153 | 154 | public init?(rawValue: String) { 155 | self.init(rawValue) 156 | } 157 | } 158 | 159 | extension Model.Image: RawRepresentable, Codable { 160 | public func encode(to encoder: any Encoder) throws { 161 | try rawValue.encode(to: encoder) 162 | } 163 | 164 | public init(from decoder: any Decoder) throws { 165 | try self.init(String(from: decoder)) 166 | } 167 | 168 | public init?(rawValue: String) { 169 | self.init(rawValue) 170 | } 171 | } 172 | -------------------------------------------------------------------------------- /macros/Generators/StructSchemaGenerator.swift: -------------------------------------------------------------------------------- 1 | import SwiftSyntax 2 | import SwiftSyntaxMacros 3 | 4 | struct StructSchemaGenerator { 5 | let name: TokenSyntax 6 | let docString: String? 7 | let attributes: AttributeListSyntax 8 | let declModifier: DeclModifierSyntax? 9 | let members: MemberBlockItemListSyntax 10 | 11 | init(fromStruct structDecl: StructDeclSyntax) { 12 | name = structDecl.name.trimmed 13 | docString = structDecl.docString 14 | attributes = structDecl.attributes 15 | members = structDecl.memberBlock.members 16 | declModifier = structDecl.modifiers.first 17 | } 18 | 19 | func makeSchema() throws -> VariableDeclSyntax { 20 | let members = try members 21 | .compactMap { $0.decl.as(VariableDeclSyntax.self) } 22 | .flatMap { variableDecl in variableDecl.bindings.map { (variableDecl, $0) } } 23 | .filter { $0.1.isStored } 24 | .compactMap { try StructMember(variableDecl: $0, patternBinding: $1) } 25 | 26 | let properties = try DictionaryElementListSyntax(members.enumerated().compactMap { i, member -> DictionaryElementSyntax? in 27 | guard let schema = try member.makeSchema() else { return nil } 28 | 29 | return DictionaryElementSyntax( 30 | leadingTrivia: .newline.merging(.tabs(2)), 31 | key: ExprSyntax(literal: member.identifier.text), 32 | value: ExprSyntax(schema), 33 | trailingComma: .commaToken(), 34 | trailingTrivia: i == members.count - 1 ? .newline.merging(.tab) : nil 35 | ) 36 | }) 37 | 38 | return try VariableDeclSyntax(""" 39 | \(declModifier)static var schema: JSONSchema { 40 | .object(properties: \(raw: DictionaryExprSyntax { properties }), description: \(literal: docString)) 41 | } 42 | """) 43 | } 44 | } 45 | 46 | extension StructSchemaGenerator { 47 | struct StructMember { 48 | let type: TypeSyntax 49 | let docString: String? 50 | let identifier: TokenSyntax 51 | let defaultValue: ExprSyntax? 52 | let declaration: any SyntaxProtocol 53 | let attributes: AttributeListSyntax 54 | 55 | init(declaration: some SyntaxProtocol, type: TypeSyntax, identifier: TokenSyntax, docString: String? = nil, attributes: AttributeListSyntax = AttributeListSyntax([])) { 56 | self.type = type 57 | defaultValue = nil 58 | self.docString = docString 59 | self.identifier = identifier 60 | self.attributes = attributes 61 | self.declaration = declaration 62 | } 63 | 64 | init?(variableDecl: VariableDeclSyntax, patternBinding: PatternBindingSyntax) throws { 65 | guard let identifier = patternBinding.pattern.as(IdentifierPatternSyntax.self)?.identifier else { return nil } 66 | guard let type = patternBinding.typeAnnotation?.type else { 67 | throw ReportableError(node: variableDecl, errorMessage: "You must provide a type for the property '\(identifier.text)'.") 68 | } 69 | 70 | self.type = type 71 | declaration = variableDecl 72 | self.identifier = identifier 73 | docString = variableDecl.docString 74 | attributes = variableDecl.attributes 75 | defaultValue = patternBinding.initializer?.value 76 | } 77 | 78 | func makeSchema() throws -> ExprSyntax? { 79 | if defaultValue != nil { 80 | let type = StructMember(declaration: declaration, type: type, identifier: identifier) 81 | guard let schema = try type.makeSchema() else { return nil } 82 | return ".anyOf([\(raw: schema), .null], description: \(literal: docString))" 83 | } 84 | 85 | switch type.typeInfo { 86 | case .notSupported: return nil 87 | case let .schemable(type): return "\(raw: type).schema(description: \(literal: docString))" 88 | case let .primitive(primitive): 89 | switch primitive { 90 | case .bool: return ".boolean(description: \(literal: docString))" 91 | case .int: 92 | var options = attributes.arguments(for: "NumberSchema") ?? LabeledExprListSyntax() 93 | options.append(LabeledExprSyntax(label: "description", expression: ExprSyntax(literal: docString))) 94 | return ".integer(\(options))" 95 | case .string: 96 | var options = attributes.arguments(for: "StringSchema") ?? LabeledExprListSyntax() 97 | options.append(LabeledExprSyntax(label: "description", expression: ExprSyntax(literal: docString))) 98 | return ".string(\(options))" 99 | case .double, .float: 100 | var options = attributes.arguments(for: "NumberSchema") ?? LabeledExprListSyntax() 101 | options.append(LabeledExprSyntax(label: "description", expression: ExprSyntax(literal: docString))) 102 | return ".number(\(options))" 103 | case let .array(type): 104 | let type = StructMember(declaration: declaration, type: type, identifier: identifier, attributes: attributes) 105 | guard let schema = try type.makeSchema() else { return nil } 106 | 107 | var options = attributes.arguments(for: "ArraySchema") ?? LabeledExprListSyntax() 108 | options.append(LabeledExprSyntax(label: "description", expression: ExprSyntax(literal: docString))) 109 | return ".array(of: \(raw: schema), \(raw: options))" 110 | case let .optional(type): 111 | let type = StructMember(declaration: declaration, type: type, identifier: identifier, attributes: attributes) 112 | guard let schema = try type.makeSchema() else { return nil } 113 | return ".anyOf([\(raw: schema), .null], description: \(literal: docString))" 114 | case .dictionary: 115 | throw ReportableError(node: declaration, errorMessage: "Dictionaries are not supported when using @Schema. Use a custom struct instead.") 116 | } 117 | } 118 | } 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /src/ConversationsAPI.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | #if canImport(FoundationNetworking) 3 | import FoundationNetworking 4 | #endif 5 | 6 | /// A Swift client for the OpenAI Conversations API. 7 | public struct ConversationsAPI: Sendable { 8 | private let client: APIClient 9 | 10 | /// Creates a new `ConversationsAPI` instance using the provided `URLRequest`. 11 | /// 12 | /// You can use this initializer to use a custom base URL or custom headers. 13 | /// 14 | /// - Parameter request: The `URLRequest` to use for the API. 15 | public init(connectingTo request: URLRequest) throws(APIClient.Error) { 16 | client = try APIClient(connectingTo: request) 17 | } 18 | 19 | /// Creates a new `ConversationsAPI` instance using the provided `authToken`. 20 | /// 21 | /// You can optionally provide an `organizationId` and/or `projectId` to use with the API. 22 | /// 23 | /// - Parameter authToken: The OpenAI API key to use for authentication. 24 | /// - Parameter organizationId: The [organization](https://platform.openai.com/docs/guides/production-best-practices#setting-up-your-organization) associated with the request. 25 | /// - Parameter projectId: The project associated with the request. 26 | public init(authToken: String, organizationId: String? = nil, projectId: String? = nil) { 27 | client = APIClient(authToken: authToken, organizationId: organizationId, projectId: projectId) 28 | } 29 | 30 | /// Create a conversation. 31 | /// 32 | /// - Parameter items: Initial items to include in the conversation context. 33 | /// - Parameter metadata: Set of 16 key-value pairs that can be attached to an object. 34 | public func create(items: [Input.ListItem]? = nil, metadata: [String: String]? = nil) async throws -> APIConversation { 35 | return try await client.send(expecting: APIConversation.self) { req, encoder in 36 | req.httpMethod = "POST" 37 | req.url!.append(path: "/v1/conversations") 38 | req.addValue("application/json", forHTTPHeaderField: "Content-Type") 39 | req.httpBody = try encoder.encode(CreateConversationRequest(items: items, metadata: metadata)) 40 | } 41 | } 42 | 43 | /// Get a conversation with the given ID. 44 | /// 45 | /// - Parameter id: The ID of the conversation to retrieve. 46 | public func get(id: String) async throws -> APIConversation { 47 | return try await client.send(expecting: APIConversation.self) { req, _ in 48 | req.httpMethod = "GET" 49 | req.url!.append(path: "/v1/conversations/\(id)") 50 | } 51 | } 52 | 53 | /// Update a conversation's metadata with the given ID. 54 | /// 55 | /// - Parameter id: The ID of the conversation to update. 56 | /// - Parameter metadata: Set of 16 key-value pairs that can be attached to an object. 57 | public func update(id: String, metadata: [String: String]? = nil) async throws -> APIConversation { 58 | return try await client.send(expecting: APIConversation.self) { req, encoder in 59 | req.httpMethod = "POST" 60 | req.url!.append(path: "/v1/conversations/\(id)") 61 | req.addValue("application/json", forHTTPHeaderField: "Content-Type") 62 | req.httpBody = try encoder.encode(UpdateConversationRequest(metadata: metadata)) 63 | } 64 | } 65 | 66 | /// Delete a conversation with the given ID. 67 | /// 68 | /// - Parameter id: The ID of the conversation to delete. 69 | public func delete(id: String) async throws { 70 | _ = try await client.send { req, _ in 71 | req.httpMethod = "DELETE" 72 | req.url!.append(path: "/v1/conversations/\(id)") 73 | } 74 | } 75 | 76 | /// List all items for a conversation with the given ID. 77 | /// 78 | /// - Parameter id: The ID of the conversation to list items for. 79 | /// - Parameter after: An item ID to list items after, used in pagination. 80 | /// - Parameter include: Specify additional output data to include in the model response. 81 | /// - Parameter limit: A limit on the number of objects to be returned. 82 | /// - Parameter order: The order to return the input items in. 83 | public func listItems(id: String, after: String? = nil, include: Request.Include? = nil, limit: Int? = nil, order: Order? = nil) async throws -> Input.ItemList { 84 | return try await client.send(expecting: Input.ItemList.self) { req, encoder in 85 | req.httpMethod = "GET" 86 | req.url!.append(path: "/v1/conversations/\(id)/items") 87 | req.addValue("application/json", forHTTPHeaderField: "Content-Type") 88 | try req.url!.append(queryItems: [ 89 | after.map { URLQueryItem(name: "after", value: $0) }, 90 | limit.map { URLQueryItem(name: "limit", value: "\($0)") }, 91 | order.map { URLQueryItem(name: "order", value: $0.rawValue) }, 92 | include.map { try URLQueryItem(name: "include", value: encoder.encodeToString($0)) }, 93 | ]) 94 | } 95 | } 96 | 97 | /// Create items in a conversation with the given ID. 98 | /// 99 | /// - Parameter id: The ID of the conversation to add the item to. 100 | /// - Parameter include: Additional fields to include in the response. 101 | /// - Parameter items: The items to add to the conversation. 102 | public func createItems(id: String, include: Request.Include? = nil, items: [Input.ListItem]) async throws -> Input.ItemList { 103 | return try await client.send(expecting: Input.ItemList.self) { req, encoder in 104 | req.httpMethod = "POST" 105 | req.url!.append(path: "/v1/conversations/\(id)/items") 106 | req.addValue("application/json", forHTTPHeaderField: "Content-Type") 107 | req.httpBody = try encoder.encode(CreateItemsRequest(include: include, items: items)) 108 | } 109 | } 110 | 111 | /// Get a single item from a conversation with the given IDs. 112 | /// 113 | /// - Parameter id: The ID of the conversation that contains the item. 114 | /// - Parameter itemId: The ID of the item to retrieve. 115 | /// - Parameter include: Additional fields to include in the response. 116 | public func getItem(id: String, itemId: String, include: [Request.Include]? = nil) async throws -> Item { 117 | return try await client.send(expecting: Item.self) { req, encoder in 118 | req.httpMethod = "GET" 119 | req.url!.append(path: "/v1/conversations/\(id)/items/\(itemId)") 120 | try req.url!.append(queryItems: [ 121 | include.map { try URLQueryItem(name: "include", value: encoder.encodeToString($0)) }, 122 | ]) 123 | } 124 | } 125 | 126 | /// Delete an item from a conversation with the given IDs. 127 | /// 128 | /// - Parameter id: The ID of the conversation that contains the item. 129 | /// - Parameter itemId: The ID of the item to delete. 130 | public func deleteItem(id: String, itemId: String) async throws { 131 | _ = try await client.send { req, _ in 132 | req.httpMethod = "DELETE" 133 | req.url!.append(path: "/v1/conversations/\(id)/items/\(itemId)") 134 | } 135 | } 136 | } 137 | 138 | private extension ConversationsAPI { 139 | struct CreateConversationRequest: Encodable { 140 | let items: [Input.ListItem]? 141 | let metadata: [String: String]? 142 | } 143 | 144 | struct UpdateConversationRequest: Encodable { 145 | let metadata: [String: String]? 146 | } 147 | 148 | struct ListItemsRequest: Encodable { 149 | let after: String? 150 | let include: Request.Include? 151 | let limit: Int? 152 | let order: Order? 153 | } 154 | 155 | struct CreateItemsRequest: Encodable { 156 | let include: Request.Include? 157 | let items: [Input.ListItem] 158 | } 159 | } 160 | -------------------------------------------------------------------------------- /src/ResponsesAPI.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | #if canImport(FoundationNetworking) 3 | import FoundationNetworking 4 | #endif 5 | 6 | /// A Swift client for the OpenAI Responses API. 7 | public struct ResponsesAPI: Sendable { 8 | private let client: APIClient 9 | 10 | /// Creates a new `ResponsesAPI` instance using the provided `URLRequest`. 11 | /// 12 | /// You can use this initializer to use a custom base URL or custom headers. 13 | /// 14 | /// - Parameter request: The `URLRequest` to use for the API. 15 | public init(connectingTo request: URLRequest) throws(APIClient.Error) { 16 | client = try APIClient(connectingTo: request) 17 | } 18 | 19 | /// Creates a new `ResponsesAPI` instance using the provided `authToken`. 20 | /// 21 | /// You can optionally provide an `organizationId` and/or `projectId` to use with the API. 22 | /// 23 | /// - Parameter authToken: The OpenAI API key to use for authentication. 24 | /// - Parameter organizationId: The [organization](https://platform.openai.com/docs/guides/production-best-practices#setting-up-your-organization) associated with the request. 25 | /// - Parameter projectId: The project associated with the request. 26 | public init(authToken: String, organizationId: String? = nil, projectId: String? = nil) { 27 | client = APIClient(authToken: authToken, organizationId: organizationId, projectId: projectId) 28 | } 29 | 30 | /// Creates a model response. 31 | /// 32 | /// > Note: To receive a stream of tokens as they are generated, use the `stream` function instead. 33 | /// 34 | /// Provide [text](https://platform.openai.com/docs/guides/text) or [image](https://platform.openai.com/docs/guides/images) inputs to generate [text](https://platform.openai.com/docs/guides/text) or [JSON](https://platform.openai.com/docs/guides/structured-outputs) outputs. 35 | /// Have the model call your own [custom code](https://platform.openai.com/docs/guides/function-calling) or use built-in [tools](https://platform.openai.com/docs/guides/tools) like [web search](https://platform.openai.com/docs/guides/tools-web-search) or [file search](https://platform.openai.com/docs/guides/tools-file-search) to use your own data as input for the model's response. 36 | /// 37 | /// - Throws: If the request fails to send or has a non-200 status code (except for 400, which will return an OpenAI error instead). 38 | public func create(_ request: Request) async throws -> Result { 39 | var request = request 40 | request.stream = false 41 | 42 | return try await client.send(expecting: Response.ResultResponse.self) { req, encoder in 43 | req.httpMethod = "POST" 44 | req.url!.append(path: "v1/responses") 45 | req.httpBody = try encoder.encode(request) 46 | req.addValue("application/json", forHTTPHeaderField: "Content-Type") 47 | }.into() 48 | } 49 | 50 | /// Creates a model response and streams the tokens as they are generated. 51 | /// 52 | /// > Note: To receive a single response, use the `create` function instead. 53 | /// 54 | /// Provide [text](https://platform.openai.com/docs/guides/text) or [image](https://platform.openai.com/docs/guides/images) inputs to generate [text](https://platform.openai.com/docs/guides/text) or [JSON](https://platform.openai.com/docs/guides/structured-outputs) outputs. 55 | /// Have the model call your own [custom code](https://platform.openai.com/docs/guides/function-calling) or use built-in [tools](https://platform.openai.com/docs/guides/tools) like [web search](https://platform.openai.com/docs/guides/tools-web-search) or [file search](https://platform.openai.com/docs/guides/tools-file-search) to use your own data as input for the model's response. 56 | /// 57 | /// - Throws: If the request fails to send or has a non-200 status code. 58 | public func stream(_ request: Request) async throws -> AsyncThrowingStream { 59 | var request = request 60 | request.stream = true 61 | 62 | return try await client.stream(expecting: Event.self) { req, encoder in 63 | req.httpMethod = "POST" 64 | req.url!.append(path: "v1/responses") 65 | req.httpBody = try encoder.encode(request) 66 | req.addValue("application/json", forHTTPHeaderField: "Content-Type") 67 | } 68 | } 69 | 70 | /// Retrieves a model response with the given ID. 71 | /// 72 | /// - Parameter id: The ID of the response to retrieve. 73 | /// - Parameter include: Additional fields to include in the response. See `Request.Include` for available options. 74 | /// 75 | /// - Throws: If the request fails to send or has a non-200 status code (except for 400, which will return an OpenAI error instead). 76 | public func get(_ id: String, include: [Request.Include]? = nil) async throws -> Result { 77 | return try await client.send(expecting: Response.ResultResponse.self) { req, encoder in 78 | req.httpMethod = "GET" 79 | req.url!.append(path: "v1/responses/\(id)") 80 | try req.url!.append(queryItems: [ 81 | include.map { try URLQueryItem(name: "include", value: encoder.encodeToString($0)) }, 82 | ]) 83 | }.into() 84 | } 85 | 86 | /// Continues streaming a model response with the given ID. 87 | /// 88 | /// - Parameter id: The ID of the response to stream. 89 | /// - Parameter startingAfter: The sequence number of the event after which to start streaming. 90 | /// - Parameter include: Additional fields to include in the response. See `Request.Include` for available options. 91 | /// 92 | /// - Throws: If the request fails to send or has a non-200 status code. 93 | public func stream(id: String, startingAfter: Int? = nil, include: [Request.Include]? = nil) async throws -> AsyncThrowingStream { 94 | return try await client.stream(expecting: Event.self) { req, encoder in 95 | req.httpMethod = "GET" 96 | req.url!.append(path: "v1/responses/\(id)") 97 | try req.url!.append(queryItems: [ 98 | URLQueryItem(name: "stream", value: "true"), 99 | startingAfter.map { URLQueryItem(name: "starting_after", value: "\($0)") }, 100 | include.map { try URLQueryItem(name: "include", value: encoder.encodeToString($0)) }, 101 | ]) 102 | } 103 | } 104 | 105 | /// Cancels a model response with the given ID. 106 | /// 107 | /// - Parameter id: The ID of the response to cancel. 108 | /// 109 | /// Only responses created with the background parameter set to true can be cancelled. [Learn more](https://platform.openai.com/docs/guides/background). 110 | public func cancel(_ id: String) async throws { 111 | _ = try await client.send { req, _ in 112 | req.httpMethod = "POST" 113 | req.url!.append(path: "v1/responses/\(id)/cancel") 114 | } 115 | } 116 | 117 | /// Deletes a model response with the given ID. 118 | /// 119 | /// - Throws: `Error.invalidResponse` if the request fails to send or has a non-200 status code. 120 | public func delete(_ id: String) async throws { 121 | _ = try await client.send { req, _ in 122 | req.httpMethod = "DELETE" 123 | req.url!.append(path: "v1/responses/\(id)") 124 | } 125 | } 126 | 127 | /// Returns a list of input items for a given response. 128 | /// 129 | /// - Throws: If the request fails to send or has a non-200 status code. 130 | public func listInputs(_ id: String) async throws -> Input.ItemList { 131 | return try await client.send(expecting: Input.ItemList.self) { req, _ in 132 | req.httpMethod = "GET" 133 | req.url!.append(path: "/\(id)/inputs") 134 | } 135 | } 136 | 137 | /// Uploads a file for later use in the API. 138 | /// 139 | /// - Parameter file: The file to upload. 140 | /// - Parameter purpose: The intended purpose of the file. 141 | public func upload(file: File.Upload, purpose: File.Purpose = .userData) async throws -> File { 142 | let form = FormData( 143 | boundary: UUID().uuidString, 144 | entries: [file.toFormEntry(), .string(paramName: "purpose", value: purpose.rawValue)] 145 | ) 146 | 147 | return try await client.send(expecting: File.self) { req, _ in 148 | req.httpMethod = "POST" 149 | req.attach(formData: form) 150 | req.url!.append(path: "v1/files") 151 | } 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /src/Models/Config.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import MetaCodable 3 | 4 | /// Configuration options for [reasoning models](https://platform.openai.com/docs/guides/reasoning). 5 | /// Only available for o-series models. 6 | @Codable @CodingKeys(.snake_case) public struct ReasoningConfig: Equatable, Hashable, Sendable { 7 | /// Constrains effort on reasoning for [reasoning models](https://platform.openai.com/docs/guides/reasoning). 8 | /// 9 | /// Reducing reasoning effort can result in faster responses and fewer tokens used on reasoning in a response. 10 | public enum Effort: String, CaseIterable, Equatable, Hashable, Codable, Sendable { 11 | case none, minimal, low, medium, high 12 | } 13 | 14 | /// A summary of the reasoning performed by the model. 15 | /// 16 | /// This can be useful for debugging and understanding the model's reasoning process. 17 | public enum SummaryConfig: String, CaseIterable, Equatable, Hashable, Codable, Sendable { 18 | case auto 19 | case concise 20 | case detailed 21 | } 22 | 23 | /// Constrains effort on reasoning for [reasoning models](https://platform.openai.com/docs/guides/reasoning). 24 | /// 25 | /// Reducing reasoning effort can result in faster responses and fewer tokens used on reasoning in a response. 26 | public var effort: Effort? 27 | 28 | /// A summary of the reasoning performed by the model. 29 | /// 30 | /// This can be useful for debugging and understanding the model's reasoning process. 31 | public var summary: SummaryConfig? 32 | 33 | /// Creates a new `ReasoningConfig` instance. 34 | /// 35 | /// - Parameter effort: Constrains effort on reasoning for reasoning models. 36 | /// - Parameter summary: A summary of the reasoning performed by the model. 37 | public init(effort: Effort? = nil, summary: SummaryConfig? = nil) { 38 | self.effort = effort 39 | self.summary = summary 40 | } 41 | } 42 | 43 | /// Configuration options for a text response from the model. Can be plain text or structured JSON data. 44 | /// 45 | /// Learn more: 46 | /// - [Text inputs and outputs](https://platform.openai.com/docs/guides/text) 47 | /// - [Structured Outputs](https://platform.openai.com/docs/guides/structured-outputs) 48 | public struct TextConfig: Equatable, Hashable, Codable, Sendable { 49 | /// An object specifying the format that the model must output. 50 | @Codable @CodedAt("type") @CodingKeys(.snake_case) public enum Format: Equatable, Hashable, Sendable { 51 | /// Used to generate text responses. 52 | case text 53 | 54 | /// JSON Schema response format. Used to generate structured JSON responses. Learn more about [Structured Outputs](https://platform.openai.com/docs/guides/structured-outputs). 55 | /// - Parameter schema: The schema for the response format, described as a JSON Schema object. Learn how to build JSON schemas [here](https://json-schema.org/). 56 | /// - Parameter description: A description of what the response format is for, used by the model to determine how to respond in the format. 57 | /// - Parameter name: The name of the response format. Must be a-z, A-Z, 0-9, or contain underscores and dashes, with a maximum length of 64. 58 | /// - Parameter strict: Whether to enable strict schema adherence when generating the output. If set to `true`, the model will always follow the exact schema defined in the schema field. Only a subset of JSON Schema is supported when `strict` is `true`. 59 | @CodedAs("json_schema") 60 | case jsonSchema( 61 | schema: JSONSchema, 62 | description: String, 63 | name: String, 64 | strict: Bool? 65 | ) 66 | 67 | /// JSON object response format. An older method of generating JSON responses. 68 | /// 69 | /// Using `jsonSchema` is recommended for models that support it. 70 | /// 71 | /// Note that the model will not generate JSON without a system or user message instructing it to do so. 72 | @CodedAs("json_object") 73 | case jsonObject 74 | } 75 | 76 | /// An object specifying the format that the model must output. 77 | public var format: Format 78 | 79 | /// Creates a new `TextConfig` instance. 80 | /// 81 | /// - Parameter format: An object specifying the format that the model must output. 82 | public init(format: Format = .text) { 83 | self.format = format 84 | } 85 | } 86 | 87 | /// The truncation strategy to use for the model response. 88 | public enum Truncation: String, CaseIterable, Equatable, Hashable, Codable, Sendable { 89 | /// If the context of this response and previous ones exceeds the model's context window size, the model will truncate the response to fit the context window by dropping input items in the middle of the conversation. 90 | case auto 91 | 92 | /// If a model response will exceed the context window size for a model, the request will fail with a 400 error. 93 | case disabled 94 | } 95 | 96 | /// The latency to use when processing the request 97 | public enum ServiceTier: String, CaseIterable, Equatable, Hashable, Codable, Sendable { 98 | /// The request will be processed with the service tier configured in the Project settings. 99 | /// 100 | /// Unless otherwise configured, the Project will use 'default'. 101 | case auto 102 | 103 | /// The requset will be processed with the standard pricing and performance for the selected model. 104 | case `default` 105 | 106 | /// The request will be processed with the Flex Processing service tier. 107 | case flex 108 | 109 | /// The request will be processed with the Priority Processing service tier. 110 | case priority 111 | } 112 | 113 | public struct Prompt: Equatable, Hashable, Codable, Sendable { 114 | /// The unique identifier of the prompt template to use. 115 | public var id: String 116 | 117 | /// Optional version of the prompt template. 118 | public var version: String? 119 | 120 | /// Optional map of values to substitute in for variables in your prompt. 121 | /// 122 | /// The substitution values can either be strings, or other Response input types like images or files. 123 | public var variables: [String: String]? 124 | 125 | /// Creates a new `Prompt` instance. 126 | /// - Parameter id: The unique identifier of the prompt template to use. 127 | /// - Parameter version: Optional version of the prompt template. 128 | /// - Parameter variables: Optional map of values to substitute in for variables in your prompt. 129 | public init(id: String, version: String? = nil, variables: [String: String]? = nil) { 130 | self.id = id 131 | self.version = version 132 | self.variables = variables 133 | } 134 | } 135 | 136 | /// Constrains the verbosity of the model's response. 137 | public enum Verbosity: String, Equatable, Hashable, Codable, Sendable { 138 | case low, medium, high 139 | } 140 | 141 | public enum Order: String, Equatable, Hashable, Codable, Sendable { 142 | /// Return the input items in ascending order. 143 | case asc 144 | 145 | /// Return the input items in descending order. 146 | case desc 147 | } 148 | 149 | public enum CacheRetention: String, Equatable, Hashable, Codable, Sendable { 150 | case oneDay = "24h" 151 | case inMemory = "in_memory" 152 | } 153 | 154 | public extension TextConfig.Format { 155 | /// JSON Schema response format. Used to generate structured JSON responses. Learn more about [Structured Outputs](https://platform.openai.com/docs/guides/structured-outputs). 156 | /// - Parameter schemable: A type conforming to `Schemable`, which provides the schema for the response format. 157 | /// - Parameter description: A description of what the response format is for, used by the model to determine how to respond in the format. 158 | /// - Parameter name: The name of the response format. Must be a-z, A-Z, 0-9, or contain underscores and dashes, with a maximum length of 64. 159 | /// - Parameter strict: Whether to enable strict schema adherence when generating the output. If set to `true`, the model will always follow the exact schema defined in the schema field. Only a subset of JSON Schema is supported when `strict` is `true`. 160 | static func jsonSchema(_: T.Type, description: String, name: String, strict: Bool? = true) -> Self { 161 | .jsonSchema(schema: T.schema, description: description, name: name, strict: strict) 162 | } 163 | } 164 | -------------------------------------------------------------------------------- /macros/Macros/ToolMacro.swift: -------------------------------------------------------------------------------- 1 | import SwiftSyntax 2 | import SwiftDiagnostics 3 | import SwiftSyntaxMacros 4 | import SwiftSyntaxBuilder 5 | 6 | public struct ToolMacro: ExtensionMacro { 7 | public static func expansion( 8 | of _: AttributeSyntax, 9 | attachedTo declaration: some DeclGroupSyntax, 10 | providingExtensionsOf type: some TypeSyntaxProtocol, 11 | conformingTo _: [TypeSyntax], 12 | in context: some MacroExpansionContext 13 | ) throws -> [ExtensionDeclSyntax] { 14 | return try ReportableError.report(in: context, for: declaration) { 15 | guard let structDecl = declaration.as(StructDeclSyntax.self) else { 16 | throw ReportableError(errorMessage: "The @Tool macro can only be applied to structs.") 17 | } 18 | 19 | let functionDecl = try getFunctionDeclaration(from: structDecl) 20 | let functionDocString = DocString.parse(functionDecl.docString) 21 | 22 | if functionDocString.isMissing { 23 | context.diagnose(Diagnostic( 24 | node: declaration, 25 | message: MacroExpansionWarningMessage("Make sure to document the `call` function of your tool to help the model understand its purpose and usage.") 26 | )) 27 | } else { 28 | for parameter in functionDecl.signature.parameterClause.parameters { 29 | if parameter.firstName.text != "_", !parameter.firstName.text.isPlaceholder, functionDocString?.for(properties: parameter.firstName.text, parameter.secondName?.text) == nil { 30 | context.diagnose(Diagnostic( 31 | node: declaration, 32 | message: MacroExpansionWarningMessage("You should document the `\(parameter.firstName.text)` parameter to help the model understand its usage.") 33 | )) 34 | } 35 | } 36 | } 37 | 38 | return try [ 39 | ExtensionDeclSyntax(extending: type, inheritsTypes: ["Toolable"]) { 40 | try addTypes(reading: functionDecl) 41 | try addProperties(reading: structDecl, and: functionDocString) 42 | try addArguments(reading: functionDecl, and: functionDocString, forwarding: context) 43 | try addFunction(reading: functionDecl, and: functionDocString) 44 | }, 45 | ] 46 | } withDefault: { [ExtensionDeclSyntax(extending: type, inheritsTypes: ["Toolable"]) {}] } 47 | } 48 | 49 | private static func getFunctionDeclaration(from structDecl: StructDeclSyntax) throws -> FunctionDeclSyntax { 50 | let functionDecls = structDecl.memberBlock.members.filter { $0.decl.as(FunctionDeclSyntax.self)?.name.text == "call" } 51 | 52 | guard functionDecls.count <= 1 else { 53 | throw ReportableError(errorMessage: "Structs annotated with the @Tool macro may only contain a single `call` function.") 54 | } 55 | guard let functionDecl = functionDecls.first?.decl.as(FunctionDeclSyntax.self) else { 56 | throw try ReportableError( 57 | errorMessage: "Structs annotated with the @Tool macro must contain a `call` function.", 58 | fixIts: [ 59 | FixIt(message: MacroExpansionFixItMessage("Add a `call` function"), changes: [ 60 | FixIt.Change.replace(oldNode: Syntax(structDecl), newNode: Syntax(tap(structDecl) { structDecl in 61 | try structDecl.memberBlock.members.append(MemberBlockItemSyntax(leadingTrivia: .newline, decl: FunctionDeclSyntax(""" 62 | /// <#Describe the purpose of your tool to help the model understand when to use it#> 63 | func call() async throws { 64 | // <#The implementation of your tool call, which can optionally return information to the model#> 65 | } 66 | """), trailingTrivia: .newline)) 67 | })), 68 | ]), 69 | ] 70 | ) 71 | } 72 | 73 | if functionDecl.signature.parameterClause.parameters.isEmpty { 74 | throw ReportableError(errorMessage: "The `call` function must have at least one parameter.") 75 | } 76 | 77 | guard !functionDecl.signature.parameterClause.parameters.allSatisfy({ 78 | $0.firstName.text == "parameters" && $0.type.as(IdentifierTypeSyntax.self)?.name.text == "Arguments" 79 | }) else { 80 | throw ReportableError( 81 | errorMessage: "When using the @Tool macro, use function parameters directly instead of manually creating an `Arguments` struct." 82 | ) 83 | } 84 | 85 | return functionDecl 86 | } 87 | 88 | private static func addTypes(reading functionDecl: FunctionDeclSyntax) throws -> [TypeAliasDeclSyntax] { 89 | let returnType = functionDecl.signature.returnClause?.type 90 | let errorType = switch functionDecl.signature.effectSpecifiers?.throwsClause { 91 | case .none: IdentifierTypeSyntax(name: TokenSyntax(stringLiteral: "Never")) 92 | case let .some(throwsClause): 93 | if let type = throwsClause.type { type.as(IdentifierTypeSyntax.self)! } 94 | else { IdentifierTypeSyntax(name: TokenSyntax(stringLiteral: "Swift.Error")) } 95 | } 96 | 97 | return [ 98 | TypeAliasDeclSyntax(name: TokenSyntax(stringLiteral: "Error"), initializer: TypeInitializerClauseSyntax(value: errorType)), 99 | TypeAliasDeclSyntax(name: TokenSyntax(stringLiteral: "Output"), initializer: TypeInitializerClauseSyntax(value: returnType ?? "NullableVoid"), trailingTrivia: .newlines(2)), 100 | ] 101 | } 102 | 103 | private static func addProperties( 104 | reading structDecl: StructDeclSyntax, 105 | and functionDocString: DocString? 106 | ) throws -> [VariableDeclSyntax] { 107 | var properties: [VariableDeclSyntax] = [] 108 | let structDeclarations = structDecl.memberBlock.members 109 | 110 | if !structDeclarations.declaresVariable(named: "name") { 111 | try properties.append(VariableDeclSyntax("var name: String { \(literal: structDecl.name.text) }")) 112 | } 113 | 114 | if !structDeclarations.declaresVariable(named: "description"), let description = functionDocString?.docString ?? structDecl.docString, !description.isPlaceholder { 115 | try properties.append(VariableDeclSyntax("var description: String { \(literal: description) }")) 116 | } 117 | 118 | return properties.map(tapping: { $0.trailingTrivia = .newlines(2) }) 119 | } 120 | 121 | private static func addArguments( 122 | reading functionDecl: FunctionDeclSyntax, 123 | and functionDocString: DocString?, 124 | forwarding _: some MacroExpansionContext 125 | ) throws -> StructDeclSyntax { 126 | var structDecl = try StructDeclSyntax(name: TokenSyntax(stringLiteral: "Arguments")) { 127 | try functionDecl.signature.parameterClause.parameters.enumerated().map { i, parameter in 128 | if parameter.firstName.text == "_" { 129 | throw ReportableError(errorMessage: "All parameters of the `call` function must have a name. The parameter at index \(i) does not have a name.") 130 | } 131 | 132 | var decl = try VariableDeclSyntax("let \(raw: parameter.firstName.text): \(parameter.type)") 133 | 134 | if let docString = functionDocString?.for(properties: parameter.firstName.text, parameter.secondName?.text), !docString.isEmpty { 135 | decl.leadingTrivia = .docLineComment("/// \(docString)").merging(.newline) 136 | } 137 | 138 | return decl 139 | } 140 | } 141 | 142 | let schemaDecl = try StructSchemaGenerator(fromStruct: structDecl).makeSchema() 143 | 144 | structDecl.trailingTrivia = .newlines(2) 145 | structDecl.memberBlock.members.append(MemberBlockItemSyntax(decl: schemaDecl.with(\.leadingTrivia, .newlines(2)))) 146 | structDecl.inheritanceClause = InheritanceClauseSyntax { 147 | InheritedTypeSyntax(type: IdentifierTypeSyntax(name: "Decodable")) 148 | InheritedTypeSyntax(type: IdentifierTypeSyntax(name: "Schemable")) 149 | } 150 | 151 | return structDecl 152 | } 153 | 154 | private static func addFunction( 155 | reading functionDecl: FunctionDeclSyntax, 156 | and _: DocString? 157 | ) throws -> FunctionDeclSyntax { 158 | let arguments = LabeledExprListSyntax(itemsBuilder: { 159 | functionDecl.signature.parameterClause.parameters.compactMap { parameter in 160 | let expression: ExprSyntax = "parameters.\(raw: parameter.firstName.text)" 161 | 162 | return LabeledExprSyntax(label: parameter.firstName.text, expression: expression) 163 | } 164 | }) 165 | 166 | var functionImpl = try FunctionDeclSyntax(""" 167 | func call(parameters: Arguments) async throws(Error) -> Output { 168 | try await self.call(\(arguments)) 169 | } 170 | """) 171 | 172 | if functionDecl.signature.returnClause == nil { 173 | functionImpl.body!.statements.append(CodeBlockItemSyntax("return NullableVoid()")) 174 | } 175 | 176 | return functionImpl 177 | } 178 | } 179 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # OpenAI Responses API 2 | > Hand-crafted Swift SDK for the [OpenAI Responses API](https://platform.openai.com/docs/api-reference/responses). 3 | 4 | [![Swift Version](https://img.shields.io/endpoint?url=https%3A%2F%2Fswiftpackageindex.com%2Fapi%2Fpackages%2Fm1guelpf%2Fswift-openai-responses%2Fbadge%3Ftype%3Dswift-versions&color=brightgreen)](https://swiftpackageindex.com/m1guelpf/swift-openai-responses) 5 | [![Swift Platforms](https://img.shields.io/endpoint?url=https%3A%2F%2Fswiftpackageindex.com%2Fapi%2Fpackages%2Fm1guelpf%2Fswift-openai-responses%2Fbadge%3Ftype%3Dplatforms&color=brightgreen)](https://swiftpackageindex.com/m1guelpf/swift-openai-responses) 6 | [![CI](https://github.com/m1guelpf/swift-openai-responses/actions/workflows/test.yml/badge.svg)](https://github.com/m1guelpf/swift-openai-responses/actions/workflows/test.yml) 7 | 8 | This package contains: 9 | - A fully typed client for the Responses API that _feels_ Swifty 10 | - `Schema` and `Tool` macros, providing elegant ways to define tools and structured responses. 11 | - A `Conversation` class, handling everything you need for multi-turn streaming conversations in your views. 12 | 13 | 14 | ## Installation 15 | 16 |
17 | 18 | 19 | Swift Package Manager 20 | 21 | 22 | Add the following to your `Package.swift`: 23 | 24 | ```swift 25 | dependencies: [ 26 | .package(url: "https://github.com/m1guelpf/swift-openai-responses.git", .branch("main")) 27 | ] 28 | ``` 29 | 30 |
31 |
32 | 33 | Installing through XCode 34 | 35 | - File > Swift Packages > Add Package Dependency 36 | - Add https://github.com/m1guelpf/swift-openai-responses.git 37 | - Select "Branch" with "main" 38 | 39 |
40 | 41 |
42 | 43 | CocoaPods 44 | 45 | Ask ChatGPT to help you migrate away from CocoaPods. 46 | 47 |
48 | 49 | ## Getting started 🚀 50 | 51 | You can build an iMessage-like app with built-in AI chat in 50 lines of code (UI included!): 52 | 53 | ```swift 54 | import OpenAI 55 | import SwiftUI 56 | 57 | struct ContentView: View { 58 | @State private var newMessage: String = "" 59 | @State private var conversation = Conversation(authToken: OPENAI_KEY, using: .gpt5) 60 | 61 | var body: some View { 62 | VStack(spacing: 0) { 63 | ScrollView { 64 | VStack(spacing: 12) { 65 | ForEach(conversation.messages, id: \.self) { message in 66 | MessageBubble(message: message) 67 | } 68 | } 69 | .padding() 70 | } 71 | 72 | HStack(spacing: 12) { 73 | HStack { 74 | TextField("Chat", text: $newMessage, onCommit: sendMessage) 75 | .frame(height: 40) 76 | .submitLabel(.send) 77 | 78 | if newMessage != "" { 79 | Button(action: sendMessage) { 80 | Image(systemName: "arrow.up.circle.fill") 81 | .resizable() 82 | .aspectRatio(contentMode: .fill) 83 | .frame(width: 28, height: 28) 84 | .foregroundStyle(.white, .blue) 85 | } 86 | } 87 | } 88 | .padding(.leading) 89 | .padding(.trailing, 6) 90 | .overlay(RoundedRectangle(cornerRadius: 20).stroke(.quaternary, lineWidth: 1)) 91 | } 92 | .padding() 93 | } 94 | .navigationTitle("Chat") 95 | .navigationBarTitleDisplayMode(.inline) 96 | } 97 | 98 | func sendMessage() { 99 | guard newMessage != "" else { return } 100 | 101 | conversation.send(text: newMessage) 102 | newMessage = "" 103 | } 104 | } 105 | ``` 106 | 107 | ## Architecture 108 | 109 | ### `Conversation` 110 | 111 | The Conversation class provides a high-level interface for managing a conversation with the model. It wraps the `ResponsesAPI` class and handles the details of sending and receiving messages, as well as managing the conversation history. 112 | 113 | #### Configuring the conversation 114 | 115 | To create a `Conversation` instance, all you need is an OpenAI API key, and the model you will be talking to: 116 | 117 | ```swift 118 | @State private var conversation = Conversation(authToken: OPENAI_API_KEY, using: .gpt5) 119 | ``` 120 | 121 | You can optionally provide a closure to configure the conversation, adding a system prompt or tools for the model to use: 122 | 123 | ```swift 124 | @State private var conversation = Conversation(authToken: OPENAI_API_KEY, using: .gpt5) { config in 125 | // configure the model's behaviour 126 | config.instructions = "You are a coding assistant that talks like a pirate" 127 | 128 | // allow the model to browse the web 129 | config.tools = [.webSearch()] 130 | } 131 | ``` 132 | 133 | Your configuration will be reused for all subsequent messages, but you can always change any of the properties (including which model you're talking to!) mid-conversation: 134 | 135 | ```swift 136 | // update a bunch of properties at once 137 | conversation.updateConfig { config in 138 | config.model = .o3Mini 139 | } 140 | 141 | // or update them directly 142 | conversation.truncation = .auto 143 | ``` 144 | 145 | #### Sending messages 146 | 147 | Your `Conversation` instance contains various helpers to make communicating with the model easier. For example, you can send a simple text message like this: 148 | 149 | ```swift 150 | conversation.send(text: "Hey!") 151 | ``` 152 | 153 | There are also helpers for providing the output of a tool call or computer use call: 154 | 155 | ```swift 156 | conversation.send(functionCallOutput: .init(callId: callId, output: "{ ... }")) 157 | conversation.send(computerCallOutput: .init(callId: callId, output: .screenshot(fileId: "..."))) 158 | ``` 159 | 160 | For more complex use cases, you can construct the `Input` yourself: 161 | 162 | ```swift 163 | conversation.send([ 164 | .message(content: [ 165 | .image(fileId: "..."), 166 | .text("Take a look at this image and tell me what you see"), 167 | ]), 168 | ]) 169 | ``` 170 | 171 | #### Reading messages 172 | 173 | You can access the messages in the conversation through the messages property. Note that this won't include function calls and its responses, only the messages between the user and the model. To access the full conversation history, use the `entries` property. For example: 174 | 175 | ```swift 176 | ScrollView { 177 | ScrollViewReader { scrollView in 178 | VStack(spacing: 12) { 179 | ForEach(conversation.messages, id: \.self) { message in 180 | MessageBubble(message: message) 181 | .id(message.hashValue) 182 | } 183 | } 184 | .onReceive(conversation.messages.publisher) { _ in 185 | withAnimation { scrollView.scrollTo(conversation.messages.last?.hashValue, anchor: .center) } 186 | } 187 | } 188 | } 189 | ``` 190 | 191 | ### `ResponsesAPI` 192 | 193 | #### Creating a new instance 194 | 195 | To interact with the Responses API directly, create a new instance of `ResponsesAPI` with your API key: 196 | 197 | ```swift 198 | let client = ResponsesAPI(authToken: YOUR_OPENAI_API_TOKEN) 199 | ``` 200 | 201 | Optionally, you can provide an Organization ID and/or project ID: 202 | 203 | ```swift 204 | let client = ResponsesAPI( 205 | authToken: YOUR_OPENAI_API_KEY, 206 | organizationId: YOUR_ORGANIZATION_ID, 207 | projectId: YOUR_PROJECT_ID 208 | ) 209 | ``` 210 | 211 | For more advanced use cases like connecting to a custom server, you can customize the `URLRequest` used to connect to the API: 212 | 213 | ```swift 214 | let urlRequest = URLRequest(url: MY_CUSTOM_ENDPOINT) 215 | urlRequest.addValue("Bearer \(YOUR_API_KEY)", forHTTPHeaderField: "Authorization") 216 | 217 | let client = ResponsesAPI(connectingTo: urlRequest) 218 | ``` 219 | 220 | #### Creating Responses 221 | 222 | To create a new response, call the `create` method with a `Request` instance: 223 | 224 | ```swift 225 | let response = try await client.create(Request( 226 | model: .gpt5, 227 | input: .text("Are semicolons optional in JavaScript?"), 228 | instructions: "You are a coding assistant that talks like a pirate" 229 | )) 230 | ``` 231 | 232 | There are plenty of helper methods to make creating your `Request` easier. Look around the [package docs](https://swiftpackageindex.com/m1guelpf/swift-openai-responses/documentation/openai) for more. 233 | 234 | #### Streaming 235 | 236 | To stream back the response as it is generated, use the `stream` method: 237 | 238 | ```swift 239 | let stream = try await client.stream(Request( 240 | model: .gpt5, 241 | input: .text("Are semicolons optional in JavaScript?"), 242 | instructions: "You are a coding assistant that talks like a pirate" 243 | )) 244 | 245 | for try await event in stream { 246 | switch event { 247 | // ... 248 | } 249 | } 250 | ``` 251 | 252 | #### Uploading files 253 | 254 | While uploading files is not part of the Responses API, you'll need it for sending content to the model (like images, PDFs, etc.). You can upload a file to the model like so: 255 | 256 | ```swift 257 | let file = try await client.upload(file: .file(name: "image.png", contents: imageData, contentType: "image/png")) 258 | 259 | // then, use it on a message 260 | try await client.create(Request( 261 | model: .gpt5, 262 | input: .message(content: [ 263 | .image(fileId: file.id), 264 | .text("Take a look at this image and tell me what you see"), 265 | ]), 266 | )) 267 | ``` 268 | 269 | You can also load files directly from the user's filesystem or the web: 270 | 271 | ```swift 272 | let file = try await client.upload(file: .url(URL(string: "https://example.com/file.pdf")!, contentType: "application/pdf")) 273 | ``` 274 | 275 | #### Other Stuff 276 | 277 | You can retrieve a previously-created response by calling the `get` method with the response ID: 278 | 279 | ```swift 280 | let response = try await client.get("resp_...") 281 | ``` 282 | 283 | You can delete a previously-created response from OpenAI's servers by calling the `delete` method with the response ID: 284 | 285 | ```swift 286 | try await client.delete("resp_...") 287 | ``` 288 | 289 | ## License 290 | 291 | This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details. 292 | -------------------------------------------------------------------------------- /src/Models/JSONSchema.swift: -------------------------------------------------------------------------------- 1 | /// Represents a JSON Schema for validating JSON data structures. 2 | public indirect enum JSONSchema: Equatable, Hashable, Sendable { 3 | /// Represents the format of a string in JSON Schema. 4 | public enum StringFormat: String, Codable, Hashable, Equatable, Sendable { 5 | case ipv4 6 | case ipv6 7 | case uuid 8 | case date 9 | case time 10 | case email 11 | case duration 12 | case hostname 13 | case dateTime = "date-time" 14 | } 15 | 16 | case null(description: String? = nil) 17 | case boolean(description: String? = nil) 18 | case anyOf([JSONSchema], description: String? = nil) 19 | case `enum`(cases: [String], description: String? = nil) 20 | case object(properties: [String: JSONSchema], description: String? = nil) 21 | case string(pattern: String? = nil, format: StringFormat? = nil, description: String? = nil) 22 | case array(of: JSONSchema, minItems: Int? = nil, maxItems: Int? = nil, description: String? = nil) 23 | case number( 24 | multipleOf: Int? = nil, 25 | minimum: Int? = nil, 26 | exclusiveMinimum: Int? = nil, 27 | maximum: Int? = nil, 28 | exclusiveMaximum: Int? = nil, 29 | description: String? = nil 30 | ) 31 | case integer( 32 | multipleOf: Int? = nil, 33 | minimum: Int? = nil, 34 | exclusiveMinimum: Int? = nil, 35 | maximum: Int? = nil, 36 | exclusiveMaximum: Int? = nil, 37 | description: String? = nil 38 | ) 39 | 40 | var description: String? { 41 | switch self { 42 | case let .null(description), 43 | let .boolean(description), 44 | let .anyOf(_, description), 45 | let .enum(_, description), 46 | let .object(_, description), 47 | let .string(_, _, description), 48 | let .array(_, _, _, description), 49 | let .number(_, _, _, _, _, description), 50 | let .integer(_, _, _, _, _, description): return description 51 | } 52 | } 53 | 54 | func withDescription(_: String?) -> JSONSchema { 55 | switch self { 56 | case .null: return .null(description: description) 57 | case .boolean: return .boolean(description: description) 58 | case let .anyOf(cases, _): return .anyOf(cases, description: description) 59 | case let .enum(cases, _): return .enum(cases: cases, description: description) 60 | case let .object(properties, _): return .object(properties: properties, description: description) 61 | case let .string(pattern, format, _): return .string(pattern: pattern, format: format, description: description) 62 | case let .array(of: items, minItems, maxItems, _): 63 | return .array(of: items, minItems: minItems, maxItems: maxItems, description: description) 64 | case let .number(multipleOf, maximum, exclusiveMaximum, minimum, exclusiveMinimum, _): 65 | return .number( 66 | multipleOf: multipleOf, minimum: minimum, exclusiveMinimum: exclusiveMinimum, 67 | maximum: maximum, exclusiveMaximum: exclusiveMaximum, description: description 68 | ) 69 | case let .integer(multipleOf, maximum, exclusiveMaximum, minimum, exclusiveMinimum, _): 70 | return .integer( 71 | multipleOf: multipleOf, minimum: minimum, exclusiveMinimum: exclusiveMinimum, 72 | maximum: maximum, exclusiveMaximum: exclusiveMaximum, description: description 73 | ) 74 | } 75 | } 76 | } 77 | 78 | extension JSONSchema: Codable { 79 | private enum CodingKeys: String, CodingKey { 80 | case type 81 | case items 82 | case `enum` 83 | case anyOf 84 | case format 85 | case pattern 86 | case required 87 | case minItems 88 | case maxItems 89 | case minimum 90 | case maximum 91 | case properties 92 | case multipleOf 93 | case description 94 | case exclusiveMinimum 95 | case exclusiveMaximum 96 | case additionalProperties 97 | } 98 | 99 | public func encode(to encoder: Encoder) throws { 100 | var container = encoder.container(keyedBy: CodingKeys.self) 101 | 102 | switch self { 103 | case let .null(description): 104 | try container.encode("null", forKey: .type) 105 | if let description = description { try container.encode(description, forKey: .description) } 106 | case let .boolean(description): 107 | try container.encode("boolean", forKey: .type) 108 | if let description = description { try container.encode(description, forKey: .description) } 109 | case let .anyOf(cases, description): 110 | try container.encode(cases, forKey: .anyOf) 111 | if let description = description { try container.encode(description, forKey: .description) } 112 | case let .enum(cases, description): 113 | try container.encode(cases, forKey: .enum) 114 | try container.encode("string", forKey: .type) 115 | if let description { try container.encode(description, forKey: .description) } 116 | case let .object(properties, description): 117 | try container.encode("object", forKey: .type) 118 | try container.encode(properties, forKey: .properties) 119 | try container.encode(false, forKey: .additionalProperties) 120 | try container.encode(Array(properties.keys), forKey: .required) 121 | if let description { try container.encode(description, forKey: .description) } 122 | case let .string(pattern, format, description): 123 | try container.encode("string", forKey: .type) 124 | if let pattern = pattern { try container.encode(pattern, forKey: .pattern) } 125 | if let description { try container.encode(description, forKey: .description) } 126 | if let format = format { try container.encode(format.rawValue, forKey: .format) } 127 | case let .array(of: items, minItems, maxItems, description): 128 | try container.encode(items, forKey: .items) 129 | try container.encode("array", forKey: .type) 130 | if let description { try container.encode(description, forKey: .description) } 131 | if let minItems = minItems { try container.encode(minItems, forKey: .minItems) } 132 | if let maxItems = maxItems { try container.encode(maxItems, forKey: .maxItems) } 133 | case let .number(multipleOf, maximum, exclusiveMaximum, minimum, exclusiveMinimum, description): 134 | try container.encode("number", forKey: .type) 135 | if let minimum = minimum { try container.encode(minimum, forKey: .minimum) } 136 | if let maximum = maximum { try container.encode(maximum, forKey: .maximum) } 137 | if let description { try container.encode(description, forKey: .description) } 138 | if let multipleOf = multipleOf { try container.encode(multipleOf, forKey: .multipleOf) } 139 | if let exclusiveMaximum = exclusiveMaximum { try container.encode(exclusiveMaximum, forKey: .exclusiveMaximum) } 140 | if let exclusiveMinimum = exclusiveMinimum { try container.encode(exclusiveMinimum, forKey: .exclusiveMinimum) } 141 | case let .integer(multipleOf, maximum, exclusiveMaximum, minimum, exclusiveMinimum, description): 142 | try container.encode("integer", forKey: .type) 143 | if let minimum = minimum { try container.encode(minimum, forKey: .minimum) } 144 | if let maximum = maximum { try container.encode(maximum, forKey: .maximum) } 145 | if let description { try container.encode(description, forKey: .description) } 146 | if let multipleOf = multipleOf { try container.encode(multipleOf, forKey: .multipleOf) } 147 | if let exclusiveMaximum = exclusiveMaximum { try container.encode(exclusiveMaximum, forKey: .exclusiveMaximum) } 148 | if let exclusiveMinimum = exclusiveMinimum { try container.encode(exclusiveMinimum, forKey: .exclusiveMinimum) } 149 | } 150 | } 151 | 152 | public init(from decoder: any Decoder) throws { 153 | let container = try decoder.container(keyedBy: CodingKeys.self) 154 | let description = try container.decodeIfPresent(String.self, forKey: .description) 155 | 156 | if let anyOf = try container.decodeIfPresent([JSONSchema].self, forKey: .anyOf) { 157 | self = .anyOf(anyOf, description: description) 158 | return 159 | } 160 | 161 | let type = try container.decode(String.self, forKey: .type) 162 | 163 | if type == "null" { 164 | self = .null(description: description) 165 | return 166 | } 167 | 168 | if type == "boolean" { 169 | self = .boolean(description: description) 170 | return 171 | } 172 | 173 | if type == "object" { 174 | self = try .object( 175 | properties: container.decode([String: JSONSchema].self, forKey: .properties), 176 | description: description 177 | ) 178 | return 179 | } 180 | 181 | if type == "string", let enumCases = try container.decodeIfPresent([String].self, forKey: .enum) { 182 | self = .enum(cases: enumCases, description: description) 183 | return 184 | } 185 | 186 | if type == "string" { 187 | self = try .string( 188 | pattern: container.decodeIfPresent(String.self, forKey: .pattern), 189 | format: container.decodeIfPresent(StringFormat.self, forKey: .format), 190 | description: description 191 | ) 192 | return 193 | } 194 | 195 | if type == "array" { 196 | self = try .array( 197 | of: container.decode(JSONSchema.self, forKey: .items), 198 | minItems: container.decodeIfPresent(Int.self, forKey: .minItems), 199 | maxItems: container.decodeIfPresent(Int.self, forKey: .maxItems), 200 | description: description 201 | ) 202 | return 203 | } 204 | 205 | if type == "number" { 206 | self = try .number( 207 | multipleOf: container.decodeIfPresent(Int.self, forKey: .multipleOf), 208 | minimum: container.decodeIfPresent(Int.self, forKey: .minimum), 209 | exclusiveMinimum: container.decodeIfPresent(Int.self, forKey: .exclusiveMinimum), 210 | maximum: container.decodeIfPresent(Int.self, forKey: .maximum), 211 | exclusiveMaximum: container.decodeIfPresent(Int.self, forKey: .exclusiveMaximum), 212 | description: description 213 | ) 214 | return 215 | } 216 | 217 | if type == "integer" { 218 | self = try .integer( 219 | multipleOf: container.decodeIfPresent(Int.self, forKey: .multipleOf), 220 | minimum: container.decodeIfPresent(Int.self, forKey: .minimum), 221 | exclusiveMinimum: container.decodeIfPresent(Int.self, forKey: .exclusiveMinimum), 222 | maximum: container.decodeIfPresent(Int.self, forKey: .maximum), 223 | exclusiveMaximum: container.decodeIfPresent(Int.self, forKey: .exclusiveMaximum), 224 | description: description 225 | ) 226 | return 227 | } 228 | 229 | throw DecodingError.dataCorruptedError(forKey: .type, in: container, debugDescription: "Unsupported schema type") 230 | } 231 | } 232 | -------------------------------------------------------------------------------- /tests/SchemaMacroTests.swift: -------------------------------------------------------------------------------- 1 | import Macros 2 | import Testing 3 | import SwiftSyntax 4 | import MacroTesting 5 | 6 | @Suite(.macros([SchemaMacro.self, ArraySchemaMacro.self, StringSchemaMacro.self, NumberSchemaMacro.self], record: .missing)) 7 | struct SchemaMacroTests { 8 | @Test("Properly generates schema for a struct with primitives") 9 | func structSchemaWithPrimitives() { 10 | assertMacro { 11 | """ 12 | /// Represents a user in the system. 13 | @Schema 14 | struct User { 15 | /// Unique identifier for the user. 16 | let id: Int 17 | let username: String 18 | let isAdmin: Bool 19 | let balance: Float 20 | /// Flags associated with the user. 21 | let flags: [String] 22 | let creditScore: Double? 23 | } 24 | """ 25 | } expansion: { 26 | """ 27 | /// Represents a user in the system. 28 | struct User { 29 | /// Unique identifier for the user. 30 | let id: Int 31 | let username: String 32 | let isAdmin: Bool 33 | let balance: Float 34 | /// Flags associated with the user. 35 | let flags: [String] 36 | let creditScore: Double? 37 | } 38 | 39 | extension User: Schemable { 40 | static var schema: JSONSchema { 41 | .object(properties: [ 42 | "id": .integer(description: "Unique identifier for the user."), 43 | "username": .string(description: nil), 44 | "isAdmin": .boolean(description: nil), 45 | "balance": .number(description: nil), 46 | "flags": .array(of: .string(description: nil), description: "Flags associated with the user."), 47 | "creditScore": .anyOf([.number(description: nil), .null], description: nil), 48 | ], description: "Represents a user in the system.") 49 | } 50 | } 51 | """ 52 | } 53 | } 54 | 55 | @Test("Treats default values in struct as optional") 56 | func structSchemaWithDefault() async throws { 57 | assertMacro { 58 | """ 59 | @Schema 60 | struct UserPermissions { 61 | /// Whether the user is an admin. 62 | let isAdmin: Bool = false 63 | let flags: Float 64 | } 65 | """ 66 | } expansion: { 67 | """ 68 | struct UserPermissions { 69 | /// Whether the user is an admin. 70 | let isAdmin: Bool = false 71 | let flags: Float 72 | } 73 | 74 | extension UserPermissions: Schemable { 75 | static var schema: JSONSchema { 76 | .object(properties: [ 77 | "isAdmin": .anyOf([.boolean(description: nil), .null], description: "Whether the user is an admin."), 78 | "flags": .number(description: nil), 79 | ], description: nil) 80 | } 81 | } 82 | """ 83 | } 84 | } 85 | 86 | @Test("Handles nested structs with primitives") 87 | func structContainingStruct() { 88 | assertMacro { 89 | """ 90 | @Schema 91 | struct Address { 92 | let street: String 93 | let city: String 94 | let zipCode: Int 95 | } 96 | 97 | @Schema 98 | struct UserProfile { 99 | let name: String 100 | let age: Int 101 | /// The user's address. 102 | let address: Address 103 | } 104 | """ 105 | } expansion: { 106 | """ 107 | struct Address { 108 | let street: String 109 | let city: String 110 | let zipCode: Int 111 | } 112 | struct UserProfile { 113 | let name: String 114 | let age: Int 115 | /// The user's address. 116 | let address: Address 117 | } 118 | 119 | extension Address: Schemable { 120 | static var schema: JSONSchema { 121 | .object(properties: [ 122 | "street": .string(description: nil), 123 | "city": .string(description: nil), 124 | "zipCode": .integer(description: nil), 125 | ], description: nil) 126 | } 127 | } 128 | 129 | extension UserProfile: Schemable { 130 | static var schema: JSONSchema { 131 | .object(properties: [ 132 | "name": .string(description: nil), 133 | "age": .integer(description: nil), 134 | "address": Address.schema(description: "The user's address."), 135 | ], description: nil) 136 | } 137 | } 138 | """ 139 | } 140 | } 141 | 142 | @Test("Ignores computed properties in struct") 143 | func structWithComputedProperty() { 144 | assertMacro { 145 | """ 146 | @Schema 147 | struct User { 148 | let id: Int 149 | let username: String 150 | 151 | /// Computed property that returns the user's full name. 152 | var fullName: String { 153 | return "\\(username) \\(id)" 154 | } 155 | } 156 | """ 157 | } expansion: { 158 | """ 159 | struct User { 160 | let id: Int 161 | let username: String 162 | 163 | /// Computed property that returns the user's full name. 164 | var fullName: String { 165 | return "\\(username) \\(id)" 166 | } 167 | } 168 | 169 | extension User: Schemable { 170 | static var schema: JSONSchema { 171 | .object(properties: [ 172 | "id": .integer(description: nil), 173 | "username": .string(description: nil), 174 | ], description: nil) 175 | } 176 | } 177 | """ 178 | } 179 | } 180 | 181 | @Test("Allows customizing string schema with @StringSchema") 182 | func structCustomStringSchema() { 183 | assertMacro { 184 | """ 185 | @Schema 186 | struct Server { 187 | /// The hostname of the server. 188 | @StringSchema(pattern: "^[a-zA-Z0-9_]+$", format: .hostname) 189 | let host: String 190 | } 191 | """ 192 | } expansion: { 193 | """ 194 | struct Server { 195 | /// The hostname of the server. 196 | let host: String 197 | } 198 | 199 | extension Server: Schemable { 200 | static var schema: JSONSchema { 201 | .object(properties: [ 202 | "host": .string(pattern: "^[a-zA-Z0-9_]+$", format: .hostname, description: "The hostname of the server."), 203 | ], description: nil) 204 | } 205 | } 206 | """ 207 | } 208 | } 209 | 210 | @Test("Allows customizing array schema with @ArraySchema") 211 | func structCustomArraySchema() { 212 | assertMacro { 213 | """ 214 | @Schema 215 | struct Post { 216 | /// A list of tags associated with the post. 217 | @ArraySchema(minItems: 1, maxItems: 10) 218 | let tags: [String] 219 | } 220 | """ 221 | } expansion: { 222 | """ 223 | struct Post { 224 | /// A list of tags associated with the post. 225 | let tags: [String] 226 | } 227 | 228 | extension Post: Schemable { 229 | static var schema: JSONSchema { 230 | .object(properties: [ 231 | "tags": .array(of: .string(description: nil), minItems: 1, maxItems: 10, description: "A list of tags associated with the post."), 232 | ], description: nil) 233 | } 234 | } 235 | """ 236 | } 237 | } 238 | 239 | @Test("Allows customizing number schema with @NumberSchema") 240 | func structCustomNumberSchema() { 241 | assertMacro { 242 | """ 243 | @Schema 244 | struct Product { 245 | @NumberSchema(maximum: 100) 246 | let id: Int 247 | /// The price of the product. 248 | @NumberSchema(multipleOf: 0.01, minimum: 0.0) 249 | let price: Double 250 | } 251 | """ 252 | } expansion: { 253 | """ 254 | struct Product { 255 | let id: Int 256 | /// The price of the product. 257 | let price: Double 258 | } 259 | 260 | extension Product: Schemable { 261 | static var schema: JSONSchema { 262 | .object(properties: [ 263 | "id": .integer(maximum: 100, description: nil), 264 | "price": .number(multipleOf: 0.01, minimum: 0.0, description: "The price of the product."), 265 | ], description: nil) 266 | } 267 | } 268 | """ 269 | } 270 | } 271 | 272 | @Test("Attribute macros get propagated correctly") 273 | func structWithNestedAttributes() { 274 | assertMacro { 275 | """ 276 | @Schema 277 | struct User { 278 | /// A collection of IDs of posts the user has liked. 279 | @NumberSchema(multipleOf: 2, minimum: 2) 280 | let likedPosts: [Int] 281 | 282 | /// The user's unique identifier. 283 | @ArraySchema(minItems: 1, maxItems: 5) 284 | @StringSchema(pattern: .uuid) 285 | let identities: [String]? 286 | 287 | /// The user's email address. 288 | @StringSchema(format: .email) 289 | let email: String? 290 | } 291 | """ 292 | } expansion: { 293 | """ 294 | struct User { 295 | /// A collection of IDs of posts the user has liked. 296 | let likedPosts: [Int] 297 | 298 | /// The user's unique identifier. 299 | let identities: [String]? 300 | 301 | /// The user's email address. 302 | let email: String? 303 | } 304 | 305 | extension User: Schemable { 306 | static var schema: JSONSchema { 307 | .object(properties: [ 308 | "likedPosts": .array(of: .integer(multipleOf: 2, minimum: 2, description: nil), description: "A collection of IDs of posts the user has liked."), 309 | "identities": .anyOf([.array(of: .string(pattern: .uuid, description: nil), minItems: 1, maxItems: 5, description: nil), .null], description: "The user's unique identifier."), 310 | "email": .anyOf([.string(format: .email, description: nil), .null], description: "The user's email address."), 311 | ], description: nil) 312 | } 313 | } 314 | """ 315 | } 316 | } 317 | 318 | @Test("Falls back to schema property for non-primitive types") 319 | func structDefaultsToSchema() { 320 | assertMacro { 321 | """ 322 | @Schema 323 | struct Cmd { 324 | let command: String 325 | let arguments: Arguments 326 | } 327 | """ 328 | } expansion: { 329 | """ 330 | struct Cmd { 331 | let command: String 332 | let arguments: Arguments 333 | } 334 | 335 | extension Cmd: Schemable { 336 | static var schema: JSONSchema { 337 | .object(properties: [ 338 | "command": .string(description: nil), 339 | "arguments": Arguments.schema(description: nil), 340 | ], description: nil) 341 | } 342 | } 343 | """ 344 | } 345 | } 346 | 347 | @Test("Generates schema for enum without associated values") 348 | func enumWithoutAssociatedValues() { 349 | assertMacro { 350 | """ 351 | /// A user role in the system. 352 | @Schema 353 | enum UserRole { 354 | case admin 355 | case user 356 | case guest 357 | } 358 | """ 359 | } expansion: { 360 | """ 361 | /// A user role in the system. 362 | enum UserRole { 363 | case admin 364 | case user 365 | case guest 366 | } 367 | 368 | extension UserRole: Schemable { 369 | static var schema: JSONSchema { 370 | .enum(cases: ["admin", "user", "guest"], description: "A user role in the system.") 371 | } 372 | } 373 | """ 374 | } 375 | } 376 | 377 | @Test("Propagates case comments to enum schema on enums without associated cases") 378 | func enumWithoutAssociatedValuesComments() { 379 | assertMacro { 380 | """ 381 | /// A user role in the system. 382 | @Schema 383 | enum UserRole { 384 | /// Admin user with full permissions. 385 | case admin 386 | case user 387 | /// Guest user with minimal permissions. 388 | case guest 389 | } 390 | """ 391 | } expansion: { 392 | #""" 393 | /// A user role in the system. 394 | enum UserRole { 395 | /// Admin user with full permissions. 396 | case admin 397 | case user 398 | /// Guest user with minimal permissions. 399 | case guest 400 | } 401 | 402 | extension UserRole: Schemable { 403 | static var schema: JSONSchema { 404 | .enum(cases: ["admin", "user", "guest"], description: "A user role in the system.\n\n- admin: Admin user with full permissions.\n- guest: Guest user with minimal permissions.") 405 | } 406 | } 407 | """# 408 | } 409 | } 410 | 411 | @Test("Generates schema for enum with associated values") 412 | func enumWithAssociatedValues() { 413 | assertMacro { 414 | """ 415 | /// A model in the API. 416 | @Schema 417 | enum Model { 418 | case gpt4o 419 | case gpt5 420 | /// Other models with a string identifier. 421 | /// - Parameter name: The identifier for the model. 422 | case other(name: String) 423 | } 424 | """ 425 | } expansion: { 426 | """ 427 | /// A model in the API. 428 | enum Model { 429 | case gpt4o 430 | case gpt5 431 | /// Other models with a string identifier. 432 | /// - Parameter name: The identifier for the model. 433 | case other(name: String) 434 | } 435 | 436 | extension Model: Schemable { 437 | static var schema: JSONSchema { 438 | .anyOf(.enum(cases: ["gpt4o", "gpt5"], description: nil), .object(properties: ["other": .object(properties: ["name": .string(description: "The identifier for the model.")])], description: "Other models with a string identifier."), description: "A model in the API.") 439 | } 440 | } 441 | """ 442 | } 443 | } 444 | 445 | @Test("Errors when struct contains a property with no concrete type") 446 | func structWithoutConcreteTypeError() { 447 | assertMacro { 448 | """ 449 | @Schema 450 | struct User { 451 | let id = UUID() 452 | } 453 | """ 454 | } diagnostics: { 455 | """ 456 | @Schema 457 | struct User { 458 | let id = UUID() 459 | ┬────────────── 460 | ╰─ 🛑 You must provide a type for the property 'id'. 461 | } 462 | """ 463 | } 464 | } 465 | 466 | @Test("Errors when struct contains a Dictionary") 467 | func structWithDictionaryError() { 468 | assertMacro { 469 | """ 470 | @Schema 471 | struct User { 472 | let id: Int 473 | let attributes: [String: String] 474 | } 475 | """ 476 | } diagnostics: { 477 | """ 478 | @Schema 479 | struct User { 480 | let id: Int 481 | let attributes: [String: String] 482 | ┬─────────────────────────────── 483 | ╰─ 🛑 Dictionaries are not supported when using @Schema. Use a custom struct instead. 484 | } 485 | """ 486 | } 487 | } 488 | 489 | @Test("Errors when applied to classes") 490 | func classError() { 491 | assertMacro { 492 | """ 493 | @Schema 494 | class User {} 495 | """ 496 | } diagnostics: { 497 | """ 498 | @Schema 499 | ╰─ 🛑 The @Schema macro can only be applied to structs or enums. 500 | class User {} 501 | """ 502 | } 503 | } 504 | } 505 | -------------------------------------------------------------------------------- /src/Models/Request.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import MetaCodable 3 | 4 | /// A request to the OpenAI Response API. 5 | @Codable @CodingKeys(.snake_case) public struct Request: Equatable, Hashable, Sendable { 6 | /// Additional output data to include in the model response. 7 | public enum Include: String, Equatable, Hashable, Codable, Sendable { 8 | /// Includes the outputs of python code execution in code interpreter tool call items. 9 | case codeInterpreterOutputs = "code_interpreter_call.outputs" 10 | 11 | /// Include image urls from the computer call output. 12 | case computerCallImageURLs = "computer_call_output.output.image_url" 13 | 14 | /// Include the search results of the file search tool call. 15 | case fileSearchResults = "file_search_call.results" 16 | 17 | /// Include image urls from the input message. 18 | case inputImageURLs = "message.input_image.image_url" 19 | 20 | /// Include logprobs with assistant messages. 21 | case outputLogprobs = "message.output_text.logprobs" 22 | 23 | /// Include the sources of the web search tool call. 24 | case webSearchSources = "web_search_call.action.sources" 25 | 26 | /// Includes an encrypted version of reasoning tokens in reasoning item outputs. 27 | /// 28 | /// This enables reasoning items to be used in multi-turn conversations when using the Responses API statelessly (like when the store parameter is set to false, or when an organization is enrolled in the zero data retention program). 29 | case encryptedReasoning = "reasoning.encrypted_content" 30 | } 31 | 32 | @Codable @CodingKeys(.snake_case) public struct StreamOptions: Equatable, Hashable, Sendable { 33 | /// Enables stream obfuscation for the response. 34 | /// 35 | /// Stream obfuscation adds random characters to an obfuscation field on streaming delta events to normalize payload sizes as a mitigation to certain side-channel attacks. 36 | /// 37 | /// These obfuscation fields are included by default, but add a small amount of overhead to the data stream. 38 | /// 39 | /// You can set `includeObfuscation` to false to optimize for bandwidth if you trust the network links between your application and the OpenAI API. 40 | public var includeObfuscation: Bool? 41 | 42 | /// Creates a new `StreamOptions` instance. 43 | /// 44 | /// - Parameter includeObfuscation: Enables stream obfuscation for the response. 45 | public init(includeObfuscation: Bool? = nil) { 46 | self.includeObfuscation = includeObfuscation 47 | } 48 | } 49 | 50 | /// Whether to run the model response in the background. 51 | /// 52 | /// - [Learn more](https://platform.openai.com/docs/guides/background) 53 | public var background: Bool? 54 | 55 | /// The unique ID of the conversation that this response belongs to. 56 | /// 57 | /// Items from this conversation are prepended to `input_items` for this response request. 58 | /// 59 | /// Input items and output items from this response are automatically added to this conversation after this response completes. 60 | public var conversation: String? 61 | 62 | /// Model ID used to generate the response. 63 | /// 64 | /// OpenAI offers a wide range of models with different capabilities, performance characteristics, and price points. Refer to the [model guide](https://platform.openai.com/docs/models) to browse and compare available models. 65 | public var model: Model 66 | 67 | /// Text, image, or file inputs to the model, used to generate a response. 68 | public var input: Input 69 | 70 | /// Specify additional output data to include in the model response. 71 | public var include: [Include]? 72 | 73 | /// Inserts a system (or developer) message as the first item in the model's context. 74 | /// 75 | /// When using along with `previous_response_id`, the instructions from a previous response will be not be carried over to the next response. This makes it simple to swap out system (or developer) messages in new responses. 76 | public var instructions: String? 77 | 78 | /// An upper bound for the number of tokens that can be generated for a response, including visible output tokens and [reasoning tokens](https://platform.openai.com/docs/guides/reasoning). 79 | public var maxOutputTokens: UInt? 80 | 81 | /// The maximum number of total calls to built-in tools that can be processed in a response. 82 | /// 83 | /// This maximum number applies across all built-in tool calls, not per individual tool. 84 | /// 85 | /// Any further attempts to call a tool by the model will be ignored. 86 | public var maxToolCalls: UInt? 87 | 88 | /// Set of 16 key-value pairs that can be attached to an object. This can be useful for storing additional information about the object in a structured format, and querying for objects via API or the dashboard. 89 | /// 90 | /// Keys are strings with a maximum length of 64 characters. Values are strings with a maximum length of 512 characters. 91 | public var metadata: [String: String]? 92 | 93 | /// Whether to allow the model to run tool calls in parallel. 94 | public var parallelToolCalls: Bool? 95 | 96 | /// The unique ID of the previous response to the model. Use this to create multi-turn conversations. 97 | /// 98 | /// Cannot be used in conjunction with `conversation`. 99 | /// 100 | /// Learn more about [conversation state](https://platform.openai.com/docs/guides/conversation-state). 101 | public var previousResponseId: String? 102 | 103 | /// Reference to a prompt template and its variables. [Learn more](https://platform.openai.com/docs/guides/text?api-mode=responses#reusable-prompts). 104 | public var prompt: Prompt? 105 | 106 | /// Used by OpenAI to cache responses for similar requests to optimize your cache hit rates. 107 | /// 108 | /// Replaces the `user` field. [Learn more](https://platform.openai.com/docs/guides/prompt-caching). 109 | public var promptCacheKey: String? 110 | 111 | /// The retention policy for the prompt cache. 112 | /// 113 | /// Set to `oneDay` to enable extended prompt caching, which keeps cached prefixes active for longer, up to a maximum of 24 hours. 114 | public var promptCacheRetention: CacheRetention? 115 | 116 | /// Configuration options for [reasoning models](https://platform.openai.com/docs/guides/reasoning). 117 | public var reasoning: ReasoningConfig? 118 | 119 | /// A stable identifier used to help detect users of your application that may be violating OpenAI's usage policies. 120 | /// 121 | /// The IDs should be a string that uniquely identifies each user. We recommend hashing their username or email address, in order to avoid sending us any identifying information. 122 | /// - [Safety Identifiers](https://platform.openai.com/docs/guides/safety-best-practices#safety-identifiers) 123 | public var safetyIdentifier: String? 124 | 125 | /// Specifies the latency tier to use for processing the request 126 | public var serviceTier: ServiceTier? 127 | 128 | /// Whether to store the generated model response for later retrieval via API. 129 | public var store: Bool? 130 | 131 | /// If set to true, the model response data will be streamed to the client as it is generated using[ server-sent events](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events#event_stream_format). 132 | public var stream: Bool? 133 | 134 | /// Options for streaming responses. 135 | /// 136 | /// Only set this when you set `stream` to true. 137 | public var streamOptions: StreamOptions? 138 | 139 | /// What sampling temperature to use, between 0 and 2. 140 | /// 141 | /// Higher values like 0.8 will make the output more random, while lower values like 0.2 will make it more focused and deterministic. 142 | /// 143 | /// We generally recommend altering this or `top_p` but not both. 144 | public var temperature: Double? 145 | 146 | /// Configuration options for a text response from the model. Can be plain text or structured JSON data. 147 | /// - [Text inputs and outputs](https://platform.openai.com/docs/guides/text) 148 | /// - [Structured Outputs](https://platform.openai.com/docs/guides/structured-outputs) 149 | public var text: TextConfig? 150 | 151 | /// How the model should select which tool (or tools) to use when generating a response. 152 | /// 153 | /// See the `tools` parameter to see how to specify which tools the model can call. 154 | public var toolChoice: Tool.Choice? 155 | 156 | /// An array of tools the model may call while generating a response. You can specify which tool to use by setting the `tool_choice` parameter. 157 | /// 158 | /// The two categories of tools you can provide the model are: 159 | /// - **Built-in tools**: Tools that are provided by OpenAI that extend the model's capabilities, like [web search](https://platform.openai.com/docs/guides/tools-web-search) or [file search](https://platform.openai.com/docs/guides/tools-file-search). Learn more about [built-in tools](https://platform.openai.com/docs/guides/tools). 160 | /// - **Function calls (custom tools)**: Functions that are defined by you, enabling the model to call your own code. Learn more about [function calling](https://platform.openai.com/docs/guides/function-calling). 161 | public var tools: [Tool]? 162 | 163 | /// An integer between 0 and 20 specifying the number of most likely tokens to return at each token position, each with an associated log probability. 164 | public var topLogprobs: UInt? 165 | 166 | /// An alternative to sampling with temperature, called nucleus sampling, where the model considers the results of the tokens with `top_p` probability mass. So 0.1 means only the tokens comprising the top 10% probability mass are considered. 167 | /// 168 | /// We generally recommend altering this or `temperature` but not both. 169 | public var topP: Double? 170 | 171 | /// The truncation strategy to use for the model response. 172 | public var truncation: Truncation? 173 | 174 | /// Constrains the verbosity of the model's response. 175 | /// 176 | /// Lower values will result in more concise responses, while higher values will result in more verbose responses. 177 | public var verbosity: Verbosity? 178 | 179 | /// Creates a new `Request` instance. 180 | /// 181 | /// - Parameter model: Model ID used to generate the response. 182 | /// - Parameter input: Text, image, or file inputs to the model, used to generate a response. 183 | /// - Parameter background: Whether to run the model response in the background. 184 | /// - Parameter conversation: The unique ID of the conversation that this response belongs to. 185 | /// - Parameter include: Specify additional output data to include in the model response. 186 | /// - Parameter instructions: Inserts a system (or developer) message as the first item in the model's context. 187 | /// - Parameter maxOutputTokens: An upper bound for the number of tokens that can be generated for a response, including visible output tokens and reasoning tokens. 188 | /// - Parameter maxToolCalls: The maximum number of total calls to built-in tools that can be processed in a response. 189 | /// - Parameter metadata: Set of 16 key-value pairs that can be attached to an object. 190 | /// - Parameter parallelToolCalls: Whether to allow the model to run tool calls in parallel. 191 | /// - Parameter previousResponseId: The unique ID of the previous response to the model. 192 | /// - Parameter prompt: Reference to a prompt template and its variables. 193 | /// - Parameter promptCacheKey: Used by OpenAI to cache responses for similar requests to optimize your cache hit rates. 194 | /// - Parameter reasoning: Configuration options for reasoning models. 195 | /// - Parameter safetyIdentifier: A stable identifier used to help detect users of your application that may be violating OpenAI's usage policies. 196 | /// - Parameter serviceTier: Specifies the latency tier to use for processing the request. 197 | /// - Parameter store: Whether to store the generated model response for later retrieval via API. 198 | /// - Parameter stream: If set to true, the model response data will be streamed to the client as it is generated. 199 | /// - Parameter streamOptions: Options for streaming responses. 200 | /// - Parameter temperature: What sampling temperature to use, between 0 and 2. 201 | /// - Parameter text: Configuration options for a text response from the model. 202 | /// - Parameter toolChoice: How the model should select which tool (or tools) to use when generating a response. 203 | /// - Parameter tools: An array of tools the model may call while generating a response. 204 | /// - Parameter topLogprobs: An integer between 0 and 20 specifying the number of most likely tokens to return at each token position, each with an associated log probability. 205 | /// - Parameter topP: An alternative to sampling with temperature, called nucleus sampling. 206 | /// - Parameter truncation: The truncation strategy to use for the model response. 207 | /// - Parameter verbosity: Constrains the verbosity of the model's response. 208 | public init( 209 | model: Model, 210 | input: Input, 211 | background: Bool? = nil, 212 | conversation: String? = nil, 213 | include: [Include]? = nil, 214 | instructions: String? = nil, 215 | maxOutputTokens: UInt? = nil, 216 | maxToolCalls: UInt? = nil, 217 | metadata: [String: String]? = nil, 218 | parallelToolCalls: Bool? = nil, 219 | previousResponseId: String? = nil, 220 | prompt: Prompt? = nil, 221 | promptCacheKey: String? = nil, 222 | promptCacheRetention: CacheRetention? = nil, 223 | reasoning: ReasoningConfig? = nil, 224 | safetyIdentifier: String? = nil, 225 | serviceTier: ServiceTier? = nil, 226 | store: Bool? = nil, 227 | stream: Bool? = nil, 228 | streamOptions: StreamOptions? = nil, 229 | temperature: Double? = nil, 230 | text: TextConfig? = nil, 231 | toolChoice: Tool.Choice? = nil, 232 | tools: [Tool]? = nil, 233 | topLogprobs: UInt? = nil, 234 | topP: Double? = nil, 235 | truncation: Truncation? = nil, 236 | verbosity: Verbosity? = nil 237 | ) { 238 | self.text = text 239 | self.topP = topP 240 | self.model = model 241 | self.input = input 242 | self.store = store 243 | self.tools = tools 244 | self.prompt = prompt 245 | self.stream = stream 246 | self.include = include 247 | self.metadata = metadata 248 | self.reasoning = reasoning 249 | self.verbosity = verbosity 250 | self.background = background 251 | self.toolChoice = toolChoice 252 | self.truncation = truncation 253 | self.serviceTier = serviceTier 254 | self.topLogprobs = topLogprobs 255 | self.temperature = temperature 256 | self.instructions = instructions 257 | self.maxToolCalls = maxToolCalls 258 | self.conversation = conversation 259 | self.streamOptions = streamOptions 260 | self.promptCacheKey = promptCacheKey 261 | self.maxOutputTokens = maxOutputTokens 262 | self.safetyIdentifier = safetyIdentifier 263 | self.parallelToolCalls = parallelToolCalls 264 | self.previousResponseId = previousResponseId 265 | self.promptCacheRetention = promptCacheRetention 266 | } 267 | } 268 | -------------------------------------------------------------------------------- /tests/ToolMacroTests.swift: -------------------------------------------------------------------------------- 1 | import Macros 2 | import Testing 3 | import SwiftSyntax 4 | import MacroTesting 5 | 6 | @Suite(.macros([ToolMacro.self], record: .missing)) 7 | struct ToolMacroTests { 8 | @Test("Properly generates Toolable conformance") 9 | func basicExpansion() { 10 | assertMacro { 11 | #""" 12 | @Tool 13 | struct GetWeather { 14 | /// Get the weather for a location. 15 | /// - Parameter location: The location to get the weather for. 16 | func call(location: String) -> String { 17 | "Sunny in \(location)" 18 | } 19 | } 20 | """# 21 | } expansion: { 22 | #""" 23 | struct GetWeather { 24 | /// Get the weather for a location. 25 | /// - Parameter location: The location to get the weather for. 26 | func call(location: String) -> String { 27 | "Sunny in \(location)" 28 | } 29 | } 30 | 31 | extension GetWeather: Toolable { 32 | typealias Error = Never 33 | typealias Output = String 34 | 35 | var name: String { 36 | "GetWeather" 37 | } 38 | 39 | var description: String { 40 | "Get the weather for a location." 41 | } 42 | 43 | struct Arguments: Decodable, Schemable { 44 | /// The location to get the weather for. 45 | let location: String 46 | 47 | static var schema: JSONSchema { 48 | .object(properties: [ 49 | "location": .string(description: "The location to get the weather for."), 50 | ], description: nil) 51 | } 52 | } 53 | 54 | func call(parameters: Arguments) async throws(Error) -> Output { 55 | try await self.call(location: parameters.location) 56 | } 57 | } 58 | """# 59 | } 60 | } 61 | 62 | @Test("Handles multi-parameter doc comments") 63 | func handlesMultiParameterDocs() { 64 | assertMacro { 65 | #""" 66 | @Tool 67 | struct GetWeather { 68 | /// Get the weather for a location. 69 | /// - Parameters: 70 | /// - location: The location to get the weather for. 71 | func call(location: String) -> String { 72 | "Sunny in \(location)" 73 | } 74 | } 75 | """# 76 | } expansion: { 77 | #""" 78 | struct GetWeather { 79 | /// Get the weather for a location. 80 | /// - Parameters: 81 | /// - location: The location to get the weather for. 82 | func call(location: String) -> String { 83 | "Sunny in \(location)" 84 | } 85 | } 86 | 87 | extension GetWeather: Toolable { 88 | typealias Error = Never 89 | typealias Output = String 90 | 91 | var name: String { 92 | "GetWeather" 93 | } 94 | 95 | var description: String { 96 | "Get the weather for a location." 97 | } 98 | 99 | struct Arguments: Decodable, Schemable { 100 | /// The location to get the weather for. 101 | let location: String 102 | 103 | static var schema: JSONSchema { 104 | .object(properties: [ 105 | "location": .string(description: "The location to get the weather for."), 106 | ], description: nil) 107 | } 108 | } 109 | 110 | func call(parameters: Arguments) async throws(Error) -> Output { 111 | try await self.call(location: parameters.location) 112 | } 113 | } 114 | """# 115 | } 116 | } 117 | 118 | @Test("Handles parameters with custom names") 119 | func handlesCustomNamedParameters() { 120 | assertMacro { 121 | #""" 122 | @Tool 123 | struct GetWeather { 124 | /// Get the weather for a location. 125 | /// - Parameter inCity: The location to get the weather for. 126 | func call(location inCity: String) -> String { 127 | "Sunny in \(inCity)" 128 | } 129 | } 130 | """# 131 | } expansion: { 132 | #""" 133 | struct GetWeather { 134 | /// Get the weather for a location. 135 | /// - Parameter inCity: The location to get the weather for. 136 | func call(location inCity: String) -> String { 137 | "Sunny in \(inCity)" 138 | } 139 | } 140 | 141 | extension GetWeather: Toolable { 142 | typealias Error = Never 143 | typealias Output = String 144 | 145 | var name: String { 146 | "GetWeather" 147 | } 148 | 149 | var description: String { 150 | "Get the weather for a location." 151 | } 152 | 153 | struct Arguments: Decodable, Schemable { 154 | /// The location to get the weather for. 155 | let location: String 156 | 157 | static var schema: JSONSchema { 158 | .object(properties: [ 159 | "location": .string(description: "The location to get the weather for."), 160 | ], description: nil) 161 | } 162 | } 163 | 164 | func call(parameters: Arguments) async throws(Error) -> Output { 165 | try await self.call(location: parameters.location) 166 | } 167 | } 168 | """# 169 | } 170 | } 171 | 172 | @Test("Requires parameters to be named") 173 | func errorsWithUnnamedParameters() { 174 | assertMacro { 175 | #""" 176 | @Tool 177 | struct GetWeather { 178 | /// Get the weather for a location. 179 | func call(_ location: String) -> String { 180 | "Sunny in \(location)" 181 | } 182 | } 183 | """# 184 | } diagnostics: { 185 | #""" 186 | @Tool 187 | ╰─ 🛑 All parameters of the `call` function must have a name. The parameter at index 0 does not have a name. 188 | struct GetWeather { 189 | /// Get the weather for a location. 190 | func call(_ location: String) -> String { 191 | "Sunny in \(location)" 192 | } 193 | } 194 | """# 195 | } 196 | } 197 | 198 | @Test("Requires call method") 199 | func requiresCallMethod() { 200 | assertMacro { 201 | #""" 202 | @Tool 203 | struct GetWeather {} 204 | """# 205 | } diagnostics: { 206 | """ 207 | @Tool 208 | ╰─ 🛑 Structs annotated with the @Tool macro must contain a `call` function. 209 | ✏️ Add a `call` function 210 | struct GetWeather {} 211 | """ 212 | } fixes: { 213 | """ 214 | @Tool 215 | struct GetWeather { 216 | /// <#Describe the purpose of your tool to help the model understand when to use it#> 217 | func call() async throws { 218 | // <#The implementation of your tool call, which can optionally return information to the model#> 219 | } 220 | } 221 | """ 222 | } 223 | } 224 | 225 | @Test("Requires a struct") 226 | func requiresStruct() { 227 | assertMacro { 228 | #""" 229 | @Tool 230 | class GetWeather { 231 | /// Get the weather for a location. 232 | func call(location: String) -> String { 233 | "Sunny in \(location)" 234 | } 235 | } 236 | """# 237 | } diagnostics: { 238 | #""" 239 | @Tool 240 | ╰─ 🛑 The @Tool macro can only be applied to structs. 241 | class GetWeather { 242 | /// Get the weather for a location. 243 | func call(location: String) -> String { 244 | "Sunny in \(location)" 245 | } 246 | } 247 | """# 248 | } 249 | } 250 | 251 | @Test("Must only have one call method") 252 | func errorsWithMultipleCallMethods() { 253 | assertMacro { 254 | #""" 255 | @Tool 256 | struct GetWeather { 257 | /// Get the weather for a location. 258 | func call(location: String) -> String { 259 | "Sunny in \(location)" 260 | } 261 | 262 | /// Get the weather for a country. 263 | func call(country: String) -> String { 264 | "Rainy in \(location)" 265 | } 266 | } 267 | """# 268 | } diagnostics: { 269 | #""" 270 | @Tool 271 | ╰─ 🛑 Structs annotated with the @Tool macro may only contain a single `call` function. 272 | struct GetWeather { 273 | /// Get the weather for a location. 274 | func call(location: String) -> String { 275 | "Sunny in \(location)" 276 | } 277 | 278 | /// Get the weather for a country. 279 | func call(country: String) -> String { 280 | "Rainy in \(location)" 281 | } 282 | } 283 | """# 284 | } 285 | } 286 | 287 | @Test("Call method in struct must not be the Toolable implementation") 288 | func callMethodMustNotBeDefault() { 289 | assertMacro { 290 | #""" 291 | @Tool 292 | struct GetWeather { 293 | /// Get the weather for a location. 294 | func call(parameters: Arguments) -> String { 295 | "Sunny in \(parameters.location)" 296 | } 297 | } 298 | """# 299 | } diagnostics: { 300 | #""" 301 | @Tool 302 | ╰─ 🛑 When using the @Tool macro, use function parameters directly instead of manually creating an `Arguments` struct. 303 | struct GetWeather { 304 | /// Get the weather for a location. 305 | func call(parameters: Arguments) -> String { 306 | "Sunny in \(parameters.location)" 307 | } 308 | } 309 | """# 310 | } 311 | } 312 | 313 | @Test("Providing no documentation on your tool shows a warning") 314 | func noDocumentationWarning() { 315 | assertMacro { 316 | #""" 317 | @Tool 318 | struct GetWeather { 319 | func call(location: String) -> String { 320 | "Sunny in \(location)" 321 | } 322 | } 323 | """# 324 | } diagnostics: { 325 | #""" 326 | @Tool 327 | ╰─ ⚠️ Make sure to document the `call` function of your tool to help the model understand its purpose and usage. 328 | struct GetWeather { 329 | func call(location: String) -> String { 330 | "Sunny in \(location)" 331 | } 332 | } 333 | """# 334 | } expansion: { 335 | #""" 336 | struct GetWeather { 337 | func call(location: String) -> String { 338 | "Sunny in \(location)" 339 | } 340 | } 341 | 342 | extension GetWeather: Toolable { 343 | typealias Error = Never 344 | typealias Output = String 345 | 346 | var name: String { 347 | "GetWeather" 348 | } 349 | 350 | struct Arguments: Decodable, Schemable { 351 | let location: String 352 | 353 | static var schema: JSONSchema { 354 | .object(properties: [ 355 | "location": .string(description: nil), 356 | ], description: nil) 357 | } 358 | } 359 | 360 | func call(parameters: Arguments) async throws(Error) -> Output { 361 | try await self.call(location: parameters.location) 362 | } 363 | } 364 | """# 365 | } 366 | 367 | assertMacro { 368 | #""" 369 | @Tool 370 | struct GetWeather { 371 | /// <#Describe the purpose of your tool to help the model understand when to use it#> 372 | func call(location: String) -> String { 373 | "Sunny in \(location)" 374 | } 375 | } 376 | """# 377 | } diagnostics: { 378 | #""" 379 | @Tool 380 | ╰─ ⚠️ Make sure to document the `call` function of your tool to help the model understand its purpose and usage. 381 | struct GetWeather { 382 | /// <#Describe the purpose of your tool to help the model understand when to use it#> 383 | func call(location: String) -> String { 384 | "Sunny in \(location)" 385 | } 386 | } 387 | """# 388 | } expansion: { 389 | #""" 390 | struct GetWeather { 391 | /// <#Describe the purpose of your tool to help the model understand when to use it#> 392 | func call(location: String) -> String { 393 | "Sunny in \(location)" 394 | } 395 | } 396 | 397 | extension GetWeather: Toolable { 398 | typealias Error = Never 399 | typealias Output = String 400 | 401 | var name: String { 402 | "GetWeather" 403 | } 404 | 405 | struct Arguments: Decodable, Schemable { 406 | let location: String 407 | 408 | static var schema: JSONSchema { 409 | .object(properties: [ 410 | "location": .string(description: nil), 411 | ], description: nil) 412 | } 413 | } 414 | 415 | func call(parameters: Arguments) async throws(Error) -> Output { 416 | try await self.call(location: parameters.location) 417 | } 418 | } 419 | """# 420 | } 421 | } 422 | 423 | @Test("Providing no documentation for a parameter shows a warning") 424 | func warnsIfNoDocumentationForParameter() { 425 | assertMacro { 426 | #""" 427 | @Tool 428 | struct GetWeather { 429 | /// Get the weather for a location. 430 | func call(location: String) -> String { 431 | "Sunny in \(location)" 432 | } 433 | } 434 | """# 435 | } diagnostics: { 436 | #""" 437 | @Tool 438 | ╰─ ⚠️ You should document the `location` parameter to help the model understand its usage. 439 | struct GetWeather { 440 | /// Get the weather for a location. 441 | func call(location: String) -> String { 442 | "Sunny in \(location)" 443 | } 444 | } 445 | """# 446 | } expansion: { 447 | #""" 448 | struct GetWeather { 449 | /// Get the weather for a location. 450 | func call(location: String) -> String { 451 | "Sunny in \(location)" 452 | } 453 | } 454 | 455 | extension GetWeather: Toolable { 456 | typealias Error = Never 457 | typealias Output = String 458 | 459 | var name: String { 460 | "GetWeather" 461 | } 462 | 463 | var description: String { 464 | "Get the weather for a location." 465 | } 466 | 467 | struct Arguments: Decodable, Schemable { 468 | let location: String 469 | 470 | static var schema: JSONSchema { 471 | .object(properties: [ 472 | "location": .string(description: nil), 473 | ], description: nil) 474 | } 475 | } 476 | 477 | func call(parameters: Arguments) async throws(Error) -> Output { 478 | try await self.call(location: parameters.location) 479 | } 480 | } 481 | """# 482 | } 483 | } 484 | 485 | @Test("Skips generating name and description if they're already present") 486 | func skipsNameAndDescriptionIfPresent() { 487 | assertMacro { 488 | #""" 489 | @Tool 490 | struct GetWeather { 491 | var name: String { 492 | "Weather Tool" 493 | } 494 | 495 | var description: String { 496 | "Get the weather for a location." 497 | } 498 | 499 | /// A different description here. 500 | /// - Parameter location: The location to get the weather for. 501 | func call(location: String) -> String { 502 | "Sunny in \(location)" 503 | } 504 | } 505 | """# 506 | } expansion: { 507 | #""" 508 | struct GetWeather { 509 | var name: String { 510 | "Weather Tool" 511 | } 512 | 513 | var description: String { 514 | "Get the weather for a location." 515 | } 516 | 517 | /// A different description here. 518 | /// - Parameter location: The location to get the weather for. 519 | func call(location: String) -> String { 520 | "Sunny in \(location)" 521 | } 522 | } 523 | 524 | extension GetWeather: Toolable { 525 | typealias Error = Never 526 | typealias Output = String 527 | 528 | struct Arguments: Decodable, Schemable { 529 | /// The location to get the weather for. 530 | let location: String 531 | 532 | static var schema: JSONSchema { 533 | .object(properties: [ 534 | "location": .string(description: "The location to get the weather for."), 535 | ], description: nil) 536 | } 537 | } 538 | 539 | func call(parameters: Arguments) async throws(Error) -> Output { 540 | try await self.call(location: parameters.location) 541 | } 542 | } 543 | """# 544 | } 545 | } 546 | } 547 | -------------------------------------------------------------------------------- /src/Models/Input.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import MetaCodable 3 | 4 | /// Text, image, or file inputs to the model, used to generate a response. 5 | @Codable @UnTagged public enum Input: Equatable, Hashable, Sendable { 6 | /// A list of items used to generate a model response. 7 | @Codable @CodingKeys(.snake_case) public struct ItemList: Equatable, Hashable, Sendable { 8 | /// A list of items used to generate this response. 9 | public var data: [Item] 10 | 11 | /// The ID of the first item in the list. 12 | public var firstId: String 13 | 14 | /// The ID of the last item in the list. 15 | public var lastId: String 16 | 17 | /// Whether there are more items available. 18 | public var hasMore: Bool 19 | } 20 | 21 | @Codable @CodedAt("type") public enum ListItem: Equatable, Hashable, Sendable { 22 | /// A message input to the model with a role indicating instruction following hierarchy. 23 | /// 24 | /// Instructions given with the `developer` or `system` role take precedence over instructions given with the `user` role. 25 | /// 26 | /// Messages with the `assistant` role are presumed to have been generated by the model in previous interactions. 27 | case message(Message.Input) 28 | 29 | /// An internal identifier for an item to reference. 30 | /// - Parameter id: The ID of the item to reference. 31 | @CodedAs("item_reference") 32 | case itemRef( 33 | id: String 34 | ) 35 | 36 | /// An item representing part of the context for the response to be generated by the model. 37 | /// Can contain text, images, and audio inputs, as well as previous assistant responses and tool call outputs. 38 | case item(Item.Input) 39 | } 40 | 41 | /// Text, image, or audio input to the model, used to generate a response. Can also contain previous assistant responses. 42 | @Codable @UnTagged public enum Content: Equatable, Hashable, Sendable { 43 | /// Text, image, or audio input to the model, used to generate a response. Can also contain previous assistant responses. 44 | @Codable @CodedAt("type") @CodingKeys(.snake_case) public enum ContentItem: Equatable, Hashable, Sendable { 45 | /// The detail level of the image sent to the model. 46 | public enum ImageDetail: String, CaseIterable, Equatable, Hashable, Codable, Sendable { 47 | case auto 48 | case low 49 | case high 50 | case medium 51 | } 52 | 53 | /// The format of the audio data sent to the model. 54 | public enum AudioFormat: String, CaseIterable, Equatable, Hashable, Codable, Sendable { 55 | case mp3 56 | case wav 57 | } 58 | 59 | /// A text input to the model. 60 | @CodedAs("input_text") case text(_ text: String) 61 | 62 | /// An image input to the model. Learn about [image inputs](https://platform.openai.com/docs/guides/vision). 63 | /// - Parameter detail: The detail level of the image to be sent to the model. 64 | /// - Parameter fileId: The ID of the file to be sent to the model. 65 | /// - Parameter imageUrl: The URL of the image to be sent to the model. A fully qualified URL or base64 encoded image in a data URL. 66 | @CodedAs("input_image") case image( 67 | detail: ImageDetail = .auto, 68 | fileId: String? = nil, 69 | imageUrl: String? = nil 70 | ) 71 | 72 | /// A file input to the model. 73 | /// - Parameter fileData: The content of the file to be sent to the model. 74 | /// - Parameter fileId: The ID of the file to be sent to the model. 75 | /// - Parameter fileUrl: The URL of the file to be sent to the model. 76 | /// - Parameter filename: The name of the file to be sent to the model. 77 | @CodedAs("input_file") case file( 78 | fileData: String? = nil, 79 | fileId: String? = nil, 80 | fileUrl: URL? = nil, 81 | filename: String? = nil 82 | ) 83 | 84 | /// An audio input to the model. 85 | /// - Parameter data: Base64-encoded audio data. 86 | /// - Parameter format: The format of the audio data. 87 | @CodedAs("input_audio") case audio(data: String, format: AudioFormat) 88 | } 89 | 90 | /// A text input to the model, equivalent to a text input. 91 | case text(String) 92 | 93 | /// A list of one or many content items to the model, containing different content types. 94 | case list([ContentItem]) 95 | } 96 | 97 | /// The messages contained in the input. 98 | public var messages: [Message] { 99 | switch self { 100 | case let .text(text): return [.input(.init(role: .user, content: .text(text)))] 101 | case let .list(items): return items.compactMap { item in 102 | switch item { 103 | case let .message(message): .input(message) 104 | case let .item(item): item.message 105 | default: nil 106 | } 107 | } 108 | } 109 | } 110 | 111 | /// The text content of the input. 112 | public var inputText: String? { 113 | switch self { 114 | case let .text(text): text 115 | case let .list(items): items.compactMap { item in 116 | switch item { 117 | case let .message(message): return message.content.text 118 | case let .item(item): return item.text 119 | default: return nil 120 | } 121 | }.joined(separator: " ") 122 | } 123 | } 124 | 125 | /// A text input to the model, equivalent to a text input with the user role. 126 | case text(String) 127 | 128 | /// A list of one or many input items to the model, containing different content types. 129 | case list([ListItem]) 130 | } 131 | 132 | public extension Input { 133 | /// Creates a new input to the model with a single message. 134 | /// 135 | /// - Parameter role: The role of the message input. 136 | /// - Parameter text: The text content of the message. 137 | /// - Parameter status: The status of the message. Populated when the message is returned via API. 138 | static func message(role: Message.Role = .user, text: String, status: Message.Status? = nil) -> Self { 139 | .list([.message(Message.Input(role: role, content: .text(text), status: status))]) 140 | } 141 | 142 | /// Creates a new input to the model with a single message. 143 | /// 144 | /// - Parameter role: The role of the message input. 145 | /// - Parameter content: A list of one or many input items to the model, containing different content types. 146 | /// - Parameter status: The status of the message. Populated when the message is returned via API. 147 | static func message(role: Message.Role = .user, content: [Input.Content.ContentItem], status: Message.Status? = nil) -> Self { 148 | .list([.message(Message.Input(role: role, content: .list(content), status: status))]) 149 | } 150 | 151 | /// Creates a new input to the model with a single item. 152 | static func item(_ item: Input.ListItem) -> Self { 153 | .list([item]) 154 | } 155 | } 156 | 157 | public extension Input.Content { 158 | /// Creates a new image input to the model. 159 | /// - Parameter detail: The detail level of the image to be sent to the model. 160 | /// - Parameter fileId: The ID of the file to be sent to the model. 161 | /// - Parameter imageUrl: The URL of the image to be sent to the model. A fully qualified URL or base64 encoded image in a data URL. 162 | static func image(detail: ContentItem.ImageDetail = .auto, fileId: String? = nil, url: String? = nil) -> Self { 163 | .list([.image(detail: detail, fileId: fileId, imageUrl: url)]) 164 | } 165 | 166 | /// Creates a new file input to the model. 167 | /// - Parameter fileData: The content of the file to be sent to the model. 168 | /// - Parameter fileId: The ID of the file to be sent to the model. 169 | /// - Parameter fileUrl: The URL of the file to be sent to the model. 170 | /// - Parameter filename: The name of the file to be sent to the model. 171 | static func file(fileData: String? = nil, fileId: String? = nil, fileUrl: URL? = nil, filename: String? = nil) -> Self { 172 | .list([.file(fileData: fileData, fileId: fileId, fileUrl: fileUrl, filename: filename)]) 173 | } 174 | } 175 | 176 | // MARK: - Text helpers 177 | 178 | public extension Input.Content { 179 | /// The text content of the input. 180 | var text: String? { 181 | switch self { 182 | case let .text(text): text 183 | case let .list(items): items.compactMap { item in 184 | if case let .text(text) = item { return text } 185 | return nil 186 | }.joined(separator: " ") 187 | } 188 | } 189 | } 190 | 191 | // MARK: - Input.ListItem Creation helpers 192 | 193 | public extension Input.ListItem { 194 | /// A message input to the model with a role indicating instruction following hierarchy. 195 | /// 196 | /// Instructions given with the `developer` or `system` role take precedence over instructions given with the `user` role. 197 | /// 198 | /// Messages with the `assistant` role are presumed to have been generated by the model in previous interactions. 199 | /// - Parameter role: The role of the message input. 200 | /// - Parameter content: Text, image, or audio input to the model, used to generate a response. Can also contain previous assistant responses. 201 | static func message(role: Message.Role = .user, content: Input.Content) -> Self { 202 | .message(Message.Input(role: role, content: content)) 203 | } 204 | 205 | /// A message input to the model with a role indicating instruction following hierarchy. 206 | /// 207 | /// Instructions given with the `Developer` or `System` role take precedence over instructions given with the `User` role. 208 | /// - Parameter role: The role of the message input. 209 | /// - Parameter content: Text, image, or audio input to the model, used to generate a response. Can also contain previous assistant responses. 210 | /// - Parameter status: The status of the message. Populated when the message is returned via API. 211 | static func inputMessage(role: Message.Role = .user, content: Input.Content, status: Message.Status? = nil) -> Self { 212 | .item(Item.Input.inputMessage(Message.Input(role: role, content: content, status: status))) 213 | } 214 | 215 | /// An output message from the model. 216 | /// - Parameter content: The content of the output message. 217 | /// - Parameter id: The unique ID of the output message. 218 | /// - Parameter role: The role of the output message. Always `assistant`. 219 | /// - Parameter status: The status of the message output. 220 | static func outputMessage(content: [Item.Output.Content], id: String, role: Message.Role = .assistant, status: Message.Status) -> Self { 221 | .item(Item.Input.outputMessage(Message.Output(content: content, id: id, role: role, status: status))) 222 | } 223 | 224 | /// The results of a file search tool call. 225 | /// 226 | /// See the [file search guide](https://platform.openai.com/docs/guides/tools-file-search) for more information. 227 | /// - Parameter id: The unique ID of the file search tool call. 228 | /// - Parameter queries: The queries used to search for files. 229 | /// - Parameter status: The status of the file search tool call. 230 | /// - Parameter results: The results of the file search tool call. 231 | static func fileSearch(id: String, queries: [String] = [], status: Item.FileSearchCall.Status, results: [Item.FileSearchCall.Result]? = nil) -> Self { 232 | .item(Item.Input.fileSearch(Item.FileSearchCall(id: id, queries: queries, status: status, results: results))) 233 | } 234 | 235 | /// A tool call to a computer use tool. 236 | /// 237 | /// See the [computer use guide](https://platform.openai.com/docs/guides/tools-computer-use) for more information. 238 | /// - Parameter action:The action to execute. 239 | /// - Parameter callId: The unique ID of the computer call. 240 | /// - Parameter pendingSafetyChecks: The pending safety checks for the computer call. 241 | /// - Parameter status: The status of the item. 242 | static func computerToolCall( 243 | action: Item.ComputerToolCall.Action, 244 | callId: String, 245 | pendingSafetyChecks: [Item.ComputerToolCall.SafetyCheck] = [], 246 | status: Item.ComputerToolCall.Status 247 | ) -> Self { 248 | .item(Item.Input.computerToolCall(Item.ComputerToolCall( 249 | action: action, callId: callId, pendingSafetyChecks: pendingSafetyChecks, status: status 250 | ))) 251 | } 252 | 253 | /// The output of a computer tool call. 254 | /// 255 | /// - Parameter id: The ID of the computer tool call output. Populated when this item is returned via API. 256 | /// - Parameter status: The status of the item. Populated when items are returned via API. 257 | /// - Parameter callId: The ID of the computer tool call that produced the output. 258 | /// - Parameter output: A computer screenshot image used with the computer use tool. 259 | /// - Parameter acknowledgedSafetyChecks: The safety checks reported by the API that have been acknowledged by the developer. 260 | static func computerToolCallOutput( 261 | id: String? = nil, 262 | status: Item.ComputerToolCall.Status? = nil, 263 | callId: String, 264 | output: Item.ComputerToolCallOutput.Output, 265 | acknowledgedSafetyChecks: [Item.ComputerToolCall.SafetyCheck]? = nil 266 | ) -> Self { 267 | .item(Item.Input.computerToolCallOutput(Item.ComputerToolCallOutput( 268 | id: id, status: status, callId: callId, output: output, acknowledgedSafetyChecks: acknowledgedSafetyChecks 269 | ))) 270 | } 271 | 272 | /// A tool call to run a web search. 273 | /// 274 | /// See the [web search guide](https://platform.openai.com/docs/guides/tools-web-search) for more information. 275 | /// - Parameter id: The unique ID of the web search tool call. 276 | /// - Parameter status: The status of the web search tool call. 277 | /// - Parameter action: An object describing the specific action taken in this web search call. 278 | static func webSearchCall(id: String, status: Item.WebSearchCall.Status, action: Item.WebSearchCall.Action? = nil) -> Self { 279 | .item(Item.Input.webSearchCall(Item.WebSearchCall(id: id, status: status, action: action))) 280 | } 281 | 282 | /// A tool call to run a function. 283 | /// 284 | /// See the [function calling guide](https://platform.openai.com/docs/guides/function-calling) for more information. 285 | /// - Parameter arguments: A JSON string of the arguments to pass to the function. 286 | /// - Parameter callId: The unique ID of the function tool call generated by the model. 287 | /// - Parameter id: The unique ID of the function tool call. 288 | /// - Parameter name: The name of the function to run. 289 | /// - Parameter status: The status of the item. 290 | static func functionCall(arguments: String, callId: String, id: String, name: String, status: Item.FunctionCall.Status) -> Self { 291 | .item(Item.Input.functionCall(Item.FunctionCall(arguments: arguments, callId: callId, id: id, name: name, status: status))) 292 | } 293 | 294 | /// The output of a function tool call. 295 | /// 296 | /// - Parameter id: The unique ID of the function tool call output. Populated when this item is returned via API. 297 | /// - Parameter status: The status of the item. Populated when items are returned via API. 298 | /// - Parameter callId: The ID of the computer tool call that produced the output. 299 | /// - Parameter output: A JSON string of the output of the function tool call. 300 | static func functionCallOutput(id: String? = nil, status: Item.FunctionCall.Status? = nil, callId: String, output: Input.Content) -> Self { 301 | .item(Item.Input.functionCallOutput(Item.FunctionCallOutput(id: id, status: status, callId: callId, output: output))) 302 | } 303 | 304 | /// A description of the chain of thought used by a reasoning model while generating a response. 305 | /// 306 | /// - Parameter id: The unique identifier of the reasoning content. 307 | /// - Parameter summary: Reasoning text contents. 308 | /// - Parameter status: The status of the item. Populated when items are returned via API. 309 | static func reasoning(id: String, summary: [Item.Reasoning.Summary] = [], status: Item.Reasoning.Status? = nil) -> Self { 310 | .item(Item.Input.reasoning(Item.Reasoning(id: id, summary: summary, status: status))) 311 | } 312 | } 313 | -------------------------------------------------------------------------------- /src/Models/Response.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import MetaCodable 3 | import HelperCoders 4 | 5 | /// A response from the OpenAI Responses API 6 | @Codable @CodingKeys(.snake_case) public struct Response: Identifiable, Equatable, Hashable, Sendable { 7 | /// An error object returned when the model fails to generate a Response. 8 | public struct Error: Equatable, Hashable, Codable { 9 | /// The type of error. 10 | public var type: String 11 | 12 | /// A human-readable description of the error. 13 | public var message: String 14 | 15 | /// The error code for the response. 16 | public var code: String? 17 | 18 | /// The parameter that caused the error. 19 | public var param: String? 20 | 21 | /// Create a new `Error` instance. 22 | /// 23 | /// - Parameter type: The type of error 24 | /// - Parameter message: A human-readable description of the error 25 | /// - Parameter code: The error code for the response 26 | /// - Parameter param: The parameter that caused the error 27 | public init(type: String, message: String, code: String? = nil, param: String? = nil) { 28 | self.type = type 29 | self.code = code 30 | self.param = param 31 | self.message = message 32 | } 33 | } 34 | 35 | /// Details about why a response is incomplete. 36 | public struct IncompleteDetails: Equatable, Codable, Hashable, Sendable { 37 | /// The reason why the response is incomplete. 38 | public var reason: String 39 | 40 | /// Create a new `IncompleteDetails` instance. 41 | /// 42 | /// - Parameter reason: The reason why the response is incomplete 43 | public init(reason: String) { 44 | self.reason = reason 45 | } 46 | } 47 | 48 | /// The status of the response generation. 49 | public enum Status: String, CaseIterable, Equatable, Hashable, Codable, Sendable { 50 | case failed 51 | case queued 52 | case completed 53 | case cancelled 54 | case incomplete 55 | case inProgress = "in_progress" 56 | } 57 | 58 | /// Represents token usage details including input tokens, output tokens, a breakdown of output tokens, and the total tokens used. 59 | @Codable @CodingKeys(.snake_case) public struct Usage: Equatable, Hashable, Sendable { 60 | /// A detailed breakdown of the input tokens. 61 | @Codable @CodingKeys(.snake_case) public struct InputTokensDetails: Equatable, Hashable, Sendable { 62 | /// The number of cached tokens. 63 | public var cachedTokens: UInt 64 | 65 | /// Create a new `InputTokensDetails` instance. 66 | /// 67 | /// - Parameter cachedTokens: The number of cached tokens 68 | public init(cachedTokens: UInt) { 69 | self.cachedTokens = cachedTokens 70 | } 71 | } 72 | 73 | /// A detailed breakdown of the output tokens. 74 | @Codable @CodingKeys(.snake_case) public struct OutputTokensDetails: Equatable, Hashable, Sendable { 75 | /// The number of reasoning tokens. 76 | public var reasoningTokens: UInt 77 | 78 | /// Create a new `OutputTokensDetails` instance. 79 | /// 80 | /// - Parameter reasoningTokens: The number of reasoning tokens 81 | public init(reasoningTokens: UInt) { 82 | self.reasoningTokens = reasoningTokens 83 | } 84 | } 85 | 86 | /// The number of input tokens. 87 | public var inputTokens: UInt 88 | 89 | /// A detailed breakdown of the input tokens. 90 | public var inputTokensDetails: InputTokensDetails 91 | 92 | /// The number of output tokens. 93 | public var outputTokens: UInt 94 | 95 | /// A detailed breakdown of the output tokens. 96 | public var outputTokensDetails: OutputTokensDetails 97 | 98 | /// The total number of tokens used. 99 | public var totalTokens: UInt 100 | 101 | /// Create a new `Usage` instance. 102 | /// 103 | /// - Parameter inputTokens: The number of input tokens 104 | /// - Parameter inputTokensDetails: A detailed breakdown of the input tokens 105 | /// - Parameter outputTokens: The number of output tokens 106 | /// - Parameter outputTokensDetails: A detailed breakdown of the output tokens 107 | /// - Parameter totalTokens: The total number of tokens used 108 | public init(inputTokens: UInt, inputTokensDetails: InputTokensDetails, outputTokens: UInt, outputTokensDetails: OutputTokensDetails, totalTokens: UInt) { 109 | self.inputTokens = inputTokens 110 | self.totalTokens = totalTokens 111 | self.outputTokens = outputTokens 112 | self.inputTokensDetails = inputTokensDetails 113 | self.outputTokensDetails = outputTokensDetails 114 | } 115 | } 116 | 117 | /// Whether to run the model response in the background. [Learn more](https://platform.openai.com/docs/guides/background-responses). 118 | public var background: Bool? 119 | 120 | /// The conversation that this response belongs to. 121 | /// 122 | /// Input items and output items from this response are automatically added to this conversation. 123 | @CodedBy(ConversationIDCoder()) 124 | public var conversation: String? 125 | 126 | /// When this Response was created. 127 | @CodedBy(Since1970DateCoder()) 128 | public var createdAt: Date 129 | 130 | /// Unique identifier for this Response. 131 | public var id: String 132 | 133 | /// Details about why the response is incomplete. 134 | public var incompleteDetails: IncompleteDetails? 135 | 136 | /// Inserts a system (or developer) message as the first item in the model's context. 137 | /// 138 | /// When using along with `previousResponseId`, the instructions from a previous response will be not be carried over to the next response. This makes it simple to swap out system (or developer) messages in new responses. 139 | public var instructions: String? 140 | 141 | /// An upper bound for the number of tokens that can be generated for a response, including visible output tokens and [reasoning tokens](https://platform.openai.com/docs/guides/reasoning). 142 | public var maxOutputTokens: UInt? 143 | 144 | /// The maximum number of total calls to built-in tools that can be processed in a response. 145 | /// 146 | /// This maximum number applies across all built-in tool calls, not per individual tool. 147 | /// 148 | /// Any further attempts to call a tool by the model will be ignored. 149 | public var maxToolCalls: UInt? 150 | 151 | /// Set of 16 key-value pairs that can be attached to an object. 152 | /// 153 | /// This can be useful for storing additional information about the object in a structured format, and querying for objects via API or the dashboard. 154 | /// 155 | /// Keys are strings with a maximum length of 64 characters. Values are strings with a maximum length of 512 characters. 156 | public var metadata: [String: String] 157 | 158 | /// Model ID used to generate the response, like gpt-4o or o1. OpenAI offers a wide range of models with different capabilities, performance characteristics, and price points. 159 | /// 160 | /// Refer to the [model guide](https://platform.openai.com/docs/models) to browse and compare available models. 161 | public var model: String 162 | 163 | /// An array of content items generated by the model. 164 | /// - The length and order of items in the `output` array is dependent on the model's response. 165 | /// - Rather than accessing the first item in the `output` array and assuming it's an assistant message with the content generated by the model, you might consider using the `output_text` function. 166 | public var output: [Item.Output] 167 | 168 | /// Whether to allow the model to run tool calls in parallel. 169 | public var parallelToolCalls: Bool 170 | 171 | /// The unique ID of the previous response to the model. Use this to create multi-turn conversations. 172 | /// - Learn more about [conversation state](https://platform.openai.com/docs/guides/conversation-state). 173 | public var previousResponseId: String? 174 | 175 | /// Reference to a prompt template and its variables. 176 | /// - [Learn more](https://platform.openai.com/docs/guides/prompt-templates) 177 | public var prompt: Prompt? 178 | 179 | /// Used by OpenAI to cache responses for similar requests to optimize your cache hit rates. 180 | /// 181 | /// Replaces the user field. [Learn more](https://platform.openai.com/docs/guides/prompt-caching) 182 | public var promptCacheKey: String? 183 | 184 | /// The retention policy for the prompt cache. 185 | /// 186 | /// Set to `oneDay` to enable extended prompt caching, which keeps cached prefixes active for longer, up to a maximum of 24 hours. 187 | public var promptCacheRetention: CacheRetention? 188 | 189 | /// Configuration options for [reasoning models](https://platform.openai.com/docs/guides/reasoning). 190 | /// Only available for o-series models. 191 | public var reasoning: ReasoningConfig? 192 | 193 | /// A stable identifier used to help detect users of your application that may be violating OpenAI's usage policies. 194 | /// 195 | /// The IDs should be a string that uniquely identifies each user. 196 | /// 197 | /// We recommend hashing their username or email address, in order to avoid sending us any identifying information. 198 | /// - [Safety Identifiers](https://platform.openai.com/docs/guides/safety-best-practices#safety-identifiers) 199 | public var safetyIdentifier: String? 200 | 201 | /// The service tier used for processing the request. 202 | public var serviceTier: ServiceTier? 203 | 204 | /// The status of the response generation. 205 | public var status: Status 206 | 207 | /// What sampling temperature to use, between 0 and 2. 208 | /// 209 | /// Higher values like 0.8 will make the output more random, while lower values like 0.2 will make it more focused and deterministic. 210 | /// 211 | /// We generally recommend altering this or `topP` but not both. 212 | public var temperature: Double 213 | 214 | /// Configuration options for a text response from the model. Can be plain text or structured JSON data. 215 | /// - [Text inputs and outputs](https://platform.openai.com/docs/guides/text) 216 | /// - [Structured Outputs](https://platform.openai.com/docs/guides/structured-outputs) 217 | public var text: TextConfig 218 | 219 | /// How the model should select which tool (or tools) to use when generating a response. 220 | /// 221 | /// See the `tools` parameter to see how to specify which tools the model can call. 222 | public var toolChoice: Tool.Choice 223 | 224 | /// An array of tools the model may call while generating a response. You can specify which tool to use by setting the `toolChoice` parameter. 225 | /// 226 | /// The two categories of tools you can provide the model are: 227 | /// - **Built-in tools**: Tools that are provided by OpenAI that extend the model's capabilities, like [web search](https://platform.openai.com/docs/guides/tools-web-search) or [file search](https://platform.openai.com/docs/guides/tools-file-search). Learn more about [built-in tools](https://platform.openai.com/docs/guides/tools). 228 | /// - **Function calls (custom tools)**: Functions that are defined by you, enabling the model to call your own code. Learn more about [function calling](https://platform.openai.com/docs/guides/function-calling). 229 | public var tools: [Tool] 230 | 231 | /// An integer between 0 and 20 specifying the number of most likely tokens to return at each token position, each with an associated log probability. 232 | public var topLogprobs: UInt? 233 | 234 | /// An alternative to sampling with temperature, called nucleus sampling, where the model considers the results of the tokens with `topP` probability mass. 235 | /// 236 | /// So 0.1 means only the tokens comprising the top 10% probability mass are considered. 237 | /// 238 | /// We generally recommend altering this or `temperature` but not both. 239 | public var topP: Double 240 | 241 | /// The truncation strategy to use for the model response. 242 | public var truncation: Truncation 243 | 244 | /// Represents token usage details including input tokens, output tokens, a breakdown of output tokens, and the total tokens used. 245 | public var usage: Usage? 246 | 247 | /// Whether the response was stored on OpenAI's server for later retrieval. 248 | public var store: Bool 249 | 250 | /// Constrains the verbosity of the model's response. 251 | /// 252 | /// Lower values will result in more concise responses, while higher values will result in more verbose responses. 253 | public var verbosity: Verbosity? 254 | 255 | /// Aggregated text output from all `outputText` items in the output array, if any are present. 256 | public var outputText: String { 257 | output.compactMap { output -> Message.Output? in 258 | guard case let .message(message) = output else { return nil } 259 | return message 260 | } 261 | .flatMap { message in message.content } 262 | .map { content in 263 | switch content { 264 | case let .text(text, _, _): return text 265 | case let .refusal(refusal): return refusal 266 | } 267 | } 268 | .joined() 269 | } 270 | 271 | /// Create a new `Response` instance. 272 | /// 273 | /// - Parameter createdAt: When this Response was created 274 | /// - Parameter id: Unique identifier for this Response 275 | /// - Parameter incompleteDetails: Details about why the response is incomplete 276 | /// - Parameter instructions: System (or developer) message as the in the model's context 277 | /// - Parameter maxOutputTokens: An upper bound for the number of tokens that can be generated for a response, including visible output tokens and reasoning tokens 278 | /// - Parameter metadata: Set of 16 key-value pairs that can be attached to an object 279 | /// - Parameter model: Model ID used to generate the response 280 | /// - Parameter output: An array of content items generated by the model 281 | /// - Parameter parallelToolCalls: Whether the model is allowed to run tool calls in parallel 282 | /// - Parameter previousResponseId: The unique ID of the previous response to the model 283 | /// - Parameter prompt: Reference to a prompt template and its variables 284 | /// - Parameter promptCacheKey: Used by OpenAI to cache responses for similar requests to optimize your cache hit rates 285 | /// - Parameter reasoning: Configuration options for reasoning models 286 | /// - Parameter safetyIdentifier: A stable identifier used to help detect users of your application that may be violating OpenAI's usage policies 287 | /// - Parameter status: The status of the response generation 288 | /// - Parameter temperature: What sampling temperature the model used, from 0 to 2 289 | /// - Parameter text: Configuration options for a text response from the model 290 | /// - Parameter toolChoice: How the model selected which tool (or tools) to use when generating a response 291 | /// - Parameter tools: An array of tools the model may call while generating a response 292 | /// - Parameter topP: An alternative to sampling with temperature, called nucleus sampling 293 | /// - Parameter truncation: The truncation strategy used for the model response 294 | /// - Parameter usage: Token usage details including input tokens, output tokens, a breakdown of output tokens, and the total tokens used by the model 295 | /// - Parameter store: Whether the response was stored on OpenAI's server for later retrieval 296 | /// - Parameter verbosity: Constrains the verbosity of the model's response. 297 | public init( 298 | createdAt: Date, 299 | id: String, 300 | incompleteDetails: IncompleteDetails? = nil, 301 | instructions: String? = nil, 302 | maxOutputTokens: UInt? = nil, 303 | metadata: [String: String] = [:], 304 | model: String, 305 | output: [Item.Output] = [], 306 | parallelToolCalls: Bool, 307 | previousResponseId: String? = nil, 308 | prompt: Prompt? = nil, 309 | promptCacheKey: String? = nil, 310 | promptCacheRetention: CacheRetention? = nil, 311 | reasoning: ReasoningConfig? = nil, 312 | safetyIdentifier: String? = nil, 313 | status: Status, 314 | temperature: Double, 315 | text: TextConfig, 316 | toolChoice: Tool.Choice, 317 | tools: [Tool] = [], 318 | topP: Double, 319 | truncation: Truncation, 320 | usage: Usage? = nil, 321 | store: Bool = true, 322 | verbosity: Verbosity? = nil 323 | ) { 324 | self.id = id 325 | self.topP = topP 326 | self.text = text 327 | self.tools = tools 328 | self.model = model 329 | self.usage = usage 330 | self.store = store 331 | self.status = status 332 | self.prompt = prompt 333 | self.output = output 334 | self.metadata = metadata 335 | self.verbosity = verbosity 336 | self.reasoning = reasoning 337 | self.createdAt = createdAt 338 | self.toolChoice = toolChoice 339 | self.truncation = truncation 340 | self.temperature = temperature 341 | self.instructions = instructions 342 | self.promptCacheKey = promptCacheKey 343 | self.maxOutputTokens = maxOutputTokens 344 | self.safetyIdentifier = safetyIdentifier 345 | self.incompleteDetails = incompleteDetails 346 | self.parallelToolCalls = parallelToolCalls 347 | self.previousResponseId = previousResponseId 348 | self.promptCacheRetention = promptCacheRetention 349 | } 350 | } 351 | 352 | extension Response.Error: Swift.Error, LocalizedError { 353 | public var errorDescription: String? { message } 354 | } 355 | 356 | extension Response { 357 | struct ErrorResponse: Decodable { 358 | var error: Error 359 | } 360 | 361 | enum ResultResponse: Decodable { 362 | case success(Response) 363 | case error(Error) 364 | 365 | enum CodingKeys: String, CodingKey { 366 | case error 367 | } 368 | 369 | init(from decoder: Decoder) throws { 370 | if let container = try? decoder.container(keyedBy: CodingKeys.self), let error = try? container.decode(Error.self, forKey: .error) { 371 | self = .error(error) 372 | return 373 | } 374 | 375 | self = try .success(Response(from: decoder)) 376 | } 377 | 378 | func into() -> Result { 379 | switch self { 380 | case let .success(response): return .success(response) 381 | case let .error(error): return .failure(error) 382 | } 383 | } 384 | } 385 | } 386 | 387 | struct ConversationIDCoder: HelperCoder { 388 | typealias Coded = String 389 | 390 | private enum CodingKeys: String, CodingKey { 391 | case id 392 | } 393 | 394 | func decode(from decoder: any Decoder) throws -> String { 395 | let container = try decoder.container(keyedBy: CodingKeys.self) 396 | 397 | return try container.decode(String.self, forKey: .id) 398 | } 399 | } 400 | -------------------------------------------------------------------------------- /src/Models/Event.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import MetaCodable 3 | 4 | /// A streaming event emitted by the Responses API 5 | @Codable @CodedAt("type") @CodingKeys(.snake_case) public enum Event: Equatable, Sendable { 6 | /// An event that is emitted when a response is created. 7 | /// 8 | /// - Parameter response: The response that was created. 9 | @CodedAs("response.created") 10 | case responseCreated(response: Response) 11 | 12 | /// Emitted when the response is in progress. 13 | /// 14 | /// - Parameter response: The response that is in progress. 15 | @CodedAs("response.in_progress") 16 | case responseInProgress(response: Response) 17 | 18 | /// Emitted when the model response is complete. 19 | /// 20 | /// - Parameter response: The response that is completed. 21 | @CodedAs("response.completed") 22 | case responseCompleted(response: Response) 23 | 24 | /// An event that is emitted when a response fails. 25 | /// 26 | /// - Parameter response: The response that failed. 27 | @CodedAs("response.failed") 28 | case responseFailed(response: Response) 29 | 30 | /// An event that is emitted when a response finishes as incomplete. 31 | /// 32 | /// - Parameter response: The response that is incomplete. 33 | @CodedAs("response.incomplete") 34 | case responseIncomplete(response: Response) 35 | 36 | /// Emitted when a response is queued and waiting to be processed. 37 | /// 38 | /// - Parameter response: The full response object that is queued. 39 | case responseQueued(response: Response) 40 | 41 | /// Emitted when a new output item is added. 42 | /// 43 | /// - Parameter item: The output item that was added. 44 | /// - Parameter outputIndex: The index of the output item that was added. 45 | @CodedAs("response.output_item.added") 46 | case outputItemAdded(item: Item.Output, outputIndex: UInt) 47 | 48 | /// Emitted when an output item is marked done. 49 | /// 50 | /// - Parameter item: The output item that was marked done. 51 | /// - Parameter outputIndex: The index of the output item that was marked done. 52 | @CodedAs("response.output_item.done") 53 | case outputItemDone(item: Item.Output, outputIndex: UInt) 54 | 55 | /// Emitted when a new content part is added. 56 | /// 57 | /// - Parameter contentIndex: The index of the content part that was added. 58 | /// - Parameter itemId: The ID of the output item that the content part was added to. 59 | /// - Parameter outputIndex: The index of the output item that the content part was added to. 60 | /// - Parameter part: The content part that was added. 61 | @CodedAs("response.content_part.added") 62 | case contentPartAdded( 63 | contentIndex: UInt, 64 | itemId: String, 65 | outputIndex: UInt, 66 | part: Item.Output.Content 67 | ) 68 | 69 | /// Emitted when a content part is done. 70 | /// 71 | /// - Parameter contentIndex: The index of the content part that is done. 72 | /// - Parameter itemId: The ID of the output item that the content part was added to. 73 | /// - Parameter outputIndex: The index of the output item that the content part was added to. 74 | /// - Parameter part: The content part that is done. 75 | @CodedAs("response.content_part.done") 76 | case contentPartDone( 77 | contentIndex: UInt, 78 | itemId: String, 79 | outputIndex: UInt, 80 | part: Item.Output.Content 81 | ) 82 | 83 | /// Emitted when there is an additional text delta. 84 | /// 85 | /// - Parameter contentIndex: The index of the content part that the text delta was added to. 86 | /// - Parameter delta: The text delta that was added. 87 | /// - Parameter itemId: The ID of the output item that the text delta was added to. 88 | /// - Parameter outputIndex: The index of the output item that the text delta was added to. 89 | /// - Parameter logprobs: The log probabilities for the text delta that was added. 90 | @CodedAs("response.output_text.delta") 91 | case outputTextDelta( 92 | contentIndex: UInt, 93 | delta: String, 94 | itemId: String, 95 | outputIndex: UInt, 96 | logprobs: [Item.Output.Content.LogProb] 97 | ) 98 | 99 | /// Emitted when a text annotation is added. 100 | /// 101 | /// - Parameter annotation: The annotation that was added. 102 | /// - Parameter annotationIndex: The index of the annotation that was added. 103 | /// - Parameter contentIndex: The index of the content part that the text annotation was added to. 104 | /// - Parameter itemId: The ID of the output item that the text annotation was added to. 105 | /// - Parameter outputIndex: The index of the output item that the text annotation was added to. 106 | @CodedAs("response.output_text.annotation.added") 107 | case outputTextAnnotationAdded( 108 | annotation: Item.Output.Content.Annotation, 109 | annotationIndex: UInt, 110 | contentIndex: UInt, 111 | itemId: String, 112 | outputIndex: UInt 113 | ) 114 | 115 | /// Emitted when text content is finalized. 116 | /// 117 | /// - Parameter contentIndex: The index of the content part that the text content is finalized. 118 | /// - Parameter itemId: The ID of the output item that the text content is finalized. 119 | /// - Parameter outputIndex: The index of the output item that the text content is finalized. 120 | /// - Parameter text: The text content that is finalized. 121 | /// - Parameter logprobs: The log probabilities for the text content that is finalized. 122 | @CodedAs("response.output_text.done") 123 | case outputTextDone( 124 | contentIndex: UInt, 125 | itemId: String, 126 | outputIndex: UInt, 127 | text: String, 128 | logprobs: [Item.Output.Content.LogProb] 129 | ) 130 | 131 | /// Emitted when there is a partial refusal text. 132 | /// 133 | /// - Parameter contentIndex: The index of the content part that the refusal text is added to. 134 | /// - Parameter delta: The refusal text that is added. 135 | /// - Parameter itemId: The ID of the output item that the refusal text is added to. 136 | /// - Parameter outputIndex: The index of the output item that the refusal text is added to. 137 | @CodedAs("response.refusal.delta") 138 | case refusalDelta( 139 | contentIndex: UInt, 140 | delta: String, 141 | itemId: String, 142 | outputIndex: UInt 143 | ) 144 | 145 | /// Emitted when refusal text is finalized. 146 | /// 147 | /// - Parameter contentIndex: The index of the content part that the refusal text is finalized. 148 | /// - Parameter itemId: The ID of the output item that the refusal text is finalized. 149 | /// - Parameter outputIndex: The index of the output item that the refusal text is finalized. 150 | /// - Parameter refusal: The refusal text that is finalized. 151 | @CodedAs("response.refusal.done") 152 | case refusalDone( 153 | contentIndex: UInt, 154 | itemId: String, 155 | outputIndex: UInt, 156 | refusal: String 157 | ) 158 | 159 | /// Emitted when there is a partial function-call arguments delta. 160 | /// 161 | /// - Parameter delta: The function-call arguments delta that is added. 162 | /// - Parameter itemId: The ID of the output item that the function-call arguments delta is added to. 163 | /// - Parameter outputIndex: The index of the output item that the function-call arguments delta is added to. 164 | @CodedAs("response.function_call_arguments.delta") 165 | case functionCallArgumentsDelta( 166 | delta: String, 167 | itemId: String, 168 | outputIndex: UInt 169 | ) 170 | 171 | /// Emitted when function-call arguments are finalized. 172 | /// 173 | /// - Parameter arguments: The function-call arguments. 174 | /// - Parameter itemId: The ID of the item. 175 | /// - Parameter outputIndex: The index of the output item. 176 | @CodedAs("response.function_call_arguments.done") 177 | case functionCallArgumentsDone( 178 | arguments: String, 179 | itemId: String, 180 | outputIndex: UInt 181 | ) 182 | 183 | /// Emitted when a file search call is initiated. 184 | /// 185 | /// - Parameter itemId: The ID of the output item that the file search call is initiated. 186 | /// - Parameter outputIndex: The index of the output item that the file search call is initiated. 187 | @CodedAs("response.file_search_call.in_progress") 188 | case fileSearchCallInitiated( 189 | itemId: String, 190 | outputIndex: UInt 191 | ) 192 | 193 | /// Emitted when a file search is currently searching. 194 | /// 195 | /// - Parameter itemId: The ID of the output item that the file search call is searching. 196 | /// - Parameter outputIndex: The index of the output item that the file search call is searching. 197 | @CodedAs("response.file_search_call.searching") 198 | case fileSearchCallSearching( 199 | itemId: String, 200 | outputIndex: UInt 201 | ) 202 | 203 | /// Emitted when a file search call is completed (results found). 204 | /// 205 | /// - Parameter itemId: The ID of the output item that the file search call completed at. 206 | /// - Parameter outputIndex: The index of the output item that the file search call completed at. 207 | @CodedAs("response.file_search_call.completed") 208 | case fileSearchCallCompleted( 209 | itemId: String, 210 | outputIndex: UInt 211 | ) 212 | 213 | /// Emitted when a web search call is initiated. 214 | /// 215 | /// - Parameter itemId: Unique ID for the output item associated with the web search call. 216 | /// - Parameter outputIndex: The index of the output item that the web search call is associated with. 217 | @CodedAs("response.web_search_call.in_progress") 218 | case webSearchCallInitiated( 219 | itemId: String, 220 | outputIndex: UInt 221 | ) 222 | 223 | /// Emitted when a web search call is executing. 224 | /// 225 | /// - Parameter itemId: Unique ID for the output item associated with the web search call. 226 | /// - Parameter outputIndex: The index of the output item that the web search call is associated with. 227 | @CodedAs("response.web_search_call.searching") 228 | case webSearchCallSearching( 229 | itemId: String, 230 | outputIndex: UInt 231 | ) 232 | 233 | /// Emitted when a web search call is completed. 234 | /// 235 | /// - Parameter itemId: Unique ID for the output item associated with the web search call. 236 | /// - Parameter outputIndex: The index of the output item that the web search call is associated with. 237 | @CodedAs("response.web_search_call.completed") 238 | case webSearchCallCompleted( 239 | itemId: String, 240 | outputIndex: UInt 241 | ) 242 | 243 | /// Emitted when a new reasoning summary part is added. 244 | /// 245 | /// - Parameter itemId: The ID of the item this summary part is associated with. 246 | /// - Parameter outputIndex: The index of the output item this summary part is associated with. 247 | /// - Parameter part: The summary part that was added. 248 | /// - Parameter summaryIndex: The index of the summary part within the reasoning summary. 249 | @CodedAs("response.reasoning_summary_part.added") 250 | case reasoningSummaryPartAdded( 251 | itemId: String, 252 | outputIndex: UInt, 253 | part: Item.Reasoning.Summary, 254 | summaryIndex: UInt 255 | ) 256 | 257 | /// Emitted when a new reasoning summary part is added. 258 | /// 259 | /// - Parameter itemId: The ID of the item this summary part is associated with. 260 | /// - Parameter outputIndex: The index of the output item this summary part is associated with. 261 | /// - Parameter summaryIndex: The index of the summary part within the reasoning summary. 262 | /// - Parameter part: The completed summary part. 263 | @CodedAs("response.reasoning_summary_part.done") 264 | case reasoningSummaryPartDone( 265 | itemId: String, 266 | outputIndex: UInt, 267 | summaryIndex: UInt, 268 | part: Item.Reasoning.Summary 269 | ) 270 | 271 | /// Emitted when there is a delta (partial update) to the reasoning summary content. 272 | /// 273 | /// - Parameter itemId: The unique identifier of the item for which the reasoning summary is being updated. 274 | /// - Parameter outputIndex: The index of the output item in the response's output array. 275 | /// - Parameter summaryIndex: The index of the reasoning summary part within the output item. 276 | /// - Parameter delta: The partial update to the reasoning summary content. 277 | @CodedAs("response.reasoning_summary.delta") 278 | case reasoningSummaryDelta( 279 | itemId: String, 280 | outputIndex: UInt, 281 | summaryIndex: UInt, 282 | delta: Item.Reasoning.SummaryDelta 283 | ) 284 | 285 | /// Emitted when the reasoning summary content is finalized for an item. 286 | /// 287 | /// - Parameter itemId: The unique identifier of the item for which the reasoning summary is finalized. 288 | /// - Parameter outputIndex: The index of the output item in the response's output array. 289 | /// - Parameter summaryIndex: The index of the summary part within the output item. 290 | /// - Parameter text: The finalized reasoning summary text. 291 | @CodedAs("response.reasoning_summary.done") 292 | case reasoningSummaryDone( 293 | itemId: String, 294 | outputIndex: UInt, 295 | summaryIndex: UInt, 296 | text: String 297 | ) 298 | 299 | /// Emitted when a delta is added to a reasoning summary text. 300 | /// 301 | /// - Parameter itemId: The ID of the item this summary text delta is associated with. 302 | /// - Parameter outputIndex: The index of the output item this summary text delta is associated with. 303 | /// - Parameter summaryIndex: The index of the summary part within the reasoning summary. 304 | /// - Parameter delta: The text delta that was added to the summary. 305 | @CodedAs("response.reasoning_summary_text.delta") 306 | case reasoningSummaryTextDelta( 307 | itemId: String, 308 | outputIndex: UInt, 309 | summaryIndex: UInt, 310 | delta: String 311 | ) 312 | 313 | /// Emitted when a delta is added to a reasoning summary text. 314 | /// 315 | /// - Parameter itemId: The ID of the item this summary text delta is associated with. 316 | /// - Parameter outputIndex: The index of the output item this summary text delta is associated with. 317 | /// - Parameter summaryIndex: The index of the summary part within the reasoning summary. 318 | /// - Parameter text: The full text of the completed reasoning summary. 319 | @CodedAs("response.reasoning_summary_text.done") 320 | case reasoningSummaryTextDone( 321 | itemId: String, 322 | outputIndex: UInt, 323 | summaryIndex: UInt, 324 | text: String 325 | ) 326 | 327 | /// Emitted when an image generation tool call is in progress. 328 | /// 329 | /// - Parameter itemId: The unique identifier of the image generation item being processed. 330 | /// - Parameter outputIndex: The index of the output item in the response's output array. 331 | @CodedAs("response.image_generation_call.in_progress") 332 | case imageGenerationCallInProgress( 333 | itemId: String, 334 | outputIndex: UInt 335 | ) 336 | 337 | /// Emitted when an image generation tool call is actively generating an image (intermediate state). 338 | /// 339 | /// - Parameter itemId: The unique identifier of the image generation item being processed. 340 | /// - Parameter outputIndex: The index of the output item in the response's output array. 341 | @CodedAs("response.image_generation_call.generating") 342 | case imageGenerationCallGenerating( 343 | itemId: String, 344 | outputIndex: UInt 345 | ) 346 | 347 | /// Emitted when a partial image is available during image generation streaming. 348 | /// 349 | /// - Parameter itemId: The unique identifier of the image generation item being processed. 350 | /// - Parameter outputIndex: The index of the output item in the response's output array. 351 | /// - Parameter partialImage: Partial image data, suitable for rendering as an image. 352 | /// - Parameter partialImageIndex: Index for the partial image 353 | @CodedAs("response.image_generation_call.partial_image") 354 | case imageGenerationCallPartialImage( 355 | itemId: String, 356 | outputIndex: UInt, 357 | partialImageB64: String, 358 | partialImageIndex: UInt 359 | ) 360 | 361 | /// Emitted when an image generation tool call has completed and the final image is available. 362 | /// 363 | /// - Parameter itemId: The unique identifier of the image generation item being processed. 364 | /// - Parameter outputIndex: The index of the output item in the response's output array. 365 | @CodedAs("response.image_generation_call.completed") 366 | case imageGenerationCallCompleted( 367 | itemId: String, 368 | outputIndex: UInt 369 | ) 370 | 371 | /// Emitted when there is a delta (partial update) to the arguments of an MCP tool call. 372 | /// 373 | /// - Parameter itemId: The unique identifier of the MCP tool call item being processed. 374 | /// - Parameter outputIndex: The index of the output item in the response's output array. 375 | /// - Parameter delta: The partial update to the arguments for the MCP tool call. 376 | @CodedAs("response.mcp_call_arguments.delta") 377 | case mcpCallArgumentsDelta( 378 | itemId: String, 379 | outputIndex: UInt, 380 | delta: String 381 | ) 382 | 383 | /// Emitted when the arguments for an MCP tool call are finalized. 384 | /// 385 | /// - Parameter itemId: The unique identifier of the MCP tool call item being processed. 386 | /// - Parameter outputIndex: The index of the output item in the response's output array. 387 | /// - Parameter arguments: The finalized arguments for the MCP tool call. 388 | @CodedAs("response.mcp_call_arguments.done") 389 | case mcpCallArgumentsDone( 390 | itemId: String, 391 | outputIndex: UInt, 392 | arguments: String 393 | ) 394 | 395 | /// Emitted when an MCP tool call has completed successfully. 396 | /// 397 | /// - Parameter itemId: The unique identifier of the MCP tool call item that completed. 398 | /// - Parameter outputIndex: The index of the output item in the response's output array. 399 | @CodedAs("response.mcp_call.completed") 400 | case mcpCallCompleted( 401 | itemId: String, 402 | outputIndex: UInt 403 | ) 404 | 405 | /// Emitted when an MCP tool call has failed. 406 | /// 407 | /// - Parameter itemId: The unique identifier of the MCP tool call item that failed. 408 | /// - Parameter outputIndex: The index of the output item in the response's output array. 409 | @CodedAs("response.mcp_call.failed") 410 | case mcpCallFailed( 411 | itemId: String, 412 | outputIndex: UInt 413 | ) 414 | 415 | /// Emitted when an MCP tool call is in progress. 416 | /// 417 | /// - Parameter itemId: The unique identifier of the MCP tool call item being processed. 418 | /// - Parameter outputIndex: The index of the output item in the response's output array. 419 | @CodedAs("response.mcp_call.in_progress") 420 | case mcpCallInProgress( 421 | itemId: String, 422 | outputIndex: UInt 423 | ) 424 | 425 | /// Emitted when the list of available MCP tools has been successfully retrieved. 426 | /// 427 | /// - Parameter itemId: The unique identifier of the MCP tool list item that completed. 428 | /// - Parameter outputIndex: The index of the output item in the response for which the MCP tool list is being retrieved. 429 | @CodedAs("response.mcp_list_tools.completed") 430 | case mcpListToolsCompleted( 431 | itemId: String, 432 | outputIndex: UInt 433 | ) 434 | 435 | /// Emitted when the attempt to list available MCP tools has failed. 436 | /// 437 | /// - Parameter itemId: The unique identifier of the MCP tool list item that failed. 438 | /// - Parameter outputIndex: The index of the output item in the response for which the MCP tool list is being retrieved. 439 | @CodedAs("response.mcp_list_tools.failed") 440 | case mcpListToolsFailed( 441 | itemId: String, 442 | outputIndex: UInt 443 | ) 444 | 445 | /// Emitted when the system is in the process of retrieving the list of available MCP tools. 446 | /// 447 | /// - Parameter itemId: The unique identifier of the MCP tool list item in progress. 448 | /// - Parameter outputIndex: The index of the output item in the response for which the MCP tool list is being retrieved. 449 | @CodedAs("response.mcp_list_tools.in_progress") 450 | case mcpListToolsInProgress( 451 | itemId: String, 452 | outputIndex: UInt 453 | ) 454 | 455 | /// Emitted when a code interpreter call is in progress. 456 | /// 457 | /// - Parameter itemId: The unique identifier of the code interpreter tool call item. 458 | /// - Parameter outputIndex: The index of the output item in the response for which the code interpreter call is in progress. 459 | @CodedAs("response.code_interpreter_call.in_progress") 460 | case codeInterpreterCallInProgress( 461 | itemId: String, 462 | outputIndex: UInt 463 | ) 464 | 465 | /// Emitted when the code interpreter is actively interpreting the code snippet. 466 | /// 467 | /// - Parameter itemId: The unique identifier of the code interpreter tool call item. 468 | /// - Parameter outputIndex: The index of the output item in the response for which the code interpreter call is in progress. 469 | @CodedAs("response.code_interpreter_call.interpreting") 470 | case codeInterpreterCallInterpreting( 471 | itemId: String, 472 | outputIndex: UInt 473 | ) 474 | 475 | /// Emitted when the code interpreter call is completed. 476 | /// 477 | /// - Parameter itemId: The unique identifier of the code interpreter tool call item. 478 | /// - Parameter outputIndex: The index of the output item in the response for which the code interpreter call is completed. 479 | @CodedAs("response.code_interpreter_call.completed") 480 | case codeInterpreterCallCompleted( 481 | itemId: String, 482 | outputIndex: UInt 483 | ) 484 | 485 | /// Emitted when a partial code snippet is streamed by the code interpreter. 486 | /// 487 | /// - Parameter itemId: The unique identifier of the code interpreter tool call item. 488 | /// - Parameter outputIndex: The index of the output item in the response for which the code is being streamed. 489 | /// - Parameter delta: The partial code snippet being streamed by the code interpreter. 490 | @CodedAs("response.code_interpreter_call_code.delta") 491 | case codeInterpreterCallCodeDelta( 492 | itemId: String, 493 | outputIndex: UInt, 494 | delta: String 495 | ) 496 | 497 | /// Emitted when the code snippet is finalized by the code interpreter. 498 | /// 499 | /// - Parameter itemId: The unique identifier of the code interpreter tool call item. 500 | /// - Parameter outputIndex: The index of the output item in the response for which the code is finalized. 501 | /// - Parameter code: The final code snippet output by the code interpreter. 502 | @CodedAs("response.code_interpreter_call_code.done") 503 | case codeInterpreterCallCodeDone( 504 | itemId: String, 505 | outputIndex: UInt, 506 | code: String 507 | ) 508 | 509 | /// Event representing a delta (partial update) to the input of a custom tool call. 510 | /// 511 | /// - Parameter itemId: Unique identifier for the API item associated with this event. 512 | /// - Parameter outputIndex: The index of the output this delta applies to. 513 | /// - Parameter delta: The incremental input data (delta) for the custom tool call. 514 | @CodedAs("response.custom_tool_call_input.delta") 515 | case customToolCallInputDelta( 516 | itemId: String, 517 | outputIndex: UInt, 518 | delta: String 519 | ) 520 | 521 | /// Event representing a delta (partial update) to the input of a custom tool call. 522 | /// 523 | /// - Parameter itemId: Unique identifier for the API item associated with this event. 524 | /// - Parameter outputIndex: The index of the output this delta applies to. 525 | /// - Parameter input: The complete input data for the custom tool call. 526 | @CodedAs("response.custom_tool_call_input.done") 527 | case customToolCallInputDone( 528 | itemId: String, 529 | outputIndex: UInt, 530 | input: String 531 | ) 532 | 533 | /// Emitted when an error occurs. 534 | /// 535 | /// - Parameter error: The error that occurred. 536 | @CodedAs("error") 537 | case error(error: Response.Error) 538 | } 539 | --------------------------------------------------------------------------------