├── .vscode ├── settings.json └── launch.json ├── Sources ├── OpenAIBits │ ├── Documentation.docc │ │ ├── Resources │ │ │ ├── blank.swift │ │ │ ├── Text │ │ │ │ ├── FixGrammar │ │ │ │ │ ├── FixGrammar_Output_01.txt │ │ │ │ │ ├── FixGrammar_Output_02.txt │ │ │ │ │ ├── FixGrammar_01.swift │ │ │ │ │ ├── FixGrammar_02.swift │ │ │ │ │ ├── FixGrammar_02_id.swift │ │ │ │ │ ├── FixGrammar_02_input.swift │ │ │ │ │ ├── FixGrammar_02_instruction.swift │ │ │ │ │ ├── FixGrammar_03.swift │ │ │ │ │ ├── FixGrammar_02_id_prev.swift │ │ │ │ │ ├── FixGrammar_02_input_prev.swift │ │ │ │ │ ├── FixGrammar_02_instruction_prev.swift │ │ │ │ │ ├── FixGrammar_04.swift │ │ │ │ │ ├── FixGrammar_05.swift │ │ │ │ │ └── FixGrammar_06.swift │ │ │ │ ├── HumptyDumpty │ │ │ │ │ ├── HumptyDumpty_01.swift │ │ │ │ │ ├── HumptyDumpty_02.swift │ │ │ │ │ ├── HumptyDumpty_04.swift │ │ │ │ │ └── HumptyDumpty_03.swift │ │ │ │ ├── CompletionsSimple │ │ │ │ │ ├── HumptyDumpty_02.swift │ │ │ │ │ ├── HumptyDumpty_04.swift │ │ │ │ │ └── HumptyDumpty_03.swift │ │ │ │ ├── ListRequiredSteps │ │ │ │ │ ├── ListRequiredSteps_02.swift │ │ │ │ │ ├── ListRequiredSteps_01.swift │ │ │ │ │ └── ListRequiredSteps_00.swift │ │ │ │ └── CompletionsComplex │ │ │ │ │ ├── completions_create_complex_call_00.swift │ │ │ │ │ ├── completions_create_complex_call_01.swift │ │ │ │ │ ├── completions_create_complex_call_01_3_n.swift │ │ │ │ │ ├── completions_create_complex_call_01_1_maxTokens.swift │ │ │ │ │ ├── completions_create_complex_call_01_2_temperature.swift │ │ │ │ │ ├── completions_create_complex_call_01_3_n_prev.swift │ │ │ │ │ ├── completions_create_complex_call_01_1_maxTokens_prev.swift │ │ │ │ │ ├── completions_create_complex_call_01_2_temperature_prev.swift │ │ │ │ │ └── completions_create_complex_call_05.swift │ │ │ ├── create_openai.swift │ │ │ ├── openai_api_keys@2x.png │ │ │ ├── openai_api_login@2x.png │ │ │ ├── openai_playground.jpg │ │ │ ├── openai_api_homepage@2x.png │ │ │ ├── openai_api_keys_new@2x.png │ │ │ ├── openai_api_overview@2x.png │ │ │ ├── openai_account_settings@2x.png │ │ │ ├── openai_api_create_account@2x.png │ │ │ ├── openai_api_homepage_login@2x.png │ │ │ ├── openai_api_homepage_signup@2x.png │ │ │ ├── openai_api_keys_generated@2x.png │ │ │ ├── openai_api_overview_menu@2x.png │ │ │ ├── openai_logo_white_on_black@2x.png │ │ │ ├── openai_api_keys_generated_copy@2x.png │ │ │ ├── openai-logo-horizontal-flat-black@2x.png │ │ │ └── openai_api_overview_menu_api_keys@2x.png │ │ ├── Tutorials │ │ │ ├── Tutorial Table of Contents.tutorial │ │ │ ├── Create an Account.tutorial │ │ │ ├── Create an API Key.tutorial │ │ │ ├── Creating Text Completions.tutorial │ │ │ └── Creating Text Edits.tutorial │ │ └── Documentation.md │ ├── CallTypes │ │ ├── PostCall.swift │ │ ├── GetCall.swift │ │ ├── BarePostCall.swift │ │ ├── DeleteCall.swift │ │ ├── HTTPResponse.swift │ │ ├── ExecutableCall.swift │ │ ├── JSONPostCall.swift │ │ ├── Call.swift │ │ ├── CallHandler.swift │ │ ├── HTTPRequestable.swift │ │ ├── JSONResponse.swift │ │ ├── MultipartPostCall.swift │ │ ├── BinaryData.swift │ │ ├── HTTPCall.swift │ │ └── JSONUtils.swift │ ├── Calls │ │ ├── Text.swift │ │ ├── Stop.swift │ │ ├── Tokens.swift │ │ ├── Moderations.swift │ │ ├── Models.swift │ │ ├── Text+Edits.swift │ │ ├── Embeddings.swift │ │ └── Files.swift │ ├── Types │ │ ├── Usage.swift │ │ ├── Identifier.swift │ │ ├── Embedding.swift │ │ ├── FinishReason.swift │ │ ├── Logprobs.swift │ │ ├── ChatModel.swift │ │ ├── ListOf.swift │ │ ├── Penalty.swift │ │ ├── Percentage.swift │ │ ├── File.swift │ │ ├── Token.swift │ │ ├── Edit.swift │ │ ├── Prompt.swift │ │ ├── Generations.swift │ │ ├── Moderation.swift │ │ ├── Model+ids.swift │ │ ├── ChatCompletion.swift │ │ ├── ChatMessage.swift │ │ ├── Completion.swift │ │ ├── Transcription.swift │ │ ├── Model.swift │ │ ├── Language.swift │ │ └── FineTune.swift │ ├── Utils │ │ ├── CodableDictionary.swift │ │ └── Pattern.swift │ └── OpenAI.swift └── OpenAIBitsTestHelpers │ └── ClientHelpers.swift ├── .gitignore ├── OpenAIBits.playground ├── playground.xcworkspace │ └── contents.xcworkspacedata ├── contents.xcplayground └── Contents.swift ├── .swiftpm └── xcode │ ├── package.xcworkspace │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist │ └── xcshareddata │ └── xcschemes │ ├── OpenAIBits.xcscheme │ ├── OpenAIBitsTestHelpers.xcscheme │ ├── swift-openai-gpt3.xcscheme │ ├── openai.xcscheme │ └── swift-openai-bits-Package.xcscheme ├── Tests └── OpenAIBitsTests │ ├── ModelIDTests.swift │ ├── JSONTests.swift │ ├── ErrorResponseTests.swift │ ├── EmbeddingsTests.swift │ ├── ModelTests.swift │ ├── PromptTests.swift │ ├── ImagesTests.swift │ ├── ChatCompletionsTests.swift │ ├── TextCompletionsTests.swift │ ├── TokenEncoderTests.swift │ └── AudioTests.swift ├── LICENSE.md ├── Package.swift └── Package.resolved /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /Sources/OpenAIBits/Documentation.docc/Resources/blank.swift: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /Sources/OpenAIBits/Documentation.docc/Resources/Text/FixGrammar/FixGrammar_Output_01.txt: -------------------------------------------------------------------------------- 1 | Please fix the momma. 2 | -------------------------------------------------------------------------------- /Sources/OpenAIBits/Documentation.docc/Resources/Text/FixGrammar/FixGrammar_Output_02.txt: -------------------------------------------------------------------------------- 1 | Please fix the grammar and spelling. 2 | -------------------------------------------------------------------------------- /Sources/OpenAIBits/Documentation.docc/Resources/create_openai.swift: -------------------------------------------------------------------------------- 1 | import OpenAIBits 2 | 3 | // Note: Don't store your API Key in source code 4 | let openai = OpenAI(apiKey: "sk-") 5 | -------------------------------------------------------------------------------- /Sources/OpenAIBits/Documentation.docc/Resources/openai_api_keys@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/randombitsco/swift-openai-bits/HEAD/Sources/OpenAIBits/Documentation.docc/Resources/openai_api_keys@2x.png -------------------------------------------------------------------------------- /Sources/OpenAIBits/Documentation.docc/Resources/openai_api_login@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/randombitsco/swift-openai-bits/HEAD/Sources/OpenAIBits/Documentation.docc/Resources/openai_api_login@2x.png -------------------------------------------------------------------------------- /Sources/OpenAIBits/Documentation.docc/Resources/openai_playground.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/randombitsco/swift-openai-bits/HEAD/Sources/OpenAIBits/Documentation.docc/Resources/openai_playground.jpg -------------------------------------------------------------------------------- /Sources/OpenAIBits/Documentation.docc/Resources/openai_api_homepage@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/randombitsco/swift-openai-bits/HEAD/Sources/OpenAIBits/Documentation.docc/Resources/openai_api_homepage@2x.png -------------------------------------------------------------------------------- /Sources/OpenAIBits/Documentation.docc/Resources/openai_api_keys_new@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/randombitsco/swift-openai-bits/HEAD/Sources/OpenAIBits/Documentation.docc/Resources/openai_api_keys_new@2x.png -------------------------------------------------------------------------------- /Sources/OpenAIBits/Documentation.docc/Resources/openai_api_overview@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/randombitsco/swift-openai-bits/HEAD/Sources/OpenAIBits/Documentation.docc/Resources/openai_api_overview@2x.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /Packages 4 | /*.xcodeproj 5 | xcuserdata/ 6 | DerivedData/ 7 | .swiftpm/config/registries.json 8 | .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata 9 | .netrc 10 | -------------------------------------------------------------------------------- /Sources/OpenAIBits/Documentation.docc/Resources/openai_account_settings@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/randombitsco/swift-openai-bits/HEAD/Sources/OpenAIBits/Documentation.docc/Resources/openai_account_settings@2x.png -------------------------------------------------------------------------------- /Sources/OpenAIBits/Documentation.docc/Resources/openai_api_create_account@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/randombitsco/swift-openai-bits/HEAD/Sources/OpenAIBits/Documentation.docc/Resources/openai_api_create_account@2x.png -------------------------------------------------------------------------------- /Sources/OpenAIBits/Documentation.docc/Resources/openai_api_homepage_login@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/randombitsco/swift-openai-bits/HEAD/Sources/OpenAIBits/Documentation.docc/Resources/openai_api_homepage_login@2x.png -------------------------------------------------------------------------------- /Sources/OpenAIBits/Documentation.docc/Resources/openai_api_homepage_signup@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/randombitsco/swift-openai-bits/HEAD/Sources/OpenAIBits/Documentation.docc/Resources/openai_api_homepage_signup@2x.png -------------------------------------------------------------------------------- /Sources/OpenAIBits/Documentation.docc/Resources/openai_api_keys_generated@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/randombitsco/swift-openai-bits/HEAD/Sources/OpenAIBits/Documentation.docc/Resources/openai_api_keys_generated@2x.png -------------------------------------------------------------------------------- /Sources/OpenAIBits/Documentation.docc/Resources/openai_api_overview_menu@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/randombitsco/swift-openai-bits/HEAD/Sources/OpenAIBits/Documentation.docc/Resources/openai_api_overview_menu@2x.png -------------------------------------------------------------------------------- /Sources/OpenAIBits/Documentation.docc/Resources/openai_logo_white_on_black@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/randombitsco/swift-openai-bits/HEAD/Sources/OpenAIBits/Documentation.docc/Resources/openai_logo_white_on_black@2x.png -------------------------------------------------------------------------------- /OpenAIBits.playground/playground.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Sources/OpenAIBits/Documentation.docc/Resources/openai_api_keys_generated_copy@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/randombitsco/swift-openai-bits/HEAD/Sources/OpenAIBits/Documentation.docc/Resources/openai_api_keys_generated_copy@2x.png -------------------------------------------------------------------------------- /Sources/OpenAIBits/Documentation.docc/Resources/openai-logo-horizontal-flat-black@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/randombitsco/swift-openai-bits/HEAD/Sources/OpenAIBits/Documentation.docc/Resources/openai-logo-horizontal-flat-black@2x.png -------------------------------------------------------------------------------- /Sources/OpenAIBits/Documentation.docc/Resources/openai_api_overview_menu_api_keys@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/randombitsco/swift-openai-bits/HEAD/Sources/OpenAIBits/Documentation.docc/Resources/openai_api_overview_menu_api_keys@2x.png -------------------------------------------------------------------------------- /Sources/OpenAIBits/CallTypes/PostCall.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /// Represents a `POST` HTTP request. 4 | protocol PostCall: HTTPCall {} 5 | 6 | extension PostCall { 7 | /// Returns `"POST"` 8 | var method: String { "POST" } 9 | } 10 | -------------------------------------------------------------------------------- /OpenAIBits.playground/contents.xcplayground: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /.swiftpm/xcode/package.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Sources/OpenAIBits/Documentation.docc/Resources/Text/HumptyDumpty/HumptyDumpty_01.swift: -------------------------------------------------------------------------------- 1 | import OpenAIBits 2 | 3 | // Note: Don't store your API Key in source code 4 | let openai = OpenAI(apiKey: "sk-") 5 | 6 | let request = Text.Completions( 7 | id: .text_davinci_003, 8 | prompt: "Humpty Dumpty sat" 9 | ) 10 | -------------------------------------------------------------------------------- /Sources/OpenAIBits/CallTypes/GetCall.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /// Represents a `GET` HTTP request 4 | protocol GetCall: HTTPCall {} 5 | 6 | extension GetCall { 7 | /// `"GET"` 8 | var method: String { "GET" } 9 | 10 | /// `nil` 11 | var contentType: String? { nil } 12 | 13 | /// `nil` 14 | func getBody() throws -> Data? { nil } 15 | } 16 | -------------------------------------------------------------------------------- /Sources/OpenAIBits/CallTypes/BarePostCall.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /// A convenience protocol for `POST` HTTP calls which have no body. 4 | protocol BarePostCall: PostCall {} 5 | 6 | extension BarePostCall { 7 | /// Content-Type is `nil` 8 | var contentType: String? { nil } 9 | 10 | /// The body is `nil`. 11 | func getBody() throws -> Data? { nil } 12 | } 13 | -------------------------------------------------------------------------------- /Sources/OpenAIBits/CallTypes/DeleteCall.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /// Represents a `DELETE` HTTP request. 4 | protocol DeleteCall: HTTPCall {} 5 | 6 | extension DeleteCall { 7 | /// `"DELETE"` 8 | var method: String { "DELETE" } 9 | 10 | /// `nil` 11 | var contentType: String? { nil } 12 | 13 | /// `nil` 14 | func getBody() throws -> Data? { nil } 15 | } 16 | -------------------------------------------------------------------------------- /Sources/OpenAIBits/CallTypes/HTTPResponse.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /// Indicates a response is provided via HTTP. 4 | protocol HTTPResponse: Equatable { 5 | 6 | /// Initialises the ``HTTPResponse``. 7 | /// - Parameters: 8 | /// - data: The data for the response. 9 | /// - response: The `Foundation.HTTPURLResponse` value. 10 | init(data: Data, response: HTTPURLResponse) throws 11 | } 12 | -------------------------------------------------------------------------------- /Sources/OpenAIBits/Documentation.docc/Resources/Text/FixGrammar/FixGrammar_01.swift: -------------------------------------------------------------------------------- 1 | import OpenAIBits 2 | 3 | // Note: Don't store your API Key in source code 4 | let openai = OpenAI(apiKey: "sk-") 5 | 6 | /// Fixes the grammar in the provided input. 7 | /// - Parameter input: The input text to fix. 8 | /// - Returns the first 9 | func fixGrammar(in input: String) -> String { 10 | 11 | } 12 | -------------------------------------------------------------------------------- /Sources/OpenAIBits/CallTypes/ExecutableCall.swift: -------------------------------------------------------------------------------- 1 | protocol ExecutableCall: Call { 2 | 3 | /// Attempts to execute the current call with the provided ``OpenAI`` instance. 4 | /// 5 | /// - Parameter client: The ``OpenAI``, containing connection details. 6 | /// - Throws: An `Error` if there is a problem. 7 | /// - Returns: The response. 8 | func execute(with client: OpenAI) async throws -> Response 9 | } 10 | -------------------------------------------------------------------------------- /Tests/OpenAIBitsTests/ModelIDTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import OpenAIBits 3 | 4 | final class ModelIDTests: XCTestCase { 5 | 6 | func testEncodeToJSON() throws { 7 | let id: Model.ID = "alpha" 8 | let json = try jsonEncode(id) 9 | XCTAssertEqual("\"alpha\"", json) 10 | } 11 | 12 | func testDecodeFromJSON() throws { 13 | let id: Model.ID = try jsonDecode("\"alpha\"") 14 | XCTAssertEqual(Model.ID("alpha"), id) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /Sources/OpenAIBits/Documentation.docc/Resources/Text/HumptyDumpty/HumptyDumpty_02.swift: -------------------------------------------------------------------------------- 1 | import OpenAIBits 2 | 3 | // Note: Don't store your API Key in source code 4 | let openai = OpenAI(apiKey: "sk-") 5 | 6 | let request = Text.Completions( 7 | id: .text_davinci_003, 8 | prompt: "Humpty Dumpty sat" 9 | ) 10 | 11 | do { 12 | let result: Completion = try await openai.call(request) 13 | } catch { 14 | print("An error occurred: \(error)") 15 | } 16 | -------------------------------------------------------------------------------- /Sources/OpenAIBits/Documentation.docc/Resources/Text/CompletionsSimple/HumptyDumpty_02.swift: -------------------------------------------------------------------------------- 1 | import OpenAIBits 2 | 3 | // Note: Don't store your API Key in source code 4 | let openai = OpenAI(apiKey: "sk-") 5 | 6 | let request = Text.Completions( 7 | id: .text_davinci_003, 8 | prompt: "Humpty Dumpty sat" 9 | ) 10 | 11 | do { 12 | let result: Completion = try await openai.call(request) 13 | } catch { 14 | print("An error occurred: \(error)") 15 | } 16 | -------------------------------------------------------------------------------- /Sources/OpenAIBits/Documentation.docc/Resources/Text/ListRequiredSteps/ListRequiredSteps_02.swift: -------------------------------------------------------------------------------- 1 | import OpenAIBits 2 | 3 | // Note: Don't store your API Key in source code 4 | let openai = OpenAI(apiKey: "sk-") 5 | 6 | /// List the required steps to perform a given task, such as "change a lightbulb". 7 | /// - Parameter task: The task to describe. 8 | /// - Returns: The list of steps. 9 | func listRequiredSteps(to task: String) -> String { 10 | 11 | } 12 | -------------------------------------------------------------------------------- /Sources/OpenAIBits/CallTypes/JSONPostCall.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /// A ``PostCall`` that encodes itself as JSON when posting. 4 | protocol JSONPostCall: PostCall, Encodable {} 5 | 6 | extension JSONPostCall { 7 | /// `"application/json"` 8 | var contentType: String? { "application/json" } 9 | 10 | /// Returns the body as `Data`, if provided. 11 | /// - Returns: The `Data`. 12 | func getBody() throws -> Data? { 13 | try jsonEncodeData(self) 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /Sources/OpenAIBits/CallTypes/Call.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /// A common protocol for all values which represent an OpenAI client call. 4 | /// These are passed into the ``OpenAI``.``OpenAI/call(_:)`` function to be executed, 5 | /// returning the ``Call/Response`` when successful, or throwing an `Error` if not. 6 | public protocol Call: Equatable { 7 | 8 | /// Every call has a `Response` type, which must be `Equitable`. 9 | associatedtype Response: Equatable 10 | } 11 | -------------------------------------------------------------------------------- /Sources/OpenAIBits/CallTypes/CallHandler.swift: -------------------------------------------------------------------------------- 1 | /// A protocol for allowing a ``OpenAI`` to handle a call. 2 | /// 3 | /// ## See Also 4 | /// 5 | /// - ``OpenAI`` 6 | protocol CallHandler { 7 | 8 | /// Executes the provided call, via the `client`. 9 | /// - Parameters: 10 | /// - call: The ``Call``. 11 | /// - client: The ``OpenAI`` client. 12 | /// - Returns: The response, if successful. 13 | func execute(call: C, with client: OpenAI) async throws -> C.Response 14 | } 15 | -------------------------------------------------------------------------------- /Sources/OpenAIBits/Documentation.docc/Resources/Text/HumptyDumpty/HumptyDumpty_04.swift: -------------------------------------------------------------------------------- 1 | import OpenAIBits 2 | 3 | // Note: Don't store your API Key in source code 4 | let openai = OpenAI(apiKey: "sk-") 5 | 6 | let request = Text.Completions( 7 | id: .text_davinci_003, 8 | prompt: "Humpty Dumpty sat" 9 | ) 10 | 11 | do { 12 | let result: Completion = try await openai.call(request) 13 | print("Result: \(result.text)") 14 | } catch { 15 | print("An error occurred: \(error)") 16 | } 17 | -------------------------------------------------------------------------------- /Sources/OpenAIBits/Documentation.docc/Resources/Text/CompletionsSimple/HumptyDumpty_04.swift: -------------------------------------------------------------------------------- 1 | import OpenAIBits 2 | 3 | // Note: Don't store your API Key in source code 4 | let openai = OpenAI(apiKey: "sk-") 5 | 6 | let request = Text.Completions( 7 | id: .text_davinci_003, 8 | prompt: "Humpty Dumpty sat" 9 | ) 10 | 11 | do { 12 | let result: Completion = try await openai.call(request) 13 | print("Result: \(result.text)") 14 | } catch { 15 | print("An error occurred: \(error)") 16 | } 17 | -------------------------------------------------------------------------------- /Sources/OpenAIBits/CallTypes/HTTPRequestable.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /// A protocol that provides required details to create an `HTTPURLRequest`. 4 | protocol HTTPRequestable { 5 | /// The HTTP method for the call. 6 | var method: String { get } 7 | 8 | /// The relative path for the call. 9 | var path: String { get } 10 | 11 | /// The `Content-Type` for the body. 12 | var contentType: String? { get } 13 | 14 | /// The HTTP body ``Data``. 15 | func getBody() throws -> Data? 16 | } 17 | 18 | -------------------------------------------------------------------------------- /Sources/OpenAIBits/Documentation.docc/Resources/Text/HumptyDumpty/HumptyDumpty_03.swift: -------------------------------------------------------------------------------- 1 | import OpenAIBits 2 | 3 | // Note: Don't store your API Key in source code 4 | let openai = OpenAI(apiKey: "sk-") 5 | 6 | let request = Text.Completions( 7 | id: .text_davinci_003, 8 | prompt: "Humpty Dumpty sat" 9 | ) 10 | 11 | do { 12 | let result: Completion = try await openai.call(request) 13 | print("Result: \(result.choices[0].text)") 14 | } catch { 15 | print("An error occurred: \(error)") 16 | } 17 | -------------------------------------------------------------------------------- /Sources/OpenAIBits/Documentation.docc/Resources/Text/CompletionsSimple/HumptyDumpty_03.swift: -------------------------------------------------------------------------------- 1 | import OpenAIBits 2 | 3 | // Note: Don't store your API Key in source code 4 | let openai = OpenAI(apiKey: "sk-") 5 | 6 | let request = Text.Completions( 7 | id: .text_davinci_003, 8 | prompt: "Humpty Dumpty sat" 9 | ) 10 | 11 | do { 12 | let result: Completion = try await openai.call(request) 13 | print("Result: \(result.choices[0].text)") 14 | } catch { 15 | print("An error occurred: \(error)") 16 | } 17 | -------------------------------------------------------------------------------- /Sources/OpenAIBits/Documentation.docc/Resources/Text/CompletionsComplex/completions_create_complex_call_00.swift: -------------------------------------------------------------------------------- 1 | import OpenAIBits 2 | 3 | // Note: Don't store your API Key in source code 4 | let openai = OpenAI(apiKey: "sk-") 5 | 6 | let request = Text.Completions( 7 | id: .text_davinci_003, 8 | prompt: "Humpty Dumpty sat" 9 | ) 10 | 11 | do { 12 | let result: Completion = try await openai.call(request) 13 | 14 | print("Result: \(result.text)") 15 | } catch { 16 | print("An error occurred: \(error)") 17 | } 18 | -------------------------------------------------------------------------------- /Sources/OpenAIBits/Documentation.docc/Resources/Text/FixGrammar/FixGrammar_02.swift: -------------------------------------------------------------------------------- 1 | import OpenAIBits 2 | 3 | // Note: Don't store your API Key in source code 4 | let openai = OpenAI(apiKey: "sk-") 5 | 6 | /// Fixes the grammar in the provided input. 7 | /// - Parameter input: The input text to fix. 8 | /// - Returns the first 9 | func fixGrammar(in input: String) -> String { 10 | let request = try await openai.call(Text.Edits( 11 | id: .text_davinci_edit_001, 12 | input: input, 13 | instruction: "Fix the grammar and spelling." 14 | )) 15 | } 16 | -------------------------------------------------------------------------------- /Sources/OpenAIBits/Documentation.docc/Resources/Text/FixGrammar/FixGrammar_02_id.swift: -------------------------------------------------------------------------------- 1 | import OpenAIBits 2 | 3 | // Note: Don't store your API Key in source code 4 | let openai = OpenAI(apiKey: "sk-") 5 | 6 | /// Fixes the grammar in the provided input. 7 | /// - Parameter input: The input text to fix. 8 | /// - Returns the first 9 | func fixGrammar(in input: String) -> String { 10 | let request = try await openai.call(Text.Edits( 11 | id: .text_davinci_edit_001, 12 | input: input, 13 | instruction: "Fix the grammar and spelling." 14 | )) 15 | } 16 | -------------------------------------------------------------------------------- /Sources/OpenAIBits/Documentation.docc/Resources/Text/FixGrammar/FixGrammar_02_input.swift: -------------------------------------------------------------------------------- 1 | import OpenAIBits 2 | 3 | // Note: Don't store your API Key in source code 4 | let openai = OpenAI(apiKey: "sk-") 5 | 6 | /// Fixes the grammar in the provided input. 7 | /// - Parameter input: The input text to fix. 8 | /// - Returns the first 9 | func fixGrammar(in input: String) -> String { 10 | let request = try await openai.call(Text.Edits( 11 | id: .text_davinci_edit_001, 12 | input: input, 13 | instruction: "Fix the grammar and spelling." 14 | )) 15 | } 16 | -------------------------------------------------------------------------------- /Sources/OpenAIBits/Documentation.docc/Resources/Text/FixGrammar/FixGrammar_02_instruction.swift: -------------------------------------------------------------------------------- 1 | import OpenAIBits 2 | 3 | // Note: Don't store your API Key in source code 4 | let openai = OpenAI(apiKey: "sk-") 5 | 6 | /// Fixes the grammar in the provided input. 7 | /// - Parameter input: The input text to fix. 8 | /// - Returns the first 9 | func fixGrammar(in input: String) -> String { 10 | let request = try await openai.call(Text.Edits( 11 | id: .text_davinci_edit_001, 12 | input: input, 13 | instruction: "Fix the grammar and spelling." 14 | )) 15 | } 16 | -------------------------------------------------------------------------------- /Sources/OpenAIBits/Documentation.docc/Resources/Text/FixGrammar/FixGrammar_03.swift: -------------------------------------------------------------------------------- 1 | import OpenAIBits 2 | 3 | // Note: Don't store your API Key in source code 4 | let openai = OpenAI(apiKey: "sk-") 5 | 6 | /// Fixes the grammar in the provided input. 7 | /// - Parameter input: The input text to fix. 8 | /// - Returns the first 9 | func fixGrammar(in input: String) async throws -> String { 10 | let request = try await openai.call(Text.Edits( 11 | id: .text_davinci_edit_001, 12 | input: input, 13 | instruction: "Fix the grammar and spelling." 14 | )) 15 | } 16 | -------------------------------------------------------------------------------- /Sources/OpenAIBits/Documentation.docc/Resources/Text/FixGrammar/FixGrammar_02_id_prev.swift: -------------------------------------------------------------------------------- 1 | import OpenAIBits 2 | 3 | // Note: Don't store your API Key in source code 4 | let openai = OpenAI(apiKey: "sk-") 5 | 6 | /// Fixes the grammar in the provided input. 7 | /// - Parameter input: The input text to fix. 8 | /// - Returns the first 9 | func fixGrammar(in input: String) -> String { 10 | let request = try await openai.call(Text.Edits( 11 | id: .text_davinci_edit_001, // highlight 12 | input: input, 13 | instruction: "Fix the grammar and spelling." 14 | )) 15 | } 16 | -------------------------------------------------------------------------------- /Sources/OpenAIBits/Documentation.docc/Resources/Text/CompletionsComplex/completions_create_complex_call_01.swift: -------------------------------------------------------------------------------- 1 | import OpenAIBits 2 | 3 | // Note: Don't store your API Key in source code 4 | let openai = OpenAI(apiKey: "sk-") 5 | 6 | let request = Text.Completions( 7 | id: .text_davinci_003, 8 | prompt: "Humpty Dumpty sat", 9 | maxTokens: 200, 10 | temperature: 0.5, 11 | n: 3 12 | ) 13 | 14 | do { 15 | let result: Completion = try await openai.call(request) 16 | 17 | print("Result: \(result.text)") 18 | } catch { 19 | print("An error occurred: \(error)") 20 | } 21 | -------------------------------------------------------------------------------- /Sources/OpenAIBits/Documentation.docc/Resources/Text/CompletionsComplex/completions_create_complex_call_01_3_n.swift: -------------------------------------------------------------------------------- 1 | import OpenAIBits 2 | 3 | // Note: Don't store your API Key in source code 4 | let openai = OpenAI(apiKey: "sk-") 5 | 6 | let request = Text.Completions( 7 | id: .text_davinci_003, 8 | prompt: "Humpty Dumpty sat", 9 | maxTokens: 200, 10 | temperature: 0.5, 11 | n: 3 12 | ) 13 | 14 | do { 15 | let result: Completion = try await openai.call(request) 16 | 17 | print("Result: \(result.text)") 18 | } catch { 19 | print("An error occurred: \(error)") 20 | } 21 | -------------------------------------------------------------------------------- /Sources/OpenAIBits/Documentation.docc/Resources/Text/FixGrammar/FixGrammar_02_input_prev.swift: -------------------------------------------------------------------------------- 1 | import OpenAIBits 2 | 3 | // Note: Don't store your API Key in source code 4 | let openai = OpenAI(apiKey: "sk-") 5 | 6 | /// Fixes the grammar in the provided input. 7 | /// - Parameter input: The input text to fix. 8 | /// - Returns the first 9 | func fixGrammar(in input: String) -> String { 10 | let request = try await openai.call(Text.Edits( 11 | id: .text_davinci_edit_001, 12 | input: input, // highlight 13 | instruction: "Fix the grammar and spelling." 14 | )) 15 | } 16 | -------------------------------------------------------------------------------- /Sources/OpenAIBits/Documentation.docc/Resources/Text/FixGrammar/FixGrammar_02_instruction_prev.swift: -------------------------------------------------------------------------------- 1 | import OpenAIBits 2 | 3 | // Note: Don't store your API Key in source code 4 | let openai = OpenAI(apiKey: "sk-") 5 | 6 | /// Fixes the grammar in the provided input. 7 | /// - Parameter input: The input text to fix. 8 | /// - Returns the first 9 | func fixGrammar(in input: String) -> String { 10 | let request = try await openai.call(Text.Edits( 11 | id: .text_davinci_edit_001, 12 | input: input, 13 | instruction: "Fix the grammar and spelling." // highlight 14 | )) 15 | } 16 | -------------------------------------------------------------------------------- /Sources/OpenAIBits/Documentation.docc/Resources/Text/CompletionsComplex/completions_create_complex_call_01_1_maxTokens.swift: -------------------------------------------------------------------------------- 1 | import OpenAIBits 2 | 3 | // Note: Don't store your API Key in source code 4 | let openai = OpenAI(apiKey: "sk-") 5 | 6 | let request = Text.Completions( 7 | id: .text_davinci_003, 8 | prompt: "Humpty Dumpty sat", 9 | maxTokens: 200, 10 | temperature: 0.5, 11 | n: 3 12 | ) 13 | 14 | do { 15 | let result: Completion = try await openai.call(request) 16 | 17 | print("Result: \(result.text)") 18 | } catch { 19 | print("An error occurred: \(error)") 20 | } 21 | -------------------------------------------------------------------------------- /Sources/OpenAIBits/Documentation.docc/Resources/Text/CompletionsComplex/completions_create_complex_call_01_2_temperature.swift: -------------------------------------------------------------------------------- 1 | import OpenAIBits 2 | 3 | // Note: Don't store your API Key in source code 4 | let openai = OpenAI(apiKey: "sk-") 5 | 6 | let request = Text.Completions( 7 | id: .text_davinci_003, 8 | prompt: "Humpty Dumpty sat", 9 | maxTokens: 200, 10 | temperature: 0.5, 11 | n: 3 12 | ) 13 | 14 | do { 15 | let result: Completion = try await openai.call(request) 16 | 17 | print("Result: \(result.text)") 18 | } catch { 19 | print("An error occurred: \(error)") 20 | } 21 | -------------------------------------------------------------------------------- /Sources/OpenAIBits/Documentation.docc/Resources/Text/FixGrammar/FixGrammar_04.swift: -------------------------------------------------------------------------------- 1 | import OpenAIBits 2 | 3 | // Note: Don't store your API Key in source code 4 | let openai = OpenAI(apiKey: "sk-") 5 | 6 | /// Fixes the grammar in the provided input. 7 | /// - Parameter input: The input text to fix. 8 | /// - Returns the first 9 | func fixGrammar(in input: String) async throws -> String { 10 | let request = try await openai.call(Text.Edits( 11 | id: .text_davinci_edit_001, 12 | input: input, 13 | instruction: "Fix the grammar and spelling." 14 | )) 15 | return request.text 16 | } 17 | -------------------------------------------------------------------------------- /Sources/OpenAIBits/Documentation.docc/Resources/Text/CompletionsComplex/completions_create_complex_call_01_3_n_prev.swift: -------------------------------------------------------------------------------- 1 | import OpenAIBits 2 | 3 | // Note: Don't store your API Key in source code 4 | let openai = OpenAI(apiKey: "sk-") 5 | 6 | let request = Text.Completions( 7 | id: .text_davinci_003, 8 | prompt: "Humpty Dumpty sat", 9 | maxTokens: 200, 10 | temperature: 0.5, 11 | n: 3 // highlight 12 | ) 13 | 14 | do { 15 | let result: Completion = try await openai.call(request) 16 | 17 | print("Result: \(result.text)") 18 | } catch { 19 | print("An error occurred: \(error)") 20 | } 21 | -------------------------------------------------------------------------------- /Sources/OpenAIBits/Documentation.docc/Resources/Text/ListRequiredSteps/ListRequiredSteps_01.swift: -------------------------------------------------------------------------------- 1 | import OpenAIBits 2 | 3 | // Note: Don't store your API Key in source code 4 | let openai = OpenAI(apiKey: "sk-") 5 | 6 | /// List the required steps to perform a given task, such as "change a lightbulb". 7 | /// - Parameter task: The task to describe. 8 | /// - Returns: The list of steps. 9 | func listRequiredSteps(to task: String) -> String { 10 | let result = try await openai.call(Text.Completions( 11 | model: .text_davinci_003, 12 | prompt: "List the required steps to \(task)" 13 | )) 14 | } 15 | -------------------------------------------------------------------------------- /Sources/OpenAIBits/Documentation.docc/Resources/Text/CompletionsComplex/completions_create_complex_call_01_1_maxTokens_prev.swift: -------------------------------------------------------------------------------- 1 | import OpenAIBits 2 | 3 | // Note: Don't store your API Key in source code 4 | let openai = OpenAI(apiKey: "sk-") 5 | 6 | let request = Text.Completions( 7 | id: .text_davinci_003, 8 | prompt: "Humpty Dumpty sat", 9 | maxTokens: 200, // highlight 10 | temperature: 0.5, 11 | n: 3 12 | ) 13 | 14 | do { 15 | let result: Completion = try await openai.call(request) 16 | 17 | print("Result: \(result.text)") 18 | } catch { 19 | print("An error occurred: \(error)") 20 | } 21 | -------------------------------------------------------------------------------- /Sources/OpenAIBits/Documentation.docc/Resources/Text/CompletionsComplex/completions_create_complex_call_01_2_temperature_prev.swift: -------------------------------------------------------------------------------- 1 | import OpenAIBits 2 | 3 | // Note: Don't store your API Key in source code 4 | let openai = OpenAI(apiKey: "sk-") 5 | 6 | let request = Text.Completions( 7 | id: .text_davinci_003, 8 | prompt: "Humpty Dumpty sat", 9 | maxTokens: 200, 10 | temperature: 0.5, // highlight 11 | n: 3 12 | ) 13 | 14 | do { 15 | let result: Completion = try await openai.call(request) 16 | 17 | print("Result: \(result.text)") 18 | } catch { 19 | print("An error occurred: \(error)") 20 | } 21 | -------------------------------------------------------------------------------- /Sources/OpenAIBits/Calls/Text.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /// Collects text-based ``Call``s. 4 | /// 5 | /// ## Calls 6 | /// 7 | /// - ``Text/Completions`` - Generates completions from a prompt. 8 | /// - ``Text/Edits`` - Given a prompt and an instruction, the model will return an edited version of the prompt. 9 | /// 10 | /// ## See Also 11 | /// 12 | /// - [OpenAI API](https://platform.openai.com/docs/api-reference/completions) 13 | /// - [Completions guide](https://platform.openai.com/docs/guides/completions) 14 | /// - [Editing code guide](https://platform.openai.com/docs/guides/code/editing-code) 15 | /// - ``Chat`` 16 | public enum Text {} 17 | -------------------------------------------------------------------------------- /OpenAIBits.playground/Contents.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | import OpenAIBits 3 | 4 | struct PlaygroundError: Error { 5 | let message: String 6 | 7 | init(_ message: String) { 8 | self.message = message 9 | } 10 | } 11 | 12 | // Store your own OpenAI API key in an environment variable called `"OPENAI_API_KEY"`. 13 | guard let apiKey = ProcessInfo.processInfo.environment["OPENAI_API_KEY"] else { 14 | throw PlaygroundError("Set an environment variable named 'OPENAI_API_KEY'") 15 | } 16 | 17 | let openai = OpenAI(apiKey: apiKey) 18 | 19 | let models = try await openai.call(Models.List()) 20 | 21 | for model in models.data { 22 | print(model) 23 | } 24 | -------------------------------------------------------------------------------- /Sources/OpenAIBits/Documentation.docc/Resources/Text/CompletionsComplex/completions_create_complex_call_05.swift: -------------------------------------------------------------------------------- 1 | import OpenAIBits 2 | 3 | // Note: Don't store your API Key in source code 4 | let openai = OpenAI(apiKey: "sk-") 5 | 6 | let request = Text.Completions( 7 | id: .text_davinci_003, 8 | prompt: "Humpty Dumpty sat", 9 | maxTokens: 200, 10 | temperature: 0.5, 11 | n: 3 12 | ) 13 | 14 | do { 15 | let result: Completion = try await openai.call(request) 16 | 17 | for (i, choice) in result.choices.enumerated() { 18 | print("Result #\(i): \(choice.text)") 19 | } 20 | } catch { 21 | print("An error occurred: \(error)") 22 | } 23 | -------------------------------------------------------------------------------- /Sources/OpenAIBits/CallTypes/JSONResponse.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /// A ``Response`` which will decode itself from `JSON`. 4 | protocol JSONResponse: HTTPResponse, Codable {} 5 | 6 | extension JSONResponse { 7 | /// Constructs a new response based on the data and the `HTTPURLResponse`. 8 | /// - Parameters: 9 | /// - data: The data. 10 | /// - response: The HTTP response. 11 | init(data: Data, response: HTTPURLResponse) throws { 12 | guard isJSON(response: response) else { 13 | let contentType = response.value(forHTTPHeaderField: CONTENT_TYPE) 14 | throw OpenAI.Error.unexpectedResponse("Expected 'Content-Type' of '\(APPLICATION_JSON)' but got '\(contentType ?? "")'") 15 | } 16 | self = try jsonDecodeData(data) 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /Sources/OpenAIBits/Documentation.docc/Resources/Text/FixGrammar/FixGrammar_05.swift: -------------------------------------------------------------------------------- 1 | import OpenAIBits 2 | 3 | // Note: Don't store your API Key in source code 4 | let openai = OpenAI(apiKey: "sk-") 5 | 6 | /// Fixes the grammar in the provided input. 7 | /// - Parameter input: The input text to fix. 8 | /// - Returns the first 9 | func fixGrammar(in input: String) async throws -> String { 10 | let request = try await openai.call(Text.Edits( 11 | id: .text_davinci_edit_001, 12 | input: input, 13 | instruction: "Fix the grammar and spelling." 14 | )) 15 | return request.text 16 | } 17 | 18 | @main 19 | struct MainApp { 20 | static func main() async { 21 | print(try! await fixGrammar(in: "Pleas fx the grammer, and spelling.")) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /Sources/OpenAIBits/Documentation.docc/Resources/Text/FixGrammar/FixGrammar_06.swift: -------------------------------------------------------------------------------- 1 | import OpenAIBits 2 | 3 | // Note: Don't store your API Key in source code 4 | let openai = OpenAI(apiKey: "sk-") 5 | 6 | /// Fixes the grammar in the provided input. 7 | /// - Parameter input: The input text to fix. 8 | /// - Returns the first 9 | func fixGrammar(in input: String) async throws -> String { 10 | let request = try await openai.call(Text.Edits( 11 | id: .text_davinci_edit_001, 12 | input: input, 13 | instruction: "Fix the grammar and spelling.", 14 | temperature: 0.0 15 | )) 16 | return request.text 17 | } 18 | 19 | @main 20 | struct MainApp { 21 | static func main() async { 22 | print(try! await fixGrammar(in: "Pleas fx the grammer, and spelling.")) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "configurations": [ 3 | { 4 | "type": "lldb", 5 | "request": "launch", 6 | "name": "Debug openai", 7 | "program": "${workspaceFolder:swift-openai-gpt3}/.build/debug/openai", 8 | "args": [], 9 | "cwd": "${workspaceFolder:swift-openai-gpt3}", 10 | "preLaunchTask": "swift: Build Debug openai" 11 | }, 12 | { 13 | "type": "lldb", 14 | "request": "launch", 15 | "name": "Release openai", 16 | "program": "${workspaceFolder:swift-openai-gpt3}/.build/release/openai", 17 | "args": [], 18 | "cwd": "${workspaceFolder:swift-openai-gpt3}", 19 | "preLaunchTask": "swift: Build Release openai" 20 | } 21 | ] 22 | } -------------------------------------------------------------------------------- /Sources/OpenAIBits/Types/Usage.swift: -------------------------------------------------------------------------------- 1 | /// Represents ``Token`` usage for a request. 2 | public struct Usage: Equatable, Codable { 3 | /// The number of ``Token``s in the prompt. 4 | public let promptTokens: Int 5 | 6 | /// The number of ``Token``s in the completion choices. 7 | public let completionTokens: Int? 8 | 9 | /// The total number of ``Tokens``s. 10 | public let totalTokens: Int 11 | 12 | /// Creates a ``Usage`` report. 13 | /// 14 | /// - Parameters: 15 | /// - promptTokens: The ``promptTokens``. 16 | /// - completionTokens: The ``completionTokens``. 17 | /// - totalTokens: The ``totalTokens``. 18 | public init(promptTokens: Int, completionTokens: Int?, totalTokens: Int) { 19 | self.promptTokens = promptTokens 20 | self.completionTokens = completionTokens 21 | self.totalTokens = totalTokens 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /Sources/OpenAIBits/Documentation.docc/Resources/Text/ListRequiredSteps/ListRequiredSteps_00.swift: -------------------------------------------------------------------------------- 1 | import OpenAIBits 2 | 3 | // Note: Don't store your API Key in source code 4 | let openai = OpenAI(apiKey: "sk-") 5 | 6 | /// List the required steps to perform a given task, such as "change a lightbulb". 7 | /// - Parameter task: The task to describe. 8 | /// - Parameter choices: The number of choices to return. 9 | /// - Returns: The array of choices for the list. 10 | func listRequiredSteps(to task: String, choices: Int) async throws -> [String] { 11 | let result = try await openai.call(Text.Completions( 12 | model: .text_davinci_003, 13 | prompt: "Concisely list the steps required to \(task), thinking step by step.", 14 | maxTokens: 100, 15 | temperature: 0.2, 16 | n: choices 17 | )) 18 | return result.choices.map(\.text) 19 | } 20 | -------------------------------------------------------------------------------- /Sources/OpenAIBits/Types/Identifier.swift: -------------------------------------------------------------------------------- 1 | /// Describes a value intended to be a single `String` that is `Codable` and can be created via a hard-coded `String` 2 | public protocol Identifier: Hashable, Codable, ExpressibleByStringLiteral, CustomStringConvertible { 3 | var value: String { get } 4 | 5 | init(_ value: String) 6 | } 7 | 8 | extension Identifier { 9 | public init(from decoder: Decoder) throws { 10 | let container = try decoder.singleValueContainer() 11 | try self.init(container.decode(String.self)) 12 | } 13 | 14 | public func encode(to encoder: Encoder) throws { 15 | var container = encoder.singleValueContainer() 16 | try container.encode(value) 17 | } 18 | 19 | public init(stringLiteral value: StaticString) { 20 | self.init(String(describing: value)) 21 | } 22 | 23 | public var description: String { value } 24 | } 25 | -------------------------------------------------------------------------------- /Sources/OpenAIBits/Types/Embedding.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /// Represents an embedding, a vector representation of a given input that can be easily consumed by machine learning models and algorithms. Created via ``Embeddings/Create``. 4 | /// 5 | /// ## Related Calls 6 | /// 7 | ///- ``Embeddings/Create`` 8 | /// 9 | /// ## See Also 10 | /// 11 | /// - [OpenAI API](https://platform.openai.com/docs/api-reference/embeddings) 12 | /// - [Embeddings Guide](https://platform.openai.com/docs/guides/embeddings) 13 | public struct Embedding: Equatable, Codable { 14 | /// The embedding digits. 15 | public let embedding: [Decimal] 16 | 17 | /// The index of the embedding result (`0`-based). 18 | public let index: Int 19 | 20 | /// Creates an embedding. 21 | /// - Parameters: 22 | /// - embedding: The embedding vector values. 23 | /// - index: The index of the result. 24 | public init(embedding: [Decimal], index: Int) { 25 | self.embedding = embedding 26 | self.index = index 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /Sources/OpenAIBits/CallTypes/MultipartPostCall.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import MultipartForm 3 | 4 | /// Represents a `POST` HTTP request that submits as a Multi-Part Form. 5 | protocol MultipartPostCall: PostCall { 6 | /// The MIME boundary for a `multipart/form-data` call. 7 | var boundary: String { get } 8 | 9 | /// The ``MultipartForm`` to post. 10 | func getForm() throws -> MultipartForm 11 | } 12 | 13 | extension MultipartPostCall { 14 | /// The `"Content-Type"` header for the call. 15 | var contentType: String? { 16 | return "multipart/form-data; boundary=\(self.boundary)" 17 | } 18 | 19 | /// Retrieves the body `Data` from the form, if available. 20 | func getBody() throws -> Data? { 21 | try getForm().bodyData 22 | } 23 | } 24 | 25 | // A default boundary that is unlikely to be in the body content. 26 | private let defaultBoundary: String = "----7MA4YWxkTrZu0gWi6F2c2cLFi3H624" 27 | 28 | extension MultipartPostCall { 29 | 30 | var boundary: String { 31 | defaultBoundary 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /Sources/OpenAIBits/Types/FinishReason.swift: -------------------------------------------------------------------------------- 1 | // MARK: Completion.FinishReason 2 | 3 | /// The reason that the result ``Completion/Choice`` finished. 4 | public enum FinishReason: RawRepresentable, Equatable, Codable, CustomStringConvertible { 5 | /// It finished due to hitting the token maximum. 6 | case length 7 | /// The model has no further predictions given the input. 8 | case stop 9 | /// Some other result. 10 | case other(String) 11 | 12 | /// The raw value, as a `String`. 13 | public var rawValue: String { 14 | switch self { 15 | case .length: return "length" 16 | case .stop: return "stop" 17 | case .other(let value): return value 18 | } 19 | } 20 | 21 | /// Initialize given a raw `String` value. 22 | /// - Parameter rawValue: The value. 23 | public init(rawValue: String) { 24 | switch rawValue { 25 | case "length": self = .length 26 | case "stop": self = .stop 27 | default: self = .other(rawValue) 28 | } 29 | } 30 | 31 | /// The `String` value. 32 | public var description: String { rawValue } 33 | } 34 | -------------------------------------------------------------------------------- /Sources/OpenAIBits/Documentation.docc/Tutorials/Tutorial Table of Contents.tutorial: -------------------------------------------------------------------------------- 1 | @Tutorials(name: "OpenAIBits Tutorials") { 2 | @Intro(title: "OpenAIBits Tutorials") { 3 | This series of tutorials walks you through the process of configuring your the OpenAI client and making various calls with it. 4 | } 5 | 6 | @Chapter(name: "OpenAI Account Management") { 7 | Setting up your OpenAI Account. 8 | 9 | @Image(alt: "The OpenAI Account Sign-in Screen", source: "openai_logo_white_on_black.png") 10 | 11 | @TutorialReference(tutorial: "doc:Create-an-Account") 12 | @TutorialReference(tutorial: "doc:Create-an-API-Key") 13 | } 14 | 15 | @Chapter(name: "GPT-3 Calls") { 16 | A guide to making various calls with the `GPT-3` text processing API. 17 | 18 | @Image(alt: "The OpenAI GPT-3 Playground.", source: "openai_playground.jpg") 19 | 20 | @TutorialReference(tutorial: "doc:Creating-Text-Completions") 21 | @TutorialReference(tutorial: "doc:Creating-Text-Edits") 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /Tests/OpenAIBitsTests/JSONTests.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import CustomDump 3 | @testable import OpenAIBits 4 | import XCTest 5 | 6 | 7 | final class JSONTests: XCTestCase { 8 | 9 | func testTokenStringDictionaryToJSON() throws { 10 | @CodableDictionary var dict: [Token: Int]? = [12: 34, 56: 78] 11 | 12 | let encoded = try jsonEncode(_dict, options: [.sortedKeys]) 13 | 14 | XCTAssertNoDifference(encoded, """ 15 | {"12":34,"56":78} 16 | """) 17 | } 18 | 19 | func testTokenStringDictionaryFromJSON() throws { 20 | let json = """ 21 | {"12":34,"56":78} 22 | """ 23 | 24 | @CodableDictionary var decoded: [Token: Int]? 25 | _decoded = try jsonDecode(json) 26 | 27 | XCTAssertNoDifference(decoded, [ 28 | 12: 34, 56: 78 29 | ]) 30 | } 31 | 32 | func testIsJSON() { 33 | XCTAssertTrue(isJSON(contentType: "application/json"), "application/json") 34 | XCTAssertTrue(isJSON(contentType: "application/json"), "application/json; charset=utf-8") 35 | XCTAssertFalse(isJSON(contentType: "foo/bar"), "foo/bar") 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /Sources/OpenAIBits/CallTypes/BinaryData.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | fileprivate let ATTACHMENT_FILENAME: Pattern = #"attachment; filename="(.*)""# 4 | 5 | /// Represents an HTTP response that returns binary data, typically a file. 6 | public struct BinaryData { 7 | /// The filename, if available. 8 | public let filename: String? 9 | 10 | /// The binary data. 11 | public let data: Data 12 | } 13 | 14 | extension BinaryData: HTTPResponse { 15 | 16 | /// Initialises the response with the returned `Foundation.Data` and `Foundation.HTTPURLResponse`. 17 | /// 18 | /// - Parameter data: The `Data` from the response. 19 | /// - Parameter response: The `HTTPURLResponse`. 20 | public init(data: Data, response: HTTPURLResponse) { 21 | self.data = data 22 | 23 | if let contentDisposition = response.value(forHTTPHeaderField: "Content-Disposition") { 24 | let result = ATTACHMENT_FILENAME.matchGroups(in: contentDisposition) 25 | let filename = result?[1] 26 | self.filename = filename != nil ? String(filename!) : nil 27 | } else { 28 | self.filename = nil 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /Sources/OpenAIBits/Types/Logprobs.swift: -------------------------------------------------------------------------------- 1 | /// The `logprobs` for a ``Completion/Choice``. Each array is the same length, and refers to the token in the 2 | public struct Logprobs: Codable, Equatable { 3 | /// The list of tokens in the text of the ``Completion/Choice``. 4 | public let tokens: [String] 5 | 6 | /// The logprobs for the matching token. 7 | public let tokenLogprobs: [Double] 8 | 9 | /// The top logprobs for other tokens. 10 | public let topLogprobs: [[String: Double]] 11 | 12 | /// The offset of the token, relative to the prompt text. 13 | public let textOffset: [Int] 14 | 15 | /// Creates a new ``Logprobs``. 16 | /// 17 | /// - Parameters: 18 | /// - tokens: The ``tokens``. 19 | /// - tokenLogprobs: The ``tokenLogprobs``. 20 | /// - topLogprobs: The ``topLogprobs``. 21 | /// - textOffset: The ``textOffset``. 22 | public init(tokens: [String], tokenLogprobs: [Double], topLogprobs: [[String : Double]], textOffset: [Int]) { 23 | self.tokens = tokens 24 | self.tokenLogprobs = tokenLogprobs 25 | self.topLogprobs = topLogprobs 26 | self.textOffset = textOffset 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /Sources/OpenAIBits/Types/ChatModel.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /// The ID of the GPT 3.5 chat model. 4 | public enum ChatModel: RawRepresentable, Equatable, Codable { 5 | /// The latest turbo model. Will be updated over time to improve performance. 6 | case gpt_3_5_turbo 7 | 8 | /// A snapshot of the default model at a specific point in time. For example, `gpt-3.5-turbo-0301`. was the initial snapshot, which will be supported up to at least 1st of June, 2023. 9 | case gpt_3_5_snapshot(String) 10 | 11 | public var rawValue: String { 12 | switch self { 13 | case .gpt_3_5_turbo: return "gpt-3.5-turbo" 14 | case .gpt_3_5_snapshot(let snapshot): return "gpt-3.5-turbo-\(snapshot)" 15 | } 16 | } 17 | 18 | public init?(rawValue: String) { 19 | switch rawValue { 20 | case "gpt-3.5-turbo": 21 | self = .gpt_3_5_turbo 22 | case let rawValue where rawValue.hasPrefix("gpt-3.5-turbo-"): 23 | self = .gpt_3_5_snapshot(String(rawValue.dropFirst("gpt-3.5-turbo-".count))) 24 | default: 25 | return nil 26 | } 27 | } 28 | } 29 | 30 | extension ChatModel: CustomStringConvertible { 31 | public var description: String { 32 | rawValue 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /Tests/OpenAIBitsTests/ErrorResponseTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import OpenAIBits 3 | 4 | final class ErrorResponseTests: XCTestCase { 5 | 6 | func testEncodeToJSON() throws { 7 | let error = OpenAI.Error( 8 | type: "invalid_request_error", 9 | code: "model_not_found", 10 | param: "model", 11 | message: "The model 'gpt-4-turbo' does not exist" 12 | ) 13 | let json = try jsonEncode(error, options: [.sortedKeys]) 14 | XCTAssertEqual(""" 15 | {"code":"model_not_found","message":"The model 'gpt-4-turbo' does not exist","param":"model","type":"invalid_request_error"} 16 | """, json) 17 | } 18 | 19 | func testDecodeFromJSON() throws { 20 | let error: OpenAI.Error = try jsonDecode(""" 21 | { 22 | "message": "The model 'gpt-4-turbo' does not exist", 23 | "type": "invalid_request_error", 24 | "param": "model", 25 | "code": "model_not_found" 26 | } 27 | """) 28 | XCTAssertEqual( 29 | OpenAI.Error( 30 | type: "invalid_request_error", 31 | code: "model_not_found", 32 | param: "model", 33 | message: "The model 'gpt-4-turbo' does not exist" 34 | ), 35 | error 36 | ) 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /Sources/OpenAIBits/Types/ListOf.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /// Represents a list of `Element` values. 4 | public struct ListOf: JSONResponse where Element: Codable, Element: Equatable { 5 | public let data: [Element] 6 | public let usage: Usage? 7 | 8 | public init(data: [Element], usage: Usage? = nil) { 9 | self.data = data 10 | self.usage = usage 11 | } 12 | } 13 | 14 | // MARK: Equatable 15 | 16 | extension ListOf: Equatable where Element: Equatable {} 17 | 18 | // MARK: Sequence 19 | 20 | extension ListOf: Sequence { 21 | public typealias Iterator = AnyIterator 22 | 23 | public func makeIterator() -> Iterator { 24 | var iterator = data.makeIterator() 25 | 26 | return AnyIterator { 27 | iterator.next() 28 | } 29 | } 30 | } 31 | 32 | // MARK: Collection 33 | 34 | extension ListOf: Collection { 35 | public typealias Index = Int 36 | 37 | public var startIndex: Int { 38 | data.startIndex 39 | } 40 | 41 | public var endIndex: Int { 42 | data.endIndex 43 | } 44 | 45 | public subscript(position: Int) -> Element { 46 | data[position] 47 | } 48 | 49 | public func index(after i: Int) -> Int { 50 | data.index(after: i) 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /Tests/OpenAIBitsTests/EmbeddingsTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | import CustomDump 3 | @testable import OpenAIBits 4 | 5 | final class EmbeddingsTests: XCTestCase { 6 | 7 | func testEmbeddingsStringRequestToJSON() throws { 8 | let value = Embeddings.Create( 9 | model: .text_davinci_003, 10 | input: "Input string." 11 | ) 12 | XCTAssertNoDifference( 13 | #"{"input":"Input string.","model":"text-davinci-003"}"#, 14 | try jsonEncode(value, options: [.sortedKeys]) 15 | ) 16 | } 17 | 18 | func testEmbeddingsTokenArrayRequestToJSON() throws { 19 | let value = Embeddings.Create( 20 | model: .text_davinci_003, 21 | input: [12,34,567] 22 | ) 23 | XCTAssertNoDifference( 24 | #"{"input":[12,34,567],"model":"text-davinci-003"}"#, 25 | try jsonEncode(value, options: [.sortedKeys]) 26 | ) 27 | } 28 | 29 | func testEmbeddingsWithUserRequestToJSON() throws { 30 | let value = Embeddings.Create( 31 | model: .text_davinci_003, 32 | input: "Input string.", 33 | user: "foo" 34 | ) 35 | XCTAssertNoDifference( 36 | #"{"input":"Input string.","model":"text-davinci-003","user":"foo"}"#, 37 | try jsonEncode(value, options: [.sortedKeys]) 38 | ) 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /Sources/OpenAIBits/Types/Penalty.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /// Represents a value that can be between `-2.0` and `2.0`. 4 | public struct Penalty: Equatable { 5 | /// The actual value. 6 | public let value: Decimal 7 | 8 | /// Creates the penalty value, clamped between `-2.0` and `2.0`. 9 | /// - Parameter value: The value. 10 | public init(_ value: Decimal) { 11 | self.value = Self.clamp(value) 12 | } 13 | } 14 | 15 | extension Penalty { 16 | /// Clamps the value between `-2.0` and `2.0`. 17 | /// 18 | /// - Parameter value: The value to clamp. 19 | /// - Returns: The clamped value. 20 | public static func clamp(_ value: Decimal) -> Decimal { 21 | return min(2.0, max(-2.0, value)) 22 | } 23 | } 24 | 25 | extension Penalty: ExpressibleByFloatLiteral { 26 | /// Allows creation of a penalty directly with a `Double`. 27 | public init(floatLiteral value: Double) { 28 | self.init(Decimal(value)) 29 | } 30 | } 31 | 32 | extension Penalty: Codable { 33 | public init(from decoder: Decoder) throws { 34 | let container = try decoder.singleValueContainer() 35 | try self.init(container.decode(Decimal.self)) 36 | } 37 | 38 | public func encode(to encoder: Encoder) throws { 39 | var container = encoder.singleValueContainer() 40 | try container.encode(self.value) 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /Sources/OpenAIBits/Types/Percentage.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /// Represents a value that can be between `0.0` and `1.0`. 4 | public struct Percentage: Equatable { 5 | public let value: Decimal 6 | 7 | /// Creates the percentage, clamping between `0.0` and `1.0`. 8 | /// - Parameter value: The value. 9 | public init(_ value: Decimal) { 10 | self.value = Self.clamp(value) 11 | } 12 | } 13 | 14 | extension Percentage { 15 | /// Clamps the value between `0.0` and `1.0`. 16 | /// 17 | /// - Parameter value: The value to clamp. 18 | /// - Returns the clamped value. 19 | public static func clamp(_ value: Decimal) -> Decimal { 20 | return min(1.0, max(0.0, value)) 21 | } 22 | } 23 | 24 | extension Percentage: ExpressibleByFloatLiteral { 25 | /// Allows the ``Percentage`` to be created via direct assignment from a `Double`. 26 | /// - Parameter value: The double value. 27 | public init(floatLiteral value: Double) { 28 | self.init(Decimal(value)) 29 | } 30 | } 31 | 32 | extension Percentage: Codable { 33 | public init(from decoder: Decoder) throws { 34 | let container = try decoder.singleValueContainer() 35 | try self.init(container.decode(Decimal.self)) 36 | } 37 | 38 | public func encode(to encoder: Encoder) throws { 39 | var container = encoder.singleValueContainer() 40 | try container.encode(self.value) 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /Sources/OpenAIBits/Documentation.docc/Tutorials/Create an Account.tutorial: -------------------------------------------------------------------------------- 1 | @Tutorial(time: 2) { 2 | @Intro(title: "Create an OpenAI Account") { 3 | This guides you through creating a new OpenAI Account. 4 | 5 | @Image(source: "openai_logo_white_on_black.png", alt: "The OpenAI Logo.") 6 | } 7 | 8 | @Section(title: "Creating an OpenAI Account") { 9 | @ContentAndMedia { 10 | This guides you through the basics of creating an OpenAI account. 11 | 12 | @Image(source: "openai_logo_white_on_black.png", alt: "The OpenAI Logo.") 13 | } 14 | 15 | @Steps { 16 | @Step { 17 | Browse to the [OpenAI API Homepage](https://openai.com/api/) 18 | 19 | @Image(source: "openai_api_homepage.png", alt: "The OpenAI Homepage.") 20 | } 21 | 22 | @Step { 23 | Click the "Sign Up" button. 24 | 25 | @Image(source: "openai_api_homepage_signup.png", alt: "The OpenAI Homepage, highlighting the 'Sign Up' button.") 26 | } 27 | 28 | @Step { 29 | Either enter your email address, or select a 3rd-party sign-in option. 30 | 31 | @Image(source: "openai_api_create_account.png", alt: "The OpenAI Account Creation screen, with an option to enter an email addres or select a 3rd-party signing partner.")` 32 | } 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /Tests/OpenAIBitsTests/ModelTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | import CustomDump 3 | @testable import OpenAIBits 4 | 5 | // 01-Jan-2000 00:00:00 UTC 6 | let dateSeconds = 946684800 7 | let date = Date(timeIntervalSince1970: .init(dateSeconds)) 8 | 9 | final class ModelTests: XCTestCase { 10 | 11 | func testDecodeFromJSON() throws { 12 | let model: Model = try jsonDecode(""" 13 | {"id":"alpha","object":"model","created":\(dateSeconds),"owned_by":"beta","permission":[],"root":"alpha"} 14 | """) 15 | 16 | XCTAssertNoDifference( 17 | Model(id: "alpha", created: date, ownedBy: "beta", permission: [], root: "alpha", parent: nil), 18 | model 19 | ) 20 | } 21 | 22 | func testDecodeFromJSONWithPermissions() throws { 23 | let model: Model = try jsonDecode(""" 24 | {"id":"alpha","object":"model","created":\(dateSeconds),"owned_by":"beta","permission":[ 25 | {"id":"gamma","created":\(dateSeconds),"allow_create_engine":true,"allow_sampling":false,"allow_logprobs":true,"allow_search_indices":false,"allow_view":true,"allow_fine_tuning":false,"organization":"omega","group":null,"is_blocking":false}, 26 | ],"root":"alpha"} 27 | """) 28 | 29 | XCTAssertNoDifference( 30 | Model(id: "alpha", created: date, ownedBy: "beta", permission: [ 31 | .init(id: "gamma", created: date, allowSampling: false, allowLogprobs: true, allowSearchIndices: false, allowView: true, allowFineTuning: false, organization: "omega", group: nil, isBlocking: false), 32 | ], root: "alpha", parent: nil), 33 | model 34 | ) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2022, David Peterson 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | 1. Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | 2. Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | 3. Neither the name of the copyright holder nor the names of its 17 | contributors may be used to endorse or promote products derived from 18 | this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /Sources/OpenAIBits/Types/File.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /// Represents a file that has been uploaded to OpenAI. Created via the ``Files/Upload`` and retrieved by ``Files/Detail``. 4 | /// 5 | /// ## Related Calls 6 | /// 7 | /// - ``Files/Content 8 | /// - ``Files/Delete`` 9 | /// - ``Files/Detail`` 10 | /// - ``Files/List`` 11 | /// - ``Files/Upload`` 12 | /// 13 | /// ## See Also 14 | /// 15 | /// - [OpenAI API](https://platform.openai.com/docs/api-reference/files) 16 | public struct File: Identifiable, JSONResponse, Equatable { 17 | /// A ``File`` `ID`. 18 | public struct ID: Identifier { 19 | /// The value. 20 | public let value: String 21 | 22 | /// Creates an `ID` with the specified value. 23 | /// 24 | /// - Parameter value: The value. 25 | public init(_ value: String) { 26 | self.value = value 27 | } 28 | } 29 | 30 | /// The ``File/ID-swift.struct`` of the file. 31 | public let id: ID 32 | 33 | /// The number of bytes in the file. 34 | public let bytes: Int64 35 | 36 | /// The date it was created at. 37 | public let created: Date 38 | 39 | /// The filename. 40 | public let filename: String 41 | 42 | /// The purpose of the file. 43 | public let purpose: String 44 | 45 | /// The status of the file. 46 | public let status: String? 47 | 48 | /// The details of the file status. 49 | public let statusDetails: String? 50 | } 51 | 52 | extension File { 53 | enum CodingKeys: String, CodingKey { 54 | case id 55 | case bytes 56 | case created = "createdAt" 57 | case filename 58 | case purpose 59 | case status 60 | case statusDetails 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /Sources/OpenAIBits/Types/Token.swift: -------------------------------------------------------------------------------- 1 | /// A single token. Each token references a string of one or more characters. 2 | public struct Token: Equatable, Hashable { 3 | /// The `Int` value of the token. 4 | public let value: Int 5 | 6 | /// Initializes a new token. 7 | /// - Parameter value: The token value. 8 | public init(_ value: Int) { 9 | self.value = value 10 | } 11 | } 12 | 13 | // MARK: Codable 14 | 15 | extension Token: Codable { 16 | public init(from decoder: Decoder) throws { 17 | let container = try decoder.singleValueContainer() 18 | try self.init(container.decode(Int.self)) 19 | } 20 | 21 | public func encode(to encoder: Encoder) throws { 22 | var container = encoder.singleValueContainer() 23 | try container.encode(value) 24 | } 25 | } 26 | 27 | extension Token: ExpressibleByIntegerLiteral { 28 | /// A ``Token`` can be initialized directly with an `Int` literal. 29 | /// - Parameter value: The value. 30 | public init(integerLiteral value: Int) { 31 | self.init(value) 32 | } 33 | } 34 | 35 | // MARK: RawRepresentable 36 | 37 | // Note: This allows Tokens to be converted to and from `String`s, which are used when tokens are used as dictionary key in ``Embedding`` results. 38 | 39 | extension Token: RawRepresentable { 40 | public var rawValue: String { 41 | String(value) 42 | } 43 | 44 | public init?(rawValue: String) { 45 | guard let intValue = Int(rawValue) else { 46 | return nil 47 | } 48 | self.value = intValue 49 | } 50 | } 51 | 52 | // MARK: CustomStringConvertible 53 | 54 | extension Token: CustomStringConvertible { 55 | public var description: String { rawValue } 56 | } 57 | -------------------------------------------------------------------------------- /Tests/OpenAIBitsTests/PromptTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import OpenAIBits 3 | 4 | final class PromptTests: XCTestCase { 5 | 6 | func testEncodeStringToJSON() throws { 7 | let prompt: Prompt = .string("alpha") 8 | let json = try jsonEncode(prompt) 9 | XCTAssertEqual("\"alpha\"", json) 10 | } 11 | 12 | func testEncodeStringsToJSON() throws { 13 | let prompt: Prompt = .strings(["alpha","beta"]) 14 | let json = try jsonEncode(prompt) 15 | XCTAssertEqual("[\"alpha\",\"beta\"]", json) 16 | } 17 | 18 | func testEncodeTokenArrayToJSON() throws { 19 | let prompt: Prompt = .tokenArray([1, 2, 100]) 20 | let json = try jsonEncode(prompt) 21 | XCTAssertEqual("[1,2,100]", json) 22 | } 23 | 24 | func testEncodeTokenArraysToJSON() throws { 25 | let prompt: Prompt = .tokenArrays([[1, 2], [100, 200]]) 26 | let json = try jsonEncode(prompt) 27 | XCTAssertEqual("[[1,2],[100,200]]", json) 28 | } 29 | 30 | func testDecodeStringFromJSON() throws { 31 | let result: Prompt = try jsonDecode("\"gamma\"") 32 | XCTAssertEqual(.string("gamma"), result) 33 | } 34 | 35 | func testDecodeStringsFromJSON() throws { 36 | let result: Prompt = try jsonDecode("[\"gamma\",\"delta\"]") 37 | XCTAssertEqual(.strings(["gamma", "delta"]), result) 38 | } 39 | 40 | func testDecodeTokenArraysFromJSON() throws { 41 | let result: Prompt = try jsonDecode("[1,2,100]") 42 | XCTAssertEqual(.tokenArray([1,2,100]), result) 43 | } 44 | 45 | func testDecodeTokensArraysFromJSON() throws { 46 | let result: Prompt = try jsonDecode("[[1,2],[100,200]]") 47 | XCTAssertEqual(.tokenArrays([[1,2],[100,200]]), result) 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /Sources/OpenAIBits/Calls/Stop.swift: -------------------------------------------------------------------------------- 1 | // MARK: Stop 2 | 3 | /// Represents 1 to 4 "stop" `String` values for a ``Text/Completions`` call. 4 | public struct Stop: Equatable, Encodable { 5 | /// The "stop" values. 6 | public let value: [String] 7 | 8 | /// A single stop value. 9 | /// 10 | /// - Parameter v1: The only `String`. 11 | public init(_ v1: String) { 12 | self.value = [v1] 13 | } 14 | 15 | /// Two stop values. 16 | /// 17 | /// - Parameters: 18 | /// - v1: The first `String`. 19 | /// - v2: The second `String`. 20 | public init(_ v1: String, _ v2: String) { 21 | self.value = [v1, v2] 22 | } 23 | 24 | /// Three stop values. 25 | /// 26 | /// - Parameters: 27 | /// - v1: The first `String`. 28 | /// - v2: The second `String`. 29 | /// - v3: The third `String`. 30 | public init(_ v1: String, _ v2: String, _ v3: String) { 31 | self.value = [v1, v2, v3] 32 | } 33 | 34 | /// Four stop values. 35 | /// 36 | /// - Parameters: 37 | /// - v1: The first `String`. 38 | /// - v2: The second `String`. 39 | /// - v3: The third `String`. 40 | /// - v4: The fourth `String`. 41 | public init(_ v1: String, _ v2: String, _ v3: String, _ v4: String) { 42 | self.value = [v1, v2, v3, v4] 43 | } 44 | 45 | /// Encodes the this as a `String` array. 46 | /// - Parameter encoder: The encoder. 47 | public func encode(to encoder: Encoder) throws { 48 | var container = encoder.singleValueContainer() 49 | try container.encode(value) 50 | } 51 | } 52 | 53 | extension Stop: ExpressibleByStringLiteral { 54 | /// Creates a single `Stop` from a single `String` literal. 55 | /// - Parameter value: The `String` literal. 56 | public init(stringLiteral value: String) { 57 | self.init(value) 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version: 5.7 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | let package = Package( 7 | name: "swift-openai-bits", 8 | platforms: [ 9 | .macOS(.v12), 10 | .iOS(.v15), 11 | .tvOS(.v15), 12 | .watchOS(.v8), 13 | ], 14 | products: [ 15 | // Products define the executables and libraries a package produces, and make them visible to other packages. 16 | .library( 17 | name: "OpenAIBits", 18 | targets: ["OpenAIBits"]), 19 | .library( 20 | name: "OpenAIBitsTestHelpers", 21 | targets: ["OpenAIBitsTestHelpers"]), 22 | ], 23 | dependencies: [ 24 | // Dependencies dehclare other packages that this package depends on. 25 | .package(url: "https://github.com/davbeck/MultipartForm", from: "0.1.0"), 26 | .package(url: "https://github.com/pointfreeco/swift-custom-dump", from: "0.8.0"), 27 | .package(url: "https://github.com/apple/swift-docc-plugin", from: "1.1.0"), 28 | .package(url: "https://github.com/apple/swift-format.git", branch: "release/5.7"), 29 | ], 30 | targets: [ 31 | // Targets are the basic building blocks of a package. A target can define a module or a test suite. 32 | // Targets can depend on other targets in this package, and on products in packages this package depends on. 33 | .target( 34 | name: "OpenAIBits", 35 | dependencies: ["MultipartForm"], 36 | resources: [.copy("models")]), 37 | .target( 38 | name: "OpenAIBitsTestHelpers", 39 | dependencies: [ 40 | "OpenAIBits", 41 | .product(name: "CustomDump", package: "swift-custom-dump"), 42 | ]), 43 | .testTarget( 44 | name: "OpenAIBitsTests", 45 | dependencies: [ 46 | "OpenAIBits", 47 | .product(name: "CustomDump", package: "swift-custom-dump"), 48 | ]), 49 | ] 50 | ) 51 | -------------------------------------------------------------------------------- /Sources/OpenAIBits/Types/Edit.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /// An ``Edit`` is the response from an ``Text/Edits`` call. 4 | /// 5 | /// ## Related Calls 6 | /// 7 | ///- ``Text/Edits`` 8 | /// 9 | /// ## See Also 10 | /// 11 | /// - [OpenAI API](https://platform.openai.com/docs/api-reference/edits) 12 | /// - [Editing code guide](https://platform.openai.com/docs/guides/code/editing-code) 13 | public struct Edit: JSONResponse, Equatable { 14 | /// The creation date. 15 | public let created: Date 16 | 17 | /// The list of choices. 18 | public let choices: [Choice] 19 | 20 | /// The token ``Usage`` from the request. 21 | public let usage: Usage 22 | 23 | /// Initializes an edit response. 24 | /// 25 | /// - Parameters: 26 | /// - created: The creation date. 27 | /// - choices: The choices generated. 28 | /// - usage: The token usage from the request. 29 | public init( 30 | created: Date, 31 | choices: [Choice], 32 | usage: Usage 33 | ) { 34 | self.created = created 35 | self.choices = choices 36 | self.usage = usage 37 | } 38 | 39 | /// A convenience accessor for the `text` value for the first ``Choice``. 40 | public var text: String { 41 | choices[0].text 42 | } 43 | } 44 | 45 | extension Edit { 46 | /// A choice returned from an ``Edit`` request. 47 | public struct Choice: Equatable, Codable, CustomStringConvertible { 48 | /// The text of the generated choice. 49 | public let text: String 50 | 51 | /// The index of the choice (`0`-based). 52 | public let index: Int 53 | 54 | /// Initializes the choice. 55 | /// 56 | /// - Parameter text: The text of the generated choice. 57 | /// - Parameter index: The index of the choice. 58 | public init(text: String, index: Int) { 59 | self.text = text 60 | self.index = index 61 | } 62 | 63 | public var description: String { text } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /Sources/OpenAIBits/Types/Prompt.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /// A ``Prompt`` can be a single `String`, an array of `String`s, a ``Token`` array, or an array of ``Token`` arrays. 4 | /// You can also assign it directly with `String` literal value, which will result in a ``Prompt/string(_:)`` value. 5 | public enum Prompt: Equatable { 6 | /// The prompt, as a `String`. 7 | case string(String) 8 | /// The prompt, as an array of `String`s. 9 | case strings([String]) 10 | /// The prompt, as an array of ``Token``s. 11 | case tokenArray([Token]) 12 | /// The prompt, as an array of ``Token`` arrays. 13 | case tokenArrays([[Token]]) 14 | } 15 | 16 | extension Prompt: ExpressibleByStringLiteral, ExpressibleByStringInterpolation { 17 | /// Initialises the ``Prompt`` with a `String` literal value. 18 | /// - Parameter value: The value. 19 | public init(stringLiteral value: String) { 20 | self = .string(value) 21 | } 22 | } 23 | 24 | extension Prompt: Codable { 25 | public init(from decoder: Decoder) throws { 26 | let container = try decoder.singleValueContainer() 27 | do { 28 | self = .string(try container.decode(String.self)) 29 | return 30 | } catch {} 31 | 32 | do { 33 | self = .strings(try container.decode([String].self)) 34 | return 35 | } catch {} 36 | 37 | do { 38 | self = .tokenArray(try container.decode([Token].self)) 39 | return 40 | } catch {} 41 | 42 | self = .tokenArrays(try container.decode([[Token]].self)) 43 | } 44 | 45 | public func encode(to encoder: Encoder) throws { 46 | var container = encoder.singleValueContainer() 47 | switch self { 48 | case .string(let value): 49 | try container.encode(value) 50 | case .strings(let values): 51 | try container.encode(values) 52 | case .tokenArray(let tokens): 53 | try container.encode(tokens) 54 | case .tokenArrays(let tokens): 55 | try container.encode(tokens) 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /Sources/OpenAIBits/Utils/CodableDictionary.swift: -------------------------------------------------------------------------------- 1 | /// Allows a `Dictionary` with a non-`String` key to be `Codable`. 2 | /// Solution originally found [here](https://www.fivestars.blog/articles/codable-swift-dictionaries/). 3 | /// 4 | /// Usage: 5 | /// ```swift 6 | /// enum MyKey: Codable { 7 | /// case alpha 8 | /// case beta 9 | /// } 10 | /// 11 | /// @CodableDictionary var dictionary: [MyKey, String] = [:]() 12 | /// ``` 13 | @propertyWrapper 14 | public struct CodableDictionary: Codable where Key.RawValue: Codable & Hashable { 15 | public var wrappedValue: [Key: Value]? 16 | 17 | public init() { 18 | wrappedValue = nil 19 | } 20 | 21 | public init(wrappedValue: [Key: Value]? = nil) { 22 | self.wrappedValue = wrappedValue 23 | } 24 | 25 | public init(wrappedValue: [Key: Value]) { 26 | self.wrappedValue = wrappedValue 27 | } 28 | 29 | public init(from decoder: Decoder) throws { 30 | let container = try decoder.singleValueContainer() 31 | let rawKeyedDictionary = try container.decode(Optional<[Key.RawValue: Value]>.self) 32 | 33 | guard let rawKeyedDictionary = rawKeyedDictionary else { 34 | wrappedValue = nil 35 | return 36 | } 37 | 38 | var wrappedValue: [Key: Value] = [:] 39 | for (rawKey, value) in rawKeyedDictionary { 40 | guard let key = Key(rawValue: rawKey) else { 41 | throw DecodingError.dataCorruptedError( 42 | in: container, 43 | debugDescription: "Invalid key: cannot initialize '\(Key.self)' from invalid '\(Key.RawValue.self)' value '\(rawKey)'") 44 | } 45 | wrappedValue[key] = value 46 | } 47 | self.wrappedValue = wrappedValue 48 | } 49 | 50 | public func encode(to encoder: Encoder) throws { 51 | // OpenAI API doesn't like receiving `null` keys, and property wrappers always get exported. 52 | let wrappedValue = wrappedValue ?? [:] 53 | var container = encoder.singleValueContainer() 54 | let rawKeyedDictionary = Dictionary(uniqueKeysWithValues: wrappedValue.map { ($0.rawValue, $1) }) 55 | try container.encode(rawKeyedDictionary) 56 | } 57 | } 58 | 59 | extension CodableDictionary: Hashable where Key: Hashable, Value: Hashable {} 60 | extension CodableDictionary: Equatable where Key: Equatable, Value: Equatable {} 61 | -------------------------------------------------------------------------------- /Sources/OpenAIBits/Documentation.docc/Tutorials/Create an API Key.tutorial: -------------------------------------------------------------------------------- 1 | @Tutorial(time: 2) { 2 | @Intro(title: "Create an API Key") { 3 | In order to access the API, you will need to provide an API Key with each request. 4 | 5 | @Image(source: "openai_logo_white_on_black.png", alt: "The OpenAI Logo.") 6 | } 7 | 8 | @Section(title: "Creating an API Key") { 9 | @ContentAndMedia { 10 | Calling the OpenAI API requires a unique key that is linked to an OpenAI account. 11 | 12 | @Image(source: "openai_api_keys.png", alt: "The API Keys admin section.") 13 | } 14 | 15 | @Steps { 16 | @Step { 17 | Navigate to the [API Keys](https://platform.openai.com/account/api-keys) section of the OpenAI site. 18 | 19 | > Note: If you need to, [create an account](doc:Create-an-Account) or sign in. 20 | 21 | @Image(source: "openai_api_keys.png", alt: "The API Keys admin section, listing any existing secret keys.") 22 | } 23 | 24 | @Step { 25 | Click the `"Create new secret key"` button to generate a new key. 26 | 27 | @Image(source: "openai_api_keys_new.png", alt: "The 'Create new secret key' button is under the list of keys.") 28 | } 29 | 30 | @Step { 31 | The new key is generated. This will be the only time the full key is available to copy. 32 | 33 | @Image(source: "openai_api_keys_generated.png", alt: "The generated secret key, with a description and a 'Copy' button.") 34 | } 35 | 36 | @Step { 37 | Copy the generated key, and save it in whatever location you will access it from. 38 | 39 | > Important: Do not store the key in a public location such as a GitHub repository. Not only is it giving anyone access to charge the account for use of the API, but OpenAI will automatically invalidate any public keys it finds on the Internet. 40 | 41 | @Image(source: "openai_api_keys_generated_copy.png", alt: "The generated secret key, with the 'Copy' button highlighted.") 42 | } 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /Tests/OpenAIBitsTests/ImagesTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | import CustomDump 3 | @testable import OpenAIBits 4 | 5 | final class ImagesTests: XCTestCase { 6 | 7 | func testImageDataURLToJSON() throws { 8 | let value = Generations.Image.url(URL(string: "http://foo.bar")!) 9 | XCTAssertNoDifference( 10 | #"{"url":"http://foo.bar"}"#, 11 | try jsonEncode(value, options: [.withoutEscapingSlashes])) 12 | } 13 | 14 | func testImageDataURLFromJSON() throws { 15 | let value: Generations.Image = try jsonDecode(#"{"url":"http://foo.bar"}"#) 16 | XCTAssertNoDifference( 17 | Generations.Image.url(URL(string: "http://foo.bar")!), 18 | value 19 | ) 20 | } 21 | 22 | func testImageDataBase64ToJSON() throws { 23 | let data = "ABC".data(using: .utf8)! 24 | let value = Generations.Image.data(data) 25 | XCTAssertNoDifference( 26 | #"{"b64_json":"QUJD"}"#, 27 | try jsonEncode(value, options: [.withoutEscapingSlashes])) 28 | } 29 | 30 | func testImageDataBase64FromJSON() throws { 31 | let data = "ABC".data(using: .utf8)! 32 | let value: Generations.Image = try jsonDecode(#"{"b64_json":"QUJD"}"#) 33 | XCTAssertNoDifference( 34 | Generations.Image.data(data), 35 | value 36 | ) 37 | } 38 | 39 | func testImageToJSON() throws { 40 | let now = Date(timeIntervalSince1970: 1589478378) 41 | let value = Generations( 42 | created: now, 43 | images: [.url(URL(string: "http://foo.bar")!)] 44 | ) 45 | 46 | XCTAssertNoDifference(""" 47 | {"created":1589478378,"data":[{"url":"http://foo.bar"}]} 48 | """, 49 | try jsonEncode(value, options: [.withoutEscapingSlashes]) 50 | ) 51 | } 52 | 53 | func testImagesRequestFormatToJSON() throws { 54 | let value = Images.ResponseFormat.data 55 | XCTAssertEqual(#""b64_json""#, try jsonEncode(value)) 56 | } 57 | 58 | func testImagesGenerationsToJSON() throws { 59 | let value = Images.Create( 60 | prompt: "foobar", 61 | n: 2, 62 | size: .of256x256, 63 | responseFormat: .data, 64 | user: "jblogs" 65 | ) 66 | 67 | XCTAssertNoDifference( 68 | #"{"n":2,"prompt":"foobar","response_format":"b64_json","size":"256x256","user":"jblogs"}"#, 69 | try jsonEncode(value, options: [.withoutEscapingSlashes,.sortedKeys]) 70 | ) 71 | } 72 | 73 | } 74 | -------------------------------------------------------------------------------- /Sources/OpenAIBits/Calls/Tokens.swift: -------------------------------------------------------------------------------- 1 | /// Encoding and decoding text to and from tokens. 2 | /// 3 | /// ## See Also 4 | /// 5 | /// - ``TokenEncoder`` 6 | public enum Tokens {} 7 | 8 | // MARK: Encode 9 | 10 | extension Tokens { 11 | 12 | /// Encodes the provided ``input`` `String`and encodes it into an array of ``Token``s. 13 | public struct Encode: ExecutableCall { 14 | /// Responds with an array of ``Token``s. 15 | public typealias Response = [Token] 16 | 17 | /// The input `String`. 18 | public let input: String 19 | } 20 | } 21 | 22 | extension Tokens.Encode { 23 | 24 | /// Executes the call with the provided ``OpenAI``. 25 | /// - Parameter client: The ``OpenAI`` details. 26 | /// - Returns: The list of tokens. 27 | func execute(with client: OpenAI) async throws -> Response { 28 | let encoder = try TokenEncoder() 29 | return try encoder.encode(text: input) 30 | } 31 | } 32 | 33 | // MARK: Decode 34 | 35 | extension Tokens { 36 | 37 | fileprivate static var encoder: TokenEncoder? 38 | 39 | /// Retrieves or creates a new ``TokenEncoder`` 40 | /// - Returns: The ``TokenEncoder`` 41 | static func getEncoder() throws -> TokenEncoder { 42 | if let encoder = encoder { 43 | return encoder 44 | } 45 | let encoder = try TokenEncoder() 46 | Self.encoder = encoder 47 | return encoder 48 | } 49 | 50 | /// Decodes the provided list of ``Token``s into a `String`. 51 | public struct Decode: ExecutableCall { 52 | /// Responds with a `String. 53 | public typealias Response = String 54 | 55 | /// The input ``Token``s. 56 | public let input: [Token] 57 | } 58 | } 59 | 60 | extension Tokens.Decode { 61 | /// Executes the call with the provided ``OpenAI``. 62 | /// - Parameter client: The ``OpenAI`` details. 63 | /// - Returns: The `String` result. 64 | func execute(with client: OpenAI) async throws -> String { 65 | let encoder = try Tokens.getEncoder() 66 | return try encoder.decode(tokens: input) 67 | } 68 | } 69 | 70 | // MARK: Count 71 | 72 | extension Tokens { 73 | struct Count: ExecutableCall { 74 | typealias Response = Int 75 | 76 | public let input: String 77 | 78 | func execute(with client: OpenAI) async throws -> Int { 79 | let encoder = try Tokens.getEncoder() 80 | return try encoder.encode(text: input).count 81 | } 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /Sources/OpenAIBits/Types/Generations.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /// A collection of ``Image``s generated by calls to the ``Images`` API. 4 | public struct Generations: JSONResponse, Equatable { 5 | /// The image data. 6 | public enum Image: Codable, Equatable { 7 | /// The URL that the image can be downloaded from. 8 | case url(URL) 9 | 10 | /// The image binary data. 11 | case data(Foundation.Data) 12 | } 13 | 14 | /// The `Date` the images were created. 15 | public let created: Date 16 | 17 | /// The list of images. 18 | public let images: [Image] 19 | } 20 | 21 | extension Generations { 22 | enum CodingKeys: String, CodingKey { 23 | case created 24 | case images = "data" 25 | } 26 | } 27 | 28 | extension Generations.Image { 29 | 30 | /// Loads the `Data` for the image, no matter which response type is nominated. 31 | /// If it is a ``Generations/Image/url(_:)``, the destination will be attempted to be read. 32 | /// If it is a ``Generations`` 33 | /// - Returns: The `Data`. 34 | public func getData() throws -> Foundation.Data { 35 | switch self { 36 | case .url(let url): 37 | return try Data(contentsOf: url) 38 | case .data(let data): 39 | return data 40 | } 41 | } 42 | 43 | enum CodingKeys: String, CodingKey { 44 | case url 45 | case data = "b64Json" 46 | } 47 | 48 | /// Decodes an image from a response data. It will be either an object with a `url` field, containing a string with the URL, or a `b64_json` field, containing a string with the base64-encoded string value for the image data. 49 | public init(from decoder: Decoder) throws { 50 | // get a keyed container 51 | let container = try decoder.container(keyedBy: CodingKeys.self) 52 | // try to decode the url 53 | if let url = try container.decodeIfPresent(URL.self, forKey: .url) { 54 | self = .url(url) 55 | return 56 | } 57 | // try to decode the base64 58 | if let base64 = try container.decodeIfPresent(Data.self, forKey: .data) { 59 | self = .data(base64) 60 | return 61 | } 62 | // if we get here, we couldn't decode either 63 | throw OpenAI.Error.unexpectedResponse("Unknown response format for image.") 64 | } 65 | 66 | public func encode(to encoder: Encoder) throws { 67 | var container = encoder.container(keyedBy: CodingKeys.self) 68 | switch self { 69 | case .url(let url): 70 | try container.encode(url, forKey: .url) 71 | case .data(let data): 72 | try container.encode(data, forKey: .data) 73 | } 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /Sources/OpenAIBits/Documentation.docc/Documentation.md: -------------------------------------------------------------------------------- 1 | # ``OpenAIBits`` 2 | 3 | A Swift library to interact with the OpenAI public API. 4 | 5 | ## Overview 6 | 7 | [OpenAI](https://openai.com) provides a [public API](https://platform.openai.com/docs/introduction) to interact with its machine learning models: GPT-3 (for text) and DALL-E (for images). 8 | 9 | This library provides a Swift-native way of calling the API using `async/await`, so requires being compiled with Swift 5.6+. 10 | 11 | Best efforts are done to keep it up-to-date with the full API. Class, function and variable names generally follow the conventions of the API, with some modifications made where the API is inconsistent (eg. where the "creation date" is sometimes `created` and sometimes `createdAt`, this library uses `created` for all of them.) 12 | 13 | ## Topics 14 | 15 | ### OpenAI Client 16 | 17 | The ``OpenAI`` struct configures your connection to OpenAI. After initialisation, you pass it a ``Call`` to perform an operation. 18 | 19 | - ``OpenAI`` 20 | - ``Call`` 21 | 22 | ### ChatGPT (GPT-3.5) 23 | 24 | ChatGPT models are optimized for dialogue. More information can be found [here](https://openai.com/blog/chatgpt). 25 | 26 | - ``Chat`` 27 | 28 | ### Text (GPT-3) 29 | 30 | OpenAI provides several tools for generating text predictions based on a prompt. This uses their **GPT-3** models, which have different levels of complexity and [cost](https://openai.com/pricing) to execute. 31 | 32 | - ``Text`` 33 | - ``Embeddings`` 34 | - ``Moderations`` 35 | 36 | ### Images (DALL-E) 37 | 38 | Another popular tool is **DALL-E**, which takes a text prompt and creates a new image. It also has the ability to edit existing images and create variations of an image. 39 | 40 | - ``Images`` 41 | 42 | ### Other Calls 43 | 44 | There are also some utility calls available, which provide extra information or facilities for tuning your use of the API. 45 | 46 | - ``Files`` 47 | - ``FineTunes`` 48 | - ``Models`` 49 | - ``Tokens`` 50 | 51 | ### Values 52 | 53 | The calls above result in response values. Some of these are also used as parameters when creating a ``Call`` type. 54 | 55 | - ``BinaryData`` 56 | - ``ChatCompletion`` 57 | - ``ChatMessage`` 58 | - ``ChatModel`` 59 | - ``Completion`` 60 | - ``Edit`` 61 | - ``Embedding`` 62 | - ``File`` 63 | - ``FineTune`` 64 | - ``FinishReason`` 65 | - ``Generations`` 66 | - ``Identifier`` 67 | - ``ListOf`` 68 | - ``Logprobs`` 69 | - ``Model`` 70 | - ``Moderation`` 71 | - ``Penalty`` 72 | - ``Percentage`` 73 | - ``Prompt`` 74 | - ``Token`` 75 | - ``Usage`` 76 | 77 | ### Utility Classes 78 | 79 | - ``TokenEncoder`` 80 | -------------------------------------------------------------------------------- /.swiftpm/xcode/xcshareddata/xcschemes/OpenAIBits.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 43 | 44 | 50 | 51 | 57 | 58 | 59 | 60 | 62 | 63 | 66 | 67 | 68 | -------------------------------------------------------------------------------- /.swiftpm/xcode/xcshareddata/xcschemes/OpenAIBitsTestHelpers.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 43 | 44 | 50 | 51 | 57 | 58 | 59 | 60 | 62 | 63 | 66 | 67 | 68 | -------------------------------------------------------------------------------- /Sources/OpenAIBits/Calls/Moderations.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /// Given a input text, outputs if the model classifies it as violating OpenAI's content policy. 4 | /// 5 | /// ## Calls 6 | /// 7 | /// - ``Moderations/Create`` - Creates a ``Moderation`` assessment. 8 | /// 9 | /// ## See Also 10 | /// 11 | /// - [OpenAI API](https://platform.openai.com/docs/api-reference/moderations) 12 | /// - [Moderations Guide](https://platform.openai.com/docs/guides/moderation) 13 | public enum Moderations {} 14 | 15 | // MARK: Create 16 | 17 | extension Moderations { 18 | 19 | /// Classifies if text violates OpenAI's Content Policy. 20 | /// 21 | /// ## Examples 22 | /// 23 | /// A simple call with the `.latest` model: 24 | /// 25 | /// ```swift 26 | /// let client = OpenAI(apiKey: ...) 27 | /// let moderation = try await client.call(Moderations.Create( 28 | /// input: "This is innocuous text." 29 | /// )) 30 | /// ``` 31 | /// 32 | /// ## See Also 33 | /// 34 | /// - [OpenAI API](https://platform.openai.com/docs/api-reference/moderations/create) 35 | /// - [Moderations Guide](https://platform.openai.com/docs/guides/moderation) 36 | public struct Create: JSONPostCall { 37 | 38 | /// Responds with a ``Moderation``. 39 | public typealias Response = Moderation 40 | 41 | /// The HTTP call path. 42 | var path: String { "moderations" } 43 | 44 | /// The input text to classify. 45 | public let input: Prompt 46 | 47 | /// Two content moderations models are available: ``Moderations/Model/stable`` and ``Moderations/Model/latest``. 48 | /// 49 | /// The default is `.latest` which will be automatically upgraded over time. 50 | /// This ensures you are always using the most accurate model. If you use 51 | /// `.stable`, we will provide advanced notice before updating the model. 52 | /// Accuracy of `.stable` may be slightly lower than for `.latest`. 53 | public let model: Model? 54 | 55 | /// Constructs a ``Moderations/Create`` call. 56 | /// 57 | /// - Parameters: 58 | /// - input: The input text to classify. 59 | /// - model: Two content moderations models are available: `.stable` and `.latest`. Defaults to `.latest`. 60 | public init(input: Prompt, model: Model? = nil) { 61 | self.input = input 62 | self.model = model 63 | } 64 | } 65 | } 66 | 67 | // MARK: Model 68 | 69 | extension Moderations { 70 | /// Two content moderations models are available: ``stable`` and ``latest``. 71 | public enum Model: String, Equatable, CaseIterable, Encodable { 72 | /// The most accurate model, automatically upgraded over time. 73 | case latest = "text-moderation-latest" 74 | /// Advanced notice will be provided before updating the model. 75 | case stable = "text-moderation-stable" 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /Sources/OpenAIBits/Types/Moderation.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | // MARK: Moderation 4 | 5 | /// The response from a ``Moderations/Create`` call. 6 | public struct Moderation: Identifiable, JSONResponse { 7 | /// The unique identifier for the moderation response. 8 | public struct ID: Identifier { 9 | public var value: String 10 | 11 | public init(_ value: String) { 12 | self.value = value 13 | } 14 | } 15 | 16 | /// The ``Moderation/ID-swift.struct``. 17 | public let id: ID 18 | 19 | /// The actual moderation ``Model/ID-swift.struct`` used to perform the moderation. 20 | public let model: Model.ID 21 | 22 | /// The list of ``Moderation/Result``s. 23 | public let results: [Result] 24 | 25 | /// Creates a ``Moderation`` with a specified `id`, `model` and `result`. 26 | /// 27 | /// - Parameters: 28 | /// - id: The ``Moderation/ID-swift.struct``. 29 | /// - model: The actual ``Model/ID-swift.struct`` used to perform the moderation. 30 | /// - results: The list of ``Moderation/Result``s. 31 | public init(id: ID, model: Model.ID, results: [Result]) { 32 | self.id = id 33 | self.model = model 34 | self.results = results 35 | } 36 | } 37 | 38 | // MARK: Moderation.Category 39 | 40 | extension Moderation { 41 | /// ``Moderation`` categories. 42 | public enum Category: String, Hashable, Codable, CaseIterable, CustomStringConvertible { 43 | case hate 44 | case hateThreatening = "hate/threatening" 45 | case selfHarm = "self-harm" 46 | case sexual 47 | case sexualMinors = "sexual/minors" 48 | case violence 49 | case violenceGraphic = "violence/graphic" 50 | 51 | public var description: String { rawValue } 52 | } 53 | } 54 | 55 | // MARK: Moderation.Result 56 | 57 | extension Moderation { 58 | /// A single ``Moderation`` result. 59 | public struct Result: Codable, Equatable { 60 | /// The set of ``Moderation/Category`` types in the ``Moderation/Result``. 61 | @CodableDictionary public var categories: [Category: Bool]? 62 | 63 | /// The set of ``Moderation/Category`` scores in the ``Moderation/Result``. 64 | @CodableDictionary public var categoryScores: [Category: Decimal]? 65 | 66 | /// Indicates if the result was flagged in any category. 67 | public let flagged: Bool 68 | 69 | /// Constructs a ``Moderation/Result``. 70 | /// 71 | /// - Parameters: 72 | /// - categories: The set of ``Moderation/Category`` results. 73 | /// - categoryScores: The set of ``Moderation/Category`` scores. 74 | /// - flagged: Is `true` if any category was flagged. 75 | public init(categories: [Category : Bool], categoryScores: [Category : Decimal], flagged: Bool) { 76 | self.categories = categories 77 | self.categoryScores = categoryScores 78 | self.flagged = flagged 79 | } 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /Sources/OpenAIBits/CallTypes/HTTPCall.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /// A type of ``Call`` that will be triggered via a HTTP request. 4 | protocol HTTPCall: Call, HTTPRequestable, ExecutableCall where Response: HTTPResponse {} 5 | 6 | extension HTTPCall { 7 | func execute(with client: OpenAI) async throws -> Response { 8 | let urlStr = "\(BASE_URL)/\(path)" 9 | 10 | guard let url = URL(string: urlStr) else { 11 | throw OpenAI.Error.invalidURL(urlStr) 12 | } 13 | 14 | var request = URLRequest(url: url) 15 | request.setValue("Bearer \(client.apiKey)", forHTTPHeaderField: "Authorization") 16 | if let organization = client.organization { 17 | request.setValue(organization, forHTTPHeaderField: "OpenAI-Organization") 18 | } 19 | 20 | request.httpMethod = method 21 | 22 | if let contentType = contentType { 23 | request.setValue(contentType, forHTTPHeaderField: "Content-Type") 24 | } 25 | if let body = try getBody() { 26 | request.httpBody = body 27 | } 28 | 29 | do { 30 | client.log?("Request: \(request.httpMethod ?? "GET") \(request)") 31 | logHeaders(request.allHTTPHeaderFields, from: "Request", to: client.log) 32 | if let httpBody = request.httpBody { 33 | client.log?("Request Data:\n\(String(decoding: httpBody, as: UTF8.self))") 34 | } 35 | 36 | let (result, response) = try await URLSession.shared.data(for: request) 37 | 38 | guard let httpResponse = response as? HTTPURLResponse else { 39 | throw OpenAI.Error.unexpectedResponse("Expected an HTTPURLResponse") 40 | } 41 | 42 | client.log?("Response Status: \(httpResponse.statusCode)") 43 | logHeaders(httpResponse.allHeaderFields, from: "Response", to: client.log) 44 | 45 | client.log?("Response Data:\n\(String(decoding: result, as: UTF8.self))") 46 | 47 | guard httpResponse.statusCode == 200 else { 48 | if isJSON(response: httpResponse) { 49 | do { 50 | throw try ErrorResponse(data: result, response: httpResponse).error 51 | } catch { 52 | throw error 53 | } 54 | } else { 55 | throw OpenAI.Error.unexpectedResponse("\(httpResponse.statusCode): \(String(decoding: result, as: UTF8.self))") 56 | } 57 | } 58 | return try Response(data: result, response: httpResponse) 59 | } catch { 60 | client.log?("Error: \(error)\n") 61 | throw error 62 | } 63 | } 64 | } 65 | 66 | // MARK: Private constants 67 | 68 | fileprivate let BASE_URL = "https://api.openai.com/v1" 69 | 70 | /// Used to parse an error response from the API. 71 | private struct ErrorResponse: JSONResponse { 72 | let error: OpenAI.Error 73 | } 74 | 75 | private func logHeaders(_ headers: [AnyHashable:Any]?, from label: String, to log: OpenAI.Logger?) { 76 | guard let log = log, let headers = headers else { return } 77 | 78 | log("\(label) Headers:") 79 | log("-------------------------------------------") 80 | for (k,v) in headers { 81 | log("\(k): \(v)") 82 | } 83 | log("-------------------------------------------") 84 | } 85 | 86 | -------------------------------------------------------------------------------- /Sources/OpenAIBits/Types/Model+ids.swift: -------------------------------------------------------------------------------- 1 | 2 | // MARK: Common Model IDs. 3 | 4 | extension Model.ID { 5 | /// Most capable GPT-3 model. Can do any task the other models can do, often with less context. In addition to responding to ``Text/Completions``, also supports [inserting](https://platform.openai.com/docs/guides/completion/inserting-text) completions within text. 6 | /// 7 | /// - Max Tokens: 4,000 8 | /// - Training Data: Up to Jun 2021 9 | public static var text_davinci_003: Self { "text-davinci-003" } 10 | 11 | /// Previous generation of `Davinci`. Can do any task the other models can do, often with less context. In addition to responding to ``Text/Completions``, also supports [inserting](https://platform.openai.com/docs/guides/completion/inserting-text) completions within text. 12 | /// 13 | /// - Max Tokens: 4,000 14 | /// - Training Data: Up to Jun 2021 15 | @available(*, deprecated, message: "Use text_davinci_003 instead.", renamed: "text_davinci_003") 16 | public static var text_davinci_002: Self { "text-davinci-002" } 17 | 18 | /// Very capable, but faster and lower cost than `Davinci`. 19 | /// 20 | /// - Max Tokens: 2,048 21 | /// - Training Data: Up to Oct 2019 22 | public static var text_curie_001: Self { "text-curie-001" } 23 | 24 | /// Capable of straightforward tasks, very fast, and lower cost. 25 | /// 26 | /// - Max Tokens: 2,048 27 | /// - Training Data: Up to Oct 2019 28 | public static var text_babbage_001: Self { "text-babbage-001" } 29 | 30 | /// Capable of very simple tasks, usually the fastest model in the GPT-3 series, and lowest cost. 31 | /// 32 | /// - Max Tokens: 2,048 33 | /// - Training Data: Up to Oct 2019 34 | public static var text_ada_001: Self { "text-ada-001" } 35 | 36 | /// A variation of the `Davinci` model for use with ``Text/Edits``. 37 | public static var text_davinci_edit_001: Self { "text-davinci-edit-001" } 38 | 39 | /// A code-focused variation of the `Davinci` model for use with ``Text/Edits``. 40 | public static var code_davinci_edit_001: Self { "code-davinci-edit-001" } 41 | 42 | /// Most capable Codex model. Particularly good at translating natural language to code. In addition to completing code, also supports inserting completions within code. 43 | /// 44 | /// - Max Tokens: 8,000 45 | /// - Training Data: Up to Jun 2021 46 | /// 47 | /// - Note: Currently in Private beta. 48 | public static var code_davinci_001: Self { "code-davinci-001" } 49 | 50 | /// Almost as capable as Davinci Codex, but slightly faster. This speed advantage may make it preferable for real-time applications. 51 | /// 52 | /// - Max Tokens: 2,048 53 | /// - Training Data: Unknown 54 | /// 55 | /// - Note: Currently in Private beta. 56 | public static var code_cushman_001: Self { "code-cushman-001" } 57 | 58 | /// Second-generation embedding model based on Ada. 59 | /// 60 | /// - Max Input Tokens: 8191 61 | /// - Output Dimensions: 1536 62 | public static var text_embedding_ada_002: Self { "text-embedding-ada-002" } 63 | 64 | /// Whisper transcribes audio to text. 65 | public static var whisper_1: Self { "whisper-1" } 66 | } 67 | -------------------------------------------------------------------------------- /Sources/OpenAIBits/Types/ChatCompletion.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /// Response from a ``Chat/Completions`` request. 4 | /// 5 | /// ## Related Calls 6 | /// 7 | /// - ``Chat/Completions`` 8 | /// 9 | /// ## See Also 10 | /// 11 | /// - [OpenAI API](https://platform.openai.com/docs/api-reference/chat) 12 | /// - [Chat Guide](https://platform.openai.com/docs/guides/chat/chat-completions-beta) 13 | public struct ChatCompletion: Identifiable, JSONResponse, Equatable { 14 | /// The unique identifier for a ``Completion``. 15 | public struct ID: Identifier { 16 | public let value: String 17 | 18 | public init(_ value: String) { 19 | self.value = value 20 | } 21 | } 22 | 23 | /// The unique `ID` of the ``Completion``. 24 | public let id: ID 25 | 26 | /// The creation `Date`. 27 | public let created: Date 28 | 29 | /// The `Model.ID` the completion was generated by. 30 | public let model: ChatModel 31 | 32 | /// The list of ``Choice`` options generated. 33 | public let choices: [Choice] 34 | 35 | /// The token ``Usage`` stats for the generation. 36 | public let usage: Usage 37 | 38 | /// Constructs a ``Completion`` result. 39 | /// 40 | /// - Parameters: 41 | /// - id: The unique `ID` 42 | /// - created: The creation `Date`. 43 | /// - model: The `Model.ID` the completion was generated by. 44 | /// - choices: The list of ``Choice`` options generated. 45 | /// - usage: The token ``Usage`` stats for the generation. 46 | public init( 47 | id: ID, 48 | created: Date, 49 | model: ChatModel, 50 | choices: [Choice], 51 | usage: Usage 52 | ) { 53 | self.id = id 54 | self.created = created 55 | self.model = model 56 | self.choices = choices 57 | self.usage = usage 58 | } 59 | 60 | /// A convenience accessor for the `text` value for the first ``Choice``. 61 | public var message: ChatMessage { 62 | choices[0].message 63 | } 64 | 65 | /// A convenience accessor for the ``FinishReason`` for the first ``Choice``. 66 | public var finishReason: FinishReason? { 67 | choices[0].finishReason 68 | } 69 | } 70 | 71 | // MARK: Completion.Choice 72 | 73 | extension ChatCompletion { 74 | /// One of the ``Completion`` choices. 75 | public struct Choice: Equatable, Codable { 76 | /// The ``ChatMessage`` of the completion. 77 | public let message: ChatMessage 78 | 79 | /// Which completion number (`0`-based). 80 | public let index: Int 81 | 82 | /// The reason for finishing. 83 | public let finishReason: FinishReason? 84 | 85 | /// Creates a ``Completion/Choice``. 86 | /// 87 | /// - Parameters: 88 | /// - message: ``ChatMessage`` 89 | /// - index: Which completion number (`0`-based). 90 | /// - finishReason: The reason for finishing. 91 | public init( 92 | message: ChatMessage, 93 | index: Int, 94 | finishReason: FinishReason? 95 | ) { 96 | self.message = message 97 | self.index = index 98 | self.finishReason = finishReason 99 | } 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "pins" : [ 3 | { 4 | "identity" : "multipartform", 5 | "kind" : "remoteSourceControl", 6 | "location" : "https://github.com/davbeck/MultipartForm", 7 | "state" : { 8 | "revision" : "c91979ea956a360bf84f0cc480afbcc7c946c753", 9 | "version" : "0.1.0" 10 | } 11 | }, 12 | { 13 | "identity" : "swift-argument-parser", 14 | "kind" : "remoteSourceControl", 15 | "location" : "https://github.com/apple/swift-argument-parser.git", 16 | "state" : { 17 | "revision" : "9f39744e025c7d377987f30b03770805dcb0bcd1", 18 | "version" : "1.1.4" 19 | } 20 | }, 21 | { 22 | "identity" : "swift-custom-dump", 23 | "kind" : "remoteSourceControl", 24 | "location" : "https://github.com/pointfreeco/swift-custom-dump", 25 | "state" : { 26 | "revision" : "dd86159e25c749873f144577e5d18309bf57534f", 27 | "version" : "0.8.0" 28 | } 29 | }, 30 | { 31 | "identity" : "swift-docc-plugin", 32 | "kind" : "remoteSourceControl", 33 | "location" : "https://github.com/apple/swift-docc-plugin.git", 34 | "state" : { 35 | "revision" : "10bc670db657d11bdd561e07de30a9041311b2b1", 36 | "version" : "1.1.0" 37 | } 38 | }, 39 | { 40 | "identity" : "swift-docc-symbolkit", 41 | "kind" : "remoteSourceControl", 42 | "location" : "https://github.com/apple/swift-docc-symbolkit", 43 | "state" : { 44 | "revision" : "b45d1f2ed151d057b54504d653e0da5552844e34", 45 | "version" : "1.0.0" 46 | } 47 | }, 48 | { 49 | "identity" : "swift-format", 50 | "kind" : "remoteSourceControl", 51 | "location" : "https://github.com/apple/swift-format.git", 52 | "state" : { 53 | "branch" : "release/5.7", 54 | "revision" : "5f184220d032a019a63df457cdea4b9c8241e911" 55 | } 56 | }, 57 | { 58 | "identity" : "swift-syntax", 59 | "kind" : "remoteSourceControl", 60 | "location" : "https://github.com/apple/swift-syntax", 61 | "state" : { 62 | "revision" : "72d3da66b085c2299dd287c2be3b92b5ebd226de", 63 | "version" : "0.50700.1" 64 | } 65 | }, 66 | { 67 | "identity" : "swift-system", 68 | "kind" : "remoteSourceControl", 69 | "location" : "https://github.com/apple/swift-system.git", 70 | "state" : { 71 | "revision" : "836bc4557b74fe6d2660218d56e3ce96aff76574", 72 | "version" : "1.1.1" 73 | } 74 | }, 75 | { 76 | "identity" : "swift-tools-support-core", 77 | "kind" : "remoteSourceControl", 78 | "location" : "https://github.com/apple/swift-tools-support-core.git", 79 | "state" : { 80 | "revision" : "4f07be3dc201f6e2ee85b6942d0c220a16926811", 81 | "version" : "0.2.7" 82 | } 83 | }, 84 | { 85 | "identity" : "xctest-dynamic-overlay", 86 | "kind" : "remoteSourceControl", 87 | "location" : "https://github.com/pointfreeco/xctest-dynamic-overlay", 88 | "state" : { 89 | "revision" : "62041e6016a30f56952f5d7d3f12a3fd7029e1cd", 90 | "version" : "0.8.3" 91 | } 92 | } 93 | ], 94 | "version" : 2 95 | } 96 | -------------------------------------------------------------------------------- /Tests/OpenAIBitsTests/ChatCompletionsTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | import CustomDump 3 | @testable import OpenAIBits 4 | 5 | final class ChatCompletionsTests: XCTestCase { 6 | 7 | func testCompletionsStringRequestToJSON() throws { 8 | let value = Chat.Completions( 9 | model: .gpt_3_5_turbo, 10 | messages: [ 11 | .init(role: .user, content: "Input string.") 12 | ] 13 | ) 14 | XCTAssertNoDifference( 15 | #"{"logit_bias":{},"messages":[{"content":"Input string.","role":"user"}],"model":"gpt-3.5-turbo"}"#, 16 | try jsonEncode(value, options: [.sortedKeys]) 17 | ) 18 | } 19 | 20 | func testCompletionsFullRquestToJSON() throws { 21 | let value = Chat.Completions( 22 | model: .gpt_3_5_snapshot("0301"), 23 | messages: .from(user: "Content."), 24 | maxTokens: 100, temperature: 0.5, topP: 0.6, n: 2, 25 | stop: "four", presencePenalty: 0.7, frequencyPenalty: 0.8, 26 | logitBias: [5234: -100], 27 | user: "jblogs" 28 | ) 29 | XCTAssertNoDifference( 30 | #"{"frequency_penalty":0.8,"logit_bias":{"5234":-100},"max_tokens":100,"messages":[{"content":"Content.","role":"user"}],"model":"gpt-3.5-turbo-0301","n":2,"presence_penalty":0.7,"stop":["four"],"temperature":0.5,"top_p":0.6,"user":"jblogs"}"#, 31 | try jsonEncode(value, options: [.sortedKeys]) 32 | ) 33 | } 34 | 35 | func testCompletionsResponseFromJSON() throws { 36 | XCTAssertNoDifference( 37 | ChatCompletion( 38 | id: "a", 39 | created: .init(timeIntervalSince1970: .zero), 40 | model: .gpt_3_5_turbo, 41 | choices: [ 42 | .init( 43 | message: .init(role: .assistant, content: "choice 1"), 44 | index: 0, finishReason: .length 45 | ), 46 | .init( 47 | message: .init(role: .assistant, content: "choice 2"), 48 | index: 1, finishReason: .stop 49 | ), 50 | ], 51 | usage: .init(promptTokens: 1, completionTokens: 2, totalTokens: 3)) 52 | , 53 | try jsonDecode(""" 54 | { 55 | "id": "a", "created": 0, "model": "gpt-3.5-turbo", 56 | "choices": [ 57 | { 58 | "message": {"role": "assistant", "content": "choice 1"}, 59 | "index": 0, 60 | "finish_reason": "length" 61 | }, 62 | { 63 | "message": {"role": "assistant", "content": "choice 2"}, 64 | "index": 1, 65 | "finish_reason": "stop" 66 | } 67 | ], 68 | "usage": {"prompt_tokens": 1, "completion_tokens": 2, "total_tokens": 3} 69 | } 70 | """) 71 | ) 72 | } 73 | 74 | func testCompletionsCreateStop() throws { 75 | XCTAssertEqual(["One"], Stop("One").value) 76 | XCTAssertEqual(["One", "Two"], Stop("One", "Two").value) 77 | XCTAssertEqual(["One", "Two", "Three"], Stop("One", "Two", "Three").value) 78 | XCTAssertEqual(["One", "Two", "Three", "Four"], Stop("One", "Two", "Three", "Four").value) 79 | } 80 | 81 | func testCompletionsCreateStopToJSON() throws { 82 | let value = Stop("One", "Two") 83 | try XCTAssertEqual(jsonEncode(value), """ 84 | ["One","Two"] 85 | """) 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /Sources/OpenAIBitsTestHelpers/ClientHelpers.swift: -------------------------------------------------------------------------------- 1 | import CustomDump 2 | @testable import OpenAIBits 3 | import XCTest 4 | 5 | //XCTAssertExpectOpenAICall { 6 | // Completions(model: "foobar", prompt: "ABC") 7 | //} returning: { 8 | // Completions.Response(...) 9 | //} whileDoing: { 10 | // try await cmd.validate() 11 | // try await cmd.run() 12 | //} 13 | 14 | /// An `Error` implementation for problems while running a call. 15 | enum TestCallError: Swift.Error { 16 | case expected(call: Any, received: Any) 17 | } 18 | 19 | class TestCallHandler: CallHandler { 20 | 21 | var called = false 22 | let expectedCall: E 23 | let returning: E.Response 24 | 25 | init(expectedCall: E, returning: E.Response) { 26 | self.expectedCall = expectedCall 27 | self.returning = returning 28 | } 29 | 30 | func execute(call: C, with client: OpenAIBits.OpenAI) async throws -> C.Response where C : OpenAIBits.Call { 31 | guard !called else { 32 | throw TestCallError.expected(call: expectedCall, received: call) 33 | } 34 | guard let call = call as? E else { 35 | throw TestCallError.expected(call: expectedCall, received: call) 36 | } 37 | called = true 38 | guard call == expectedCall else { 39 | throw TestCallError.expected(call: expectedCall, received: call) 40 | } 41 | return returning as! C.Response 42 | } 43 | } 44 | 45 | /// A function for use during `XCTestCase` evaluation to test an `OpenAIBits` `Call`. 46 | /// 47 | /// ## Examples 48 | /// 49 | /// ```swift 50 | /// func testModelsDetail() async throws { 51 | /// let client = OpenAI(apiKey: "foobar") 52 | /// let now = Date() 53 | /// 54 | /// XCTAssertExpectOpenAICall { 55 | /// Models.Detail(id: "my-model") 56 | /// } response: { 57 | /// Model(id: "my-model", created: now, organization: "my-org") 58 | /// } doing: { 59 | /// let response = try await client.call(Models.Detail(id: "my-model")) 60 | /// XCTAssertEqual(response, Model(id: "my-model", created: now, organization: "my-org") 61 | /// } 62 | /// } 63 | /// ``` 64 | /// 65 | /// Of course, this is pretty useless by itself. Where it is helpful is when working with another library that is making calls to `OpenAI` on your behalf. 66 | /// 67 | /// - Parameters: 68 | /// - call: Returns the expected `OpenAIBits.Call` instance. 69 | /// - response: Returns the `OpenAIBits.Call.Response` instance. 70 | /// - doing: A closure containing the code that will exercise the `OpenAIBits.OpenAI.run()` function. 71 | /// - file: The originating file (defaults to the calling file). 72 | /// - line: The originating line number (defaults to the originating line number). 73 | /// - Throws: An error if `whileDoing` throws an error. 74 | public func XCTAssertExpectOpenAICall( 75 | _ call: () -> C, 76 | response: () -> C.Response, 77 | doing: () async throws -> Void, 78 | file: StaticString = #file, 79 | line: UInt = #line 80 | ) async rethrows { 81 | let oldHandler = OpenAI.handler 82 | defer { OpenAI.handler = oldHandler } 83 | 84 | OpenAI.handler = TestCallHandler(expectedCall: call(), returning: response()) 85 | 86 | do { 87 | try await doing() 88 | } catch let TestCallError.expected(call: expectedCall, received: receivedCall) { 89 | XCTAssertNoDifference(String(describing: expectedCall), String(describing: receivedCall), file: file, line: line) 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /Sources/OpenAIBits/Documentation.docc/Tutorials/Creating Text Completions.tutorial: -------------------------------------------------------------------------------- 1 | @Tutorial(time: 5) { 2 | @Intro(title: "Creating Text Completions") { 3 | Learn how to use the ``OpenAIBits/Text/Completions`` call to generate a completion for a text prompt. 4 | } 5 | 6 | @Section(title: "Make a basic call") { 7 | @ContentAndMedia { 8 | The only required parameters for a text completion call are the ``OpenAIBits/Model/ID-swift.struct`` and the text prompt. This will use default values for the remaining parameters, as defined in the [OpenAI API](https://platform.openai.com/docs/api-reference/completions/create). In particular, the number of tokens in the response currently defaults to `16`. 9 | } 10 | 11 | @Steps { 12 | @Step { 13 | Create the ``OpenAIBits/OpenAI`` instance, using your personal Open AI Key. 14 | 15 | > Important: It is not recommended to hard-code an API Key into your source code. Instead, store it in the Keychain, a plist, or in an environment variable if working in on a command line app. 16 | 17 | @Code(name: "HumptyDumpty.swift", file: "create_openai.swift") 18 | } 19 | 20 | @Step { 21 | Next, create the ``OpenAIBits/Text/Completions`` call. 22 | 23 | @Code(name: "HumptyDumpty.swift", file: "HumptyDumpty_01.swift") 24 | } 25 | 26 | @Step { 27 | Then, send it to OpenAI. 28 | 29 | The method is asynchronous and may throw an error if there is an issue, so we wrap it in a `do`/`catch`. 30 | 31 | @Code(name: "HumptyDumpty.swift", file: "HumptyDumpty_02.swift") 32 | } 33 | 34 | @Step { 35 | Lastly, print the text from the first ``OpenAIBits/Completion/Choice``. 36 | 37 | > Note: If a ``OpenAIBits/Completion`` is returned, it should always have at least one value. If you don't set the `'n'` parameter in the call, one value will be returned. 38 | 39 | @Code(name: "HumptyDumpty.swift", file: "HumptyDumpty_03.swift") 40 | } 41 | 42 | @Step { 43 | However, this is a common situation, so you can access the `text` value of the first choice directly from the ``OpenAIBits/Completion/text`` property. 44 | 45 | @Code(name: "HumptyDumpty.swift", file: "HumptyDumpty_04.swift") 46 | } 47 | } 48 | } 49 | 50 | @Section(title: "Make a more complex call") { 51 | @ContentAndMedia { 52 | Calling the API directly is probably going to be less common. Usually you will have a particular goal in mind, but want user input to make it specific. 53 | 54 | In this case, we will build a function that gives us a to-list for accomplishing a user-defined task. 55 | } 56 | 57 | @Steps { 58 | @Step { 59 | This time, let's define a function that will take the task description and return the list as a ``String``. 60 | 61 | @Code(name: "ListRequiredSteps.swift", file: "ListRequiredSteps_01.swift") 62 | } 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /Sources/OpenAIBits/Types/ChatMessage.swift: -------------------------------------------------------------------------------- 1 | /// Represents a chat message. 2 | public struct ChatMessage: Equatable, Codable { 3 | /// The role of the message. 4 | public let role: Role 5 | 6 | /// The content of the message. 7 | public let content: String 8 | 9 | /// Creates a ``ChatMessage``. 10 | /// 11 | /// - Parameters: 12 | /// - role: The role of the message. 13 | /// - content: The content of the message. 14 | public init(role: Role, content: String) { 15 | self.role = role 16 | self.content = content 17 | } 18 | } 19 | 20 | extension ChatMessage { 21 | /// The role of a ``ChatMessage``. 22 | public enum Role: String, Equatable, Codable, CustomStringConvertible { 23 | /// Sets up the context for the conversation. 24 | case system 25 | 26 | /// The message is from the assistant. 27 | case assistant 28 | 29 | /// The message is from the user. 30 | case user 31 | 32 | public var description: String { rawValue } 33 | } 34 | } 35 | 36 | // MARK: Array of ChatMessage 37 | 38 | public extension Array where Element == ChatMessage { 39 | /// Creates a new array of ``ChatMessage``s with a single ``ChatMessage/Role/system`` role message. 40 | /// 41 | /// - Parameter content: The message to add. 42 | /// - Returns: A new array of ``ChatMessage``s with a single ``ChatMessage/Role/system`` role message. 43 | static func from(system content: String) -> [ChatMessage] { 44 | [.init(role: .system, content: content)] 45 | } 46 | 47 | /// Creates a new array of ``ChatMessage``s with a single `assistant` role message. 48 | /// 49 | /// - Parameter message: The message to add. 50 | /// - Returns: A new array of ``ChatMessage``s with a single `assistant` role message. 51 | static func from(assistant message: String) -> [ChatMessage] { 52 | [.init(role: .assistant, content: message)] 53 | } 54 | 55 | /// Creates a new array of ``ChatMessage``s with a single `user` role message. 56 | /// 57 | /// - Parameter message: The message to add. 58 | /// - Returns: A new array of ``ChatMessage``s with a single `user` role message. 59 | static func from(user message: String) -> [ChatMessage] { 60 | [.init(role: .user, content: message)] 61 | } 62 | 63 | /// Adds a new ``ChatMessage/Role/system`` role message to the returned array. 64 | /// 65 | /// - Parameter content: The message to add. 66 | /// - Returns: A new array of ``ChatMessage``s with a single ``ChatMessage/Role/system`` role message. 67 | func from(system content: String) -> [ChatMessage] { 68 | var messages = self 69 | messages.append(.init(role: .system, content: content)) 70 | return messages 71 | } 72 | 73 | /// Adds a new `assistant` role message to the returned array. 74 | /// 75 | /// - Parameter message: The message to add. 76 | /// - Returns: A new array of ``ChatMessage``s with a single `assistant` role message. 77 | func from(assistant message: String) -> [ChatMessage] { 78 | var messages = self 79 | messages.append(.init(role: .assistant, content: message)) 80 | return messages 81 | } 82 | 83 | /// Adds a new `user` role message to the returned array. 84 | /// 85 | /// - Parameter message: The message to add. 86 | /// - Returns: A new array of ``ChatMessage``s with a single `user` role message. 87 | func from(user message: String) -> [ChatMessage] { 88 | var messages = self 89 | messages.append(.init(role: .user, content: message)) 90 | return messages 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /Tests/OpenAIBitsTests/TextCompletionsTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | import CustomDump 3 | @testable import OpenAIBits 4 | 5 | final class TextCompletionsTests: XCTestCase { 6 | 7 | func testCompletionsStringRequestToJSON() throws { 8 | let value = Text.Completions( 9 | model: .text_davinci_003, 10 | prompt: "Input string." 11 | ) 12 | XCTAssertNoDifference( 13 | #"{"logit_bias":{},"model":"text-davinci-003","prompt":"Input string."}"#, 14 | try jsonEncode(value, options: [.sortedKeys]) 15 | ) 16 | } 17 | 18 | func testCompletionsFullRquestToJSON() throws { 19 | let value = Text.Completions( 20 | model: "foo", prompt: "bar", suffix: "yada", 21 | maxTokens: 100, temperature: 0.5, topP: 0.6, n: 2, logprobs: 3, echo: false, 22 | stop: .init("four"), presencePenalty: 0.7, frequencyPenalty: 0.8, bestOf: 4, 23 | logitBias: [5234: -100], 24 | user: "jblogs" 25 | ) 26 | XCTAssertNoDifference( 27 | #"{"best_of":4,"echo":false,"frequency_penalty":0.8,"logit_bias":{"5234":-100},"logprobs":3,"max_tokens":100,"model":"foo","n":2,"presence_penalty":0.7,"prompt":"bar","stop":["four"],"suffix":"yada","temperature":0.5,"top_p":0.6,"user":"jblogs"}"#, 28 | try jsonEncode(value, options: [.sortedKeys]) 29 | ) 30 | } 31 | 32 | func testCompletionsResponseFromJSON() throws { 33 | XCTAssertNoDifference( 34 | Completion( 35 | id: "a", 36 | created: .init(timeIntervalSince1970: .zero), 37 | model: "b", 38 | choices: [ 39 | .init( 40 | text: "choice 1", index: 0, 41 | logprobs: .init( 42 | tokens: ["choice", " 1"], 43 | tokenLogprobs: [-1, -2], 44 | topLogprobs: [ 45 | ["a": -1, "b": -2, "c": -3], 46 | ["d": -4, "e": -5, "f": -6], 47 | ], 48 | textOffset: [1, 7] 49 | ), 50 | finishReason: .length 51 | ), 52 | .init( 53 | text: "choice 2", index: 1, 54 | finishReason: .stop 55 | ), 56 | ], 57 | usage: .init(promptTokens: 1, completionTokens: 2, totalTokens: 3)) 58 | , 59 | try jsonDecode(""" 60 | { 61 | "id": "a", "created": 0, "model": "b", 62 | "choices": [ 63 | { 64 | "text": "choice 1", 65 | "index": 0, 66 | "logprobs": { 67 | "tokens": ["choice", " 1"], 68 | "token_logprobs": [-1, -2], 69 | "top_logprobs": [ 70 | {"a": -1, "b": -2, "c": -3}, 71 | {"d": -4, "e": -5, "f": -6} 72 | ], 73 | "text_offset": [1, 7] 74 | }, 75 | "finish_reason": "length" 76 | }, 77 | {"text": "choice 2", "index": 1, "finish_reason": "stop"} 78 | ], 79 | "usage": {"prompt_tokens": 1, "completion_tokens": 2, "total_tokens": 3} 80 | } 81 | """) 82 | ) 83 | } 84 | 85 | func testCompletionsCreateStop() throws { 86 | XCTAssertEqual(["One"], Stop("One").value) 87 | XCTAssertEqual(["One", "Two"], Stop("One", "Two").value) 88 | XCTAssertEqual(["One", "Two", "Three"], Stop("One", "Two", "Three").value) 89 | XCTAssertEqual(["One", "Two", "Three", "Four"], Stop("One", "Two", "Three", "Four").value) 90 | } 91 | 92 | func testCompletionsCreateStopToJSON() throws { 93 | let value = Stop("One", "Two") 94 | try XCTAssertEqual(jsonEncode(value), """ 95 | ["One","Two"] 96 | """) 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /Sources/OpenAIBits/Types/Completion.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /// Response from a ``Text/Completions`` request. 4 | /// 5 | /// ## Related Calls 6 | /// 7 | /// - ``Text/Completions`` 8 | /// 9 | /// ## See Also 10 | /// 11 | /// - [OpenAI API](https://platform.openai.com/docs/api-reference/completions) 12 | /// - [Text Completion Guide](https://platform.openai.com/docs/guides/completion) 13 | /// - [Code Completion Guide](https://platform.openai.com/docs/guides/code) 14 | public struct Completion: Identifiable, JSONResponse, Equatable { 15 | /// The unique identifier for a ``Completion``. 16 | public struct ID: Identifier { 17 | public let value: String 18 | 19 | public init(_ value: String) { 20 | self.value = value 21 | } 22 | } 23 | 24 | /// The unique `ID` of the ``Completion``. 25 | public let id: ID 26 | 27 | /// The creation `Date`. 28 | public let created: Date 29 | 30 | /// The `Model.ID` the completion was generated by. 31 | public let model: Model.ID 32 | 33 | /// The list of ``Choice`` options generated. 34 | public let choices: [Choice] 35 | 36 | /// The token ``Usage`` stats for the generation. 37 | public let usage: Usage 38 | 39 | /// Constructs a ``Completion`` result. 40 | /// 41 | /// - Parameters: 42 | /// - id: The unique `ID` 43 | /// - created: The creation `Date`. 44 | /// - model: The `Model.ID` the completion was generated by. 45 | /// - choices: The list of ``Choice`` options generated. 46 | /// - usage: The token ``Usage`` stats for the generation. 47 | public init( 48 | id: ID, 49 | created: Date, 50 | model: Model.ID, 51 | choices: [Choice], 52 | usage: Usage 53 | ) { 54 | self.id = id 55 | self.created = created 56 | self.model = model 57 | self.choices = choices 58 | self.usage = usage 59 | } 60 | 61 | /// A convenience accessor for the `text` value for the first ``Choice``. 62 | public var text: String { 63 | choices[0].text 64 | } 65 | 66 | /// A convenience adccessor for the ``Logprobs`` for the first ``Choice``. 67 | public var logprobs: Logprobs? { 68 | choices[0].logprobs 69 | } 70 | 71 | /// A convenience accessor for the ``FinishReason`` for the first ``Choice``. 72 | public var finishReason: FinishReason { 73 | choices[0].finishReason 74 | } 75 | } 76 | 77 | // MARK: Completion.Choice 78 | 79 | extension Completion { 80 | /// One of the ``Completion`` choices. 81 | public struct Choice: Equatable, Codable { 82 | /// The text of the completion. 83 | public let text: String 84 | 85 | /// Which completion number (`0`-based). 86 | public let index: Int 87 | 88 | /// The list of `logprobs`, if present. 89 | public let logprobs: Logprobs? 90 | 91 | /// The reason for finishing. 92 | public let finishReason: FinishReason 93 | 94 | /// Creates a ``Completion/Choice``. 95 | /// 96 | /// - Parameters: 97 | /// - text: The text of the completion. 98 | /// - index: Which completion number (`0`-based). 99 | /// - logprobs: The list of `logprobs`, if present. 100 | /// - finishReason: The reason for finishing. 101 | public init( 102 | text: String, 103 | index: Int, 104 | logprobs: Logprobs? = nil, 105 | finishReason: FinishReason 106 | ) { 107 | self.text = text 108 | self.index = index 109 | self.logprobs = logprobs 110 | self.finishReason = finishReason 111 | } 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /.swiftpm/xcode/xcshareddata/xcschemes/swift-openai-gpt3.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 29 | 35 | 36 | 37 | 38 | 39 | 44 | 45 | 47 | 53 | 54 | 55 | 56 | 57 | 67 | 68 | 74 | 75 | 81 | 82 | 83 | 84 | 86 | 87 | 90 | 91 | 92 | -------------------------------------------------------------------------------- /Sources/OpenAIBits/Calls/Models.swift: -------------------------------------------------------------------------------- 1 | /// List and describe the various ``Model``s available in the API. You can refer to the [Models documentation](https://platform.openai.com/docs/models) to understand what models are available and the differences between them. 2 | /// 3 | /// ## Calls 4 | /// 5 | /// - ``Models/List`` - Lists all available ``Model``s. 6 | /// - ``Models/Detail`` - Retrieves details for a single ``Model``. 7 | /// - ``Models/Delete`` - Deletes a ``Model`` you own. 8 | /// 9 | /// ## See Also 10 | /// 11 | /// - [OpenAI API](https://platform.openai.com/docs/api-reference/models) 12 | /// - [Models documentation](https://platform.openai.com/docs/models) 13 | public enum Models {} 14 | 15 | // MARK: List 16 | 17 | extension Models { 18 | /// A ``Call`` that lists the currently available ``Model``s, and provides basic information about each one such as the owner and availability. 19 | /// 20 | /// ## See Also 21 | /// 22 | /// - [OpenAI API](https://platform.openai.com/docs/api-reference/models/list) 23 | public struct List: GetCall { 24 | /// Response with a ``ListOf`` ``Model``s. 25 | public typealias Response = ListOf 26 | 27 | var path: String { "models" } 28 | 29 | /// Constructs the ``Models/List`` call. 30 | public init() {} 31 | } 32 | } 33 | 34 | // MARK: Detail 35 | 36 | extension Models { 37 | /// A ``Call`` that retrieves a ``Model`` instance, providing basic information about the ``Model`` such as the owner and permissioning. 38 | /// 39 | /// ## See Also 40 | /// 41 | /// - [OpenAI API](https://platform.openai.com/docs/api-reference/models/retrieve) 42 | public struct Detail: GetCall { 43 | /// Responds with a ``Model``. 44 | public typealias Response = Model 45 | 46 | var path: String { "models/\(id.value)"} 47 | 48 | /// The ``Model/ID-swift.struct``. 49 | public let id: Model.ID 50 | 51 | /// Constructs ``Models`` ``Models/Detail`` with the specified ``Model/ID-swift.struct``. 52 | /// 53 | /// - Parameter id: The ``Model`` `ID`. 54 | public init(id: Model.ID) { 55 | self.id = id 56 | } 57 | } 58 | } 59 | 60 | // MARK: Delete 61 | 62 | extension Models { 63 | /// A ``Call`` to delete a ``Model``. You must have the `Owner` role in your organization, usually ``FineTune`` models. 64 | /// 65 | /// ## See Also 66 | /// 67 | /// - [OpenAI API](https://platform.openai.com/docs/api-reference/fine-tunes/delete-model) 68 | public struct Delete: DeleteCall { 69 | /// The HTTP call path. 70 | var path: String { "models/\(id)" } 71 | 72 | /// The ``FineTunes/Delete`` `Response`. 73 | public struct Response: JSONResponse { 74 | /// The ``Model/ID-swift.struct`` for the fine-tuned ``Model`` that was deleted. 75 | public let id: Model.ID 76 | 77 | /// Indicates if it was deleted. 78 | public let deleted: Bool 79 | 80 | /// The ``FineTunes/Delete`` `Response`. 81 | /// 82 | /// - Parameter id: The ``Model/ID-swift.struct`` for the fine-tuned ``Model`` that was deleted. 83 | /// - Parameter deleted: Indicates if it was deleted. 84 | public init(id: Model.ID, deleted: Bool) { 85 | self.id = id 86 | self.deleted = deleted 87 | } 88 | } 89 | 90 | /// The ``Model/ID-swift.struct`` to delete. 91 | public let id: Model.ID 92 | 93 | 94 | /// Delete a fine-tuned model. You must have the `Owner` role in your organization. 95 | /// 96 | /// - Parameter id: The ``Model/ID-swift.struct`` to delete. 97 | public init(id: Model.ID) { 98 | self.id = id 99 | } 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /Sources/OpenAIBits/Calls/Text+Edits.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | // MARK: Edits 4 | 5 | extension Text { 6 | /// A ``Call`` that creates a new ``Edit`` for the provided ``input``, ``instruction``, and other parameters. 7 | /// 8 | /// ## Examples 9 | /// 10 | /// ### A simple edit 11 | /// 12 | /// ```swift 13 | /// let client = OpenAI(apiKey: "...") 14 | /// let edit = try await client.call(Text.Edits( 15 | /// model: .davinci, 16 | /// input: "The quick brown fox jumps over the lazy dog." 17 | /// instruction: "Change the dog to a cat." 18 | /// )) 19 | /// ``` 20 | /// 21 | /// ## See Also 22 | /// 23 | /// - [OpenAI API](https://platform.openai.com/docs/api-reference/edits/create) 24 | /// - [Editing code guide](https://platform.openai.com/docs/guides/code/editing-code) 25 | public struct Edits: JSONPostCall { 26 | /// Response with an ``Edit``. 27 | public typealias Response = Edit 28 | 29 | /// The path to the call. 30 | var path: String { "edits" } 31 | 32 | /// `ID` of the ``Model`` to use. You can use ``Models/List`` to see all of your available models, or see our [Model overview](https://platform.openai.com/docs/models/overview) for descriptions of them. 33 | public let model: Model.ID 34 | 35 | /// The input text to use as a starting point for the edit. 36 | public let input: String? 37 | 38 | /// The instruction that tells the model how to edit the prompt. 39 | public let instruction: String 40 | 41 | /// The number of ``Edit/Choice`` values to return. (defaults to `1`) 42 | public let n: Int? 43 | 44 | /// What sampling temperature to use. Higher values means tzhe model will take more risks. Try `0.9` for more creative applications, and `0` (argmax sampling) for ones with a well-defined answer. 45 | /// 46 | /// We generally recommend altering this or ``topP`` but not both. 47 | public let temperature: Percentage? 48 | 49 | /// 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. 50 | /// 51 | /// We generally recommend altering this or ``temperature`` but not both. 52 | public let topP: Percentage? 53 | 54 | /// Creates a new ``Edit`` creation call. 55 | /// 56 | /// - Parameters: 57 | /// - model: ID of the model to use. You can use ``Models/List`` to see all of your available models, or see our [Model overview](https://platform.openai.com/docs/models/overview) for descriptions of them. 58 | /// - input: The input text to use as a starting point for the edit. 59 | /// - instruction: The instruction that tells the model how to edit the prompt. 60 | /// - n: The number of ``Edit/Choice`` values to return. (defaults to `1`) 61 | /// - temperature: What sampling temperature to use. Higher values means tzhe model will take more risks. Try `0.9` for more creative applications, and `0` (argmax sampling) for ones with a well-defined answer. 62 | /// - topP: 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. 63 | /// 64 | /// - Note: We generally recommend altering `temperature` or `topP` but not both. 65 | public init( 66 | model: Model.ID, 67 | input: String? = nil, 68 | instruction: String, 69 | n: Int? = nil, 70 | temperature: Percentage? = nil, 71 | topP: Percentage? = nil 72 | ) { 73 | self.model = model 74 | self.input = input 75 | self.instruction = instruction 76 | self.n = n 77 | self.temperature = temperature 78 | self.topP = topP 79 | } 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /Sources/OpenAIBits/Types/Transcription.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /// A ``Audio/Transcription`` or ``Audio/Translation`` `Call` response. 4 | public enum Transcription: HTTPResponse, Equatable { 5 | 6 | case json(JSONTranscription) 7 | case srt(SRTTranscription) 8 | case text(TextTranscription) 9 | case verboseJson(VerboseJSONTranscription) 10 | case vtt(VTTTranscription) 11 | 12 | init(data: Data, contentType: String) throws { 13 | if isJSON(contentType: contentType) { 14 | // try decode as VerboseJSONTranscription first, then JSONTranscription 15 | do { 16 | let VerboseJSONTranscription = try JSONDecoder().decode( 17 | VerboseJSONTranscription.self, from: data) 18 | self = .verboseJson(VerboseJSONTranscription) 19 | return 20 | } catch { 21 | let jsonTranscription = try JSONDecoder().decode(JSONTranscription.self, from: data) 22 | self = .json(jsonTranscription) 23 | return 24 | } 25 | } 26 | 27 | if isText(contentType: contentType) { 28 | let body = String(data: data, encoding: .utf8) ?? "" 29 | 30 | // Try VTT, SRT, then Text 31 | if body.hasPrefix("WEBVTT\n") { 32 | self = .vtt(VTTTranscription(value: body)) 33 | return 34 | } else if body.hasPrefix("1\n") { 35 | self = .srt(SRTTranscription(value: body)) 36 | return 37 | } else { 38 | self = .text(TextTranscription(value: body)) 39 | return 40 | } 41 | } 42 | 43 | throw OpenAI.Error.unexpectedResponse("Unexpected Content-Type: \(contentType)") 44 | } 45 | 46 | init(data: Data, response: HTTPURLResponse) throws { 47 | guard response.statusCode == 200 else { 48 | throw OpenAI.Error.unexpectedResponse("Unexpected status code: \(response.statusCode)") 49 | } 50 | 51 | guard let contentType = response.value(forHTTPHeaderField: CONTENT_TYPE) else { 52 | throw OpenAI.Error.unexpectedResponse("No Content-Type header in response") 53 | } 54 | 55 | self = try Self.init(data: data, contentType: contentType) 56 | } 57 | } 58 | 59 | extension Transcription { 60 | public func textValue() throws -> String { 61 | switch self { 62 | case .json(let transcription): 63 | return try jsonEncode(transcription, options: [.prettyPrinted, .sortedKeys]) 64 | case .srt(let transcription): 65 | return transcription.value 66 | case .text(let transcription): 67 | return transcription.value 68 | case .verboseJson(let transcription): 69 | return try jsonEncode(transcription, options: [.prettyPrinted, .sortedKeys]) 70 | case .vtt(let transcription): 71 | return transcription.value 72 | } 73 | } 74 | } 75 | 76 | // MARK: JSON Transcription 77 | 78 | public struct JSONTranscription: Codable, Equatable { 79 | public let text: String 80 | } 81 | 82 | // MARK: Verbose JSON Transcription 83 | 84 | public struct VerboseJSONTranscription: Codable, Equatable { 85 | public typealias Seconds = Double 86 | 87 | public let task: String 88 | public let language: String 89 | public let duration: Seconds 90 | public let segments: [Segment] 91 | public let text: String 92 | } 93 | 94 | extension VerboseJSONTranscription { 95 | public struct Segment: Codable, Equatable { 96 | public let id: Int 97 | public let seek: Int 98 | public let start: Seconds 99 | public let end: Seconds 100 | public let text: String 101 | public let tokens: [Token] 102 | public let temperature: Double? 103 | public let avgLogprob: Double? 104 | public let compressionRatio: Double? 105 | public let noSpeechProb: Double? 106 | public let transient: Bool? 107 | } 108 | } 109 | 110 | // MARK: Text Transcription 111 | 112 | public struct TextTranscription: Codable, Equatable { 113 | public let value: String 114 | } 115 | 116 | // MARK: SRT Transcription 117 | 118 | public struct SRTTranscription: Codable, Equatable { 119 | public let value: String 120 | } 121 | 122 | // MARK: VTT Transcription 123 | 124 | public struct VTTTranscription: Codable, Equatable { 125 | public let value: String 126 | } 127 | -------------------------------------------------------------------------------- /Tests/OpenAIBitsTests/TokenEncoderTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | import CustomDump 3 | @testable import OpenAIBits 4 | 5 | final class TokenEncoderTests: XCTestCase { 6 | 7 | override func setUpWithError() throws { 8 | // Put setup code here. This method is called before the invocation of each test method in the class. 9 | } 10 | 11 | override func tearDownWithError() throws { 12 | // Put teardown code here. This method is called after the invocation of each test method in the class. 13 | } 14 | 15 | func testGPT3SimpleText() throws { 16 | let text = "This is some text." 17 | let tokens: [Token] = [1212, 318, 617, 2420, 13] 18 | 19 | let encoder = try TokenEncoder() 20 | 21 | XCTAssertNoDifference(try encoder.encode(text: text), tokens) 22 | XCTAssertNoDifference(try encoder.decode(tokens: tokens), text) 23 | } 24 | 25 | func testGPT3Lorem() throws { 26 | let input = "Lorem" 27 | let output: [Token] = [43, 29625] 28 | 29 | let encoder = try TokenEncoder() 30 | 31 | XCTAssertNoDifference(try encoder.encode(text: input), output) 32 | XCTAssertNoDifference(try encoder.decode(tokens: output), input) 33 | } 34 | 35 | func testLongerText() throws { 36 | let text = """ 37 | Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. 38 | """ 39 | let tokens: [Token] = [43, 29625, 220, 2419, 388, 288, 45621, 1650, 716, 316, 11, 369, 8831, 316, 333, 31659, 271, 2259, 1288, 270, 11, 10081, 466, 304, 3754, 4666, 10042, 753, 312, 312, 2797, 3384, 2248, 382, 2123, 288, 349, 382, 2153, 2616, 435, 1557, 64, 13, 7273, 551, 320, 512, 10356, 8710, 1789, 11, 627, 271, 18216, 81, 463, 4208, 3780, 334, 297, 321, 1073, 4827, 271, 299, 23267, 3384, 435, 1557, 541, 409, 304, 64, 13088, 78, 4937, 265, 13, 10343, 271, 257, 1133, 4173, 495, 288, 45621, 287, 1128, 260, 258, 681, 270, 287, 2322, 37623, 378, 11555, 270, 1658, 325, 269, 359, 388, 288, 349, 382, 304, 84, 31497, 5375, 9242, 64, 1582, 72, 2541, 13, 18181, 23365, 264, 600, 1609, 64, 721, 265, 6508, 312, 265, 265, 1729, 386, 738, 11, 264, 2797, 287, 10845, 8957, 45567, 1163, 544, 748, 263, 2797, 285, 692, 270, 2355, 4686, 1556, 4827, 388, 13] 40 | 41 | let encoder = try TokenEncoder() 42 | XCTAssertNoDifference(try encoder.encode(text: text), tokens) 43 | XCTAssertNoDifference(try encoder.decode(tokens: tokens), text) 44 | } 45 | 46 | func testFailingDecode() throws { 47 | let tokens: [Token] = [43, 150000] // 150000 is well above the token boundary for GPT-3 and will fail. 48 | 49 | let encoder = try TokenEncoder() 50 | XCTAssertThrowsError(try encoder.decode(tokens: tokens)) { err in 51 | XCTAssertEqual(err as! TokenEncoder.Error, TokenEncoder.Error.invalidToken(value: 150000)) 52 | } 53 | } 54 | 55 | func testEncodeMultibyteText() throws { 56 | let text = "🇺🇸" 57 | let tokens: [Token] = [8582, 229, 118, 8582, 229, 116] 58 | 59 | let encoder = try TokenEncoder() 60 | 61 | XCTAssertNoDifference(try encoder.encode(text: text), tokens) 62 | } 63 | 64 | func testDecodeMultibyteText() throws { 65 | let text = "🇺🇸" 66 | let tokens: [Token] = [8582, 229, 118, 8582, 229, 116] 67 | 68 | let encoder = try TokenEncoder() 69 | 70 | XCTAssertNoDifference(try encoder.decode(tokens: tokens), text) 71 | } 72 | 73 | func testByteEncoder() throws { 74 | XCTAssertEqual(256, TokenEncoder.byteEncoder.count) 75 | for index in 0...UInt8.max { 76 | XCTAssertNotNil(TokenEncoder.byteEncoder[index], "\(index)") 77 | } 78 | } 79 | 80 | func testByteDecoder() throws { 81 | XCTAssertEqual(256, TokenEncoder.byteDecoder.count) 82 | var valueSet = Set() 83 | for (_, value) in TokenEncoder.byteDecoder { 84 | valueSet.insert(value) 85 | } 86 | XCTAssertEqual(256, valueSet.count) 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /.swiftpm/xcode/xcshareddata/xcschemes/openai.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 33 | 39 | 40 | 41 | 43 | 49 | 50 | 51 | 52 | 53 | 63 | 65 | 71 | 72 | 73 | 74 | 77 | 78 | 79 | 80 | 84 | 85 | 86 | 87 | 93 | 95 | 101 | 102 | 103 | 104 | 106 | 107 | 110 | 111 | 112 | -------------------------------------------------------------------------------- /Sources/OpenAIBits/Documentation.docc/Tutorials/Creating Text Edits.tutorial: -------------------------------------------------------------------------------- 1 | @Tutorial(time: 5) { 2 | @Intro(title: "Creating Text Edits") { 3 | Learn how to use the ``OpenAIBits/Text/Edits`` call to edit a text prompt with a natural language instruction. 4 | } 5 | 6 | @Section(title: "Create a Grammar Check Function") { 7 | @ContentAndMedia { 8 | This will walk through creating a function that takes some input text and corrects the spelling and grammar. 9 | } 10 | 11 | @Steps { 12 | @Step { 13 | Create the ``OpenAIBits/OpenAI`` instance, using your personal Open AI Key. 14 | 15 | > Important: It is not recommended to hard-code an API Key into your source code. Instead, store it in the Keychain, a plist, or in an environment variable if working in on a command line app. 16 | 17 | @Code(name: "FixGrammar.swift", file: "create_openai.swift") 18 | } 19 | 20 | @Step { 21 | Next, we create the `fixGrammar(in:)` function, which takes a single `String` as the input and outputs a `String` with the result. 22 | 23 | @Code(name: "FixGrammar.swift", file: "FixGrammar_01.swift") 24 | } 25 | 26 | @Step { 27 | We call the `openai` client with an ``OpenAIBits/Text/Edits`` call. 28 | 29 | @Code(name: "FixGrammar.swift", file: "FixGrammar_02.swift") 30 | } 31 | 32 | @Step { 33 | The `'id'` determines which model to use. 34 | 35 | > Note: It must be one of the ``OpenAIBits/Model``s that return `true` from ``OpenAIBits/Model/supportsEdit`` (for example, ``OpenAIBits/Model/ID-swift.struct/text_davinci_edit_001`` or ``OpenAIBits/Model/ID-swift.struct/code_davinci_edit_001``). 36 | 37 | @Code(name: "FixGrammar.swift", file: "FixGrammar_02_id.swift", previousFile: "FixGrammar_02_id_prev.swift") 38 | } 39 | 40 | @Step { 41 | The `'input'` is the text that will be edited. 42 | 43 | @Code(name: "FixGrammar.swift", file: "FixGrammar_02_input.swift", previousFile: "FixGrammar_02_input_prev.swift") 44 | } 45 | 46 | @Step { 47 | The `'instruction'` is a description of what you want to change. 48 | 49 | @Code(name: "FixGrammar.swift", file: "FixGrammar_02_instruction.swift", previousFile: "FixGrammar_02_instruction_prev.swift") 50 | } 51 | 52 | @Step { 53 | Because we are now calling a function with `try await`, we need to handle that. In this case, we will simply add `async throws` to our function to pass it up the chain. 54 | 55 | @Code(name: "FixGrammar.swift", file: "FixGrammar_03.swift") 56 | } 57 | 58 | @Step { 59 | Lastly, return the result's `text`. 60 | 61 | > Note: If multiple choices are returned, they can be accessed via the ``OpenAIBits/Edit/choices`` array. 62 | 63 | @Code(name: "FixGrammar.swift", file: "FixGrammar_04.swift") 64 | } 65 | 66 | @Step { 67 | Now, we can call the function. 68 | 69 | > Note: Swift does not allow calling of `async` functions at the global level. This example is code you could run as a command line application. 70 | 71 | @Code(name: "FixGrammar.swift", file: "FixGrammar_05.swift") 72 | } 73 | 74 | @Step { 75 | It might return something like this: 76 | 77 | @Code(name: "Output", file: "FixGrammar_Output_01.txt") 78 | } 79 | 80 | @Step { 81 | The issue here is that by default, this will return a more "creative" (or random) result. To fix that, we can change the `'temperature'` to `0.0` instead of the default of `1.0`. 82 | 83 | @Code(name: "FixGrammar.swift", file: "FixGrammar_06.swift", previousFile: "FixGrammar_05.swift") 84 | } 85 | 86 | @Step { 87 | This time, we should get this: 88 | 89 | @Code(name: "Output", file: "FixGrammar_Output_02.txt") 90 | } 91 | } 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /Sources/OpenAIBits/Calls/Embeddings.swift: -------------------------------------------------------------------------------- 1 | /// Create a vector representation of a given input that can be easily consumed by machine learning models and algorithms. 2 | /// 3 | /// ## Calls 4 | /// 5 | /// - ``Embeddings/Create`` - Creates a new embedding vector for a given text input. 6 | /// 7 | /// ## See Also 8 | /// - [OpenAI API](https://platform.openai.com/docs/api-reference/embeddings) 9 | /// - [Embeddings Guide](https://platform.openai.com/docs/guides/embeddings) 10 | public enum Embeddings {} 11 | 12 | // MARK: Create 13 | 14 | extension Embeddings { 15 | 16 | /// A ``Call`` that creates an embedding vector representing the input text. 17 | /// 18 | /// ## See Also 19 | /// 20 | /// - [OpenAI API](https://platform.openai.com/docs/api-reference/embeddings/create) 21 | /// - [Embeddings Guide](https://platform.openai.com/docs/guides/embeddings) 22 | public struct Create: JSONPostCall, Equatable { 23 | 24 | /// The input to an ``Embeddings/Create`` ``Call`` is either a `.string(...)` or 25 | /// an array of `.tokens(...)`. 26 | /// 27 | /// ## Notes 28 | /// 29 | /// - The `.strings` case can be created directly with a `"String Value"`, and\ 30 | /// the `.tokens` case can be created directly with an `[123, 456]` array of ``Token``s. 31 | public enum Input: Equatable { 32 | /// The input as a `String`. 33 | case string(String) 34 | 35 | /// The input as an array of ``Token``s. 36 | case tokens([Token]) 37 | } 38 | 39 | /// The path to the API endpoint. 40 | var path: String { "embeddings" } 41 | 42 | /// Responds with a ``ListOf`` ``Embedding`` values. 43 | public typealias Response = ListOf 44 | 45 | /// ID of the model to use. You can use the ``Models/List`` ``Call`` to see all of 46 | /// your available models, or see the [Model overview](https://platform.openai.com/docs/models/overview) 47 | /// for descriptions of them. 48 | public let model: Model.ID 49 | 50 | /// Input text to get embeddings for, encoded as a string or array of tokens. To get embeddings for multiple inputs in a single request, pass an array of strings or array of token arrays. Each input must not exceed 2048 tokens in length. 51 | /// 52 | /// Unless you are embedding code, we suggest replacing newlines (`"\n"`) in your input with a single space, as we have observed inferior results when newlines are present. 53 | public let input: Input 54 | 55 | /// A unique identifier representing your end-user, which will help OpenAI to monitor and detect abuse. 56 | public let user: String? 57 | 58 | /// Constructs a `Create` call. 59 | /// 60 | /// - Parameters: 61 | /// - model: ID of the model to use. 62 | /// - input: Input text to get embeddings for, encoded as a string or array of tokens. 63 | /// - user: A unique identifier representing your end-user, which will help OpenAI to monitor and detect abuse. 64 | public init(model: Model.ID, input: Input, user: String? = nil) { 65 | self.model = model 66 | self.input = input 67 | self.user = user 68 | } 69 | } 70 | } 71 | 72 | // MARK: Expressible 73 | 74 | extension Embeddings.Create.Input: ExpressibleByStringLiteral, ExpressibleByStringInterpolation { 75 | /// Initializes the `Input` with a `String` literal value. 76 | /// 77 | /// - Parameter stringLiteral: The `String`. 78 | public init(stringLiteral value: String) { 79 | self = .string(value) 80 | } 81 | } 82 | 83 | extension Embeddings.Create.Input: ExpressibleByArrayLiteral { 84 | /// Initializes the `Input` with an array of ``Token``s. 85 | /// 86 | /// - Parameter arrayLiteral: The array of ``Token``s. 87 | public init(arrayLiteral elements: Token...) { 88 | self = .tokens(elements) 89 | } 90 | } 91 | 92 | // MARK: Codable 93 | 94 | extension Embeddings.Create.Input: Codable { 95 | public init(from decoder: Decoder) throws { 96 | let container = try decoder.singleValueContainer() 97 | do { 98 | self = .string(try container.decode(String.self)) 99 | return 100 | } catch {} 101 | 102 | self = .tokens(try container.decode([Token].self)) 103 | } 104 | 105 | public func encode(to encoder: Encoder) throws { 106 | var container = encoder.singleValueContainer() 107 | switch self { 108 | case .string(let value): 109 | try container.encode(value) 110 | case .tokens(let tokens): 111 | try container.encode(tokens) 112 | } 113 | } 114 | } 115 | 116 | -------------------------------------------------------------------------------- /Sources/OpenAIBits/CallTypes/JSONUtils.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /// An implementation of CodingKey that's useful for combining and transforming keys as strings. 4 | struct AnyKey: CodingKey { 5 | var stringValue: String 6 | var intValue: Int? 7 | 8 | init?(stringValue: String) { 9 | self.stringValue = stringValue 10 | self.intValue = nil 11 | } 12 | 13 | init?(intValue: Int) { 14 | self.stringValue = String(intValue) 15 | self.intValue = intValue 16 | } 17 | } 18 | 19 | // MARK: jsonEncode 20 | 21 | /// Encodes the provided value to a JSON string 22 | /// 23 | /// - Parameter value: The value to encode 24 | /// - Parameter options: The output formatting options. Defaults to none. 25 | /// - Returns The encoded value. 26 | func jsonEncode(_ value: T, options: JSONEncoder.OutputFormatting = []) throws -> String { 27 | return try String(decoding: jsonEncodeData(value, options: options), as: UTF8.self) 28 | } 29 | 30 | // MARK: jsonEncodeData 31 | 32 | /// Encodes the provided value to a JSON ``Data`` value 33 | /// 34 | /// - Parameter value: The value to encode 35 | /// - Returns the encoded value. 36 | func jsonEncodeData(_ value: T, options: JSONEncoder.OutputFormatting = []) throws -> Data { 37 | let encoder = JSONEncoder() 38 | encoder.outputFormatting = options 39 | encoder.keyEncodingStrategy = .convertToSnakeCase 40 | encoder.dateEncodingStrategy = .custom({ date, encoder in 41 | let seconds = Int64(date.timeIntervalSince1970) 42 | var singleValueEnc = encoder.singleValueContainer() 43 | try singleValueEnc.encode(seconds) 44 | }) 45 | encoder.dataEncodingStrategy = .base64 46 | return try encoder.encode(value) 47 | } 48 | 49 | // MARK: jsonDecode 50 | 51 | /// Attempts to decode the provided `String` value into the target type `T`. 52 | /// 53 | /// - Parameters: 54 | /// - value: The value to decode 55 | /// - targetType: The type to decode to (optional) 56 | func jsonDecode(_ value: String, as targetType: T.Type = T.self) throws -> T { 57 | return try jsonDecodeData(value.data(using: .utf8)!) 58 | } 59 | 60 | // MARK: jsonDecodeData 61 | 62 | /// Attempts to decode the provided `Data` value into the target type `T`. 63 | /// 64 | /// - Parameters: 65 | /// - value: The value to decode. 66 | /// - targetType: The type the decoded value must escape to. 67 | /// - Throws: An error if there is a decoding issue. 68 | /// - Returns: The decoded value. 69 | func jsonDecodeData(_ value: Data, as targetType: T.Type = T.self) throws -> T { 70 | let decoder = JSONDecoder() 71 | decoder.keyDecodingStrategy = .convertFromSnakeCase 72 | decoder.dateDecodingStrategy = .custom({ decoder in 73 | let seconds: Int64 = try decoder.singleValueContainer().decode(Int64.self) 74 | return Date(timeIntervalSince1970: TimeInterval(seconds)) 75 | }) 76 | decoder.dataDecodingStrategy = .base64 77 | return try decoder.decode(targetType, from: value) 78 | } 79 | 80 | let CONTENT_TYPE = "Content-Type" 81 | let APPLICATION_JSON = "application/json" 82 | let TEXT_PLAIN = "text/plain" 83 | 84 | // MARK: isJSON 85 | 86 | /// Tests if the provided `contentType` is JSON. 87 | /// 88 | /// - Parameter contentType: The value to test. 89 | /// - Returns `true` if it matches. 90 | func isJSON(contentType: String) -> Bool { 91 | contentType.starts(with: APPLICATION_JSON) 92 | } 93 | 94 | /// Checks if the `"Content-Type"` header in the provided ``HTTPURLResponse`` is `JSON`. 95 | /// 96 | /// - Parameter response: The ``HTTPURLResponse``. 97 | /// - Returns `true` if the `"Content-Type"` header is JSON. 98 | func isJSON(response: HTTPURLResponse) -> Bool { 99 | guard let contentType = response.value(forHTTPHeaderField: CONTENT_TYPE) else { return false } 100 | return isJSON(contentType: contentType) 101 | } 102 | 103 | // MARK: isText 104 | 105 | /// Tests if the provided `contentType` is text. 106 | /// 107 | /// - Parameter contentType: The value to test. 108 | /// - Returns `true` if it matches. 109 | func isText(contentType: String) -> Bool { 110 | contentType.starts(with: TEXT_PLAIN) 111 | } 112 | 113 | /// Checks if the `"Content-Type"` header in the provided ``HTTPURLResponse`` is text. 114 | /// 115 | /// - Parameter response: The ``HTTPURLResponse``. 116 | /// - Returns `true` if the `"Content-Type"` header is text. 117 | func isText(response: HTTPURLResponse) -> Bool { 118 | guard let contentType = response.value(forHTTPHeaderField: CONTENT_TYPE) else { return false } 119 | return isText(contentType: contentType) 120 | } -------------------------------------------------------------------------------- /.swiftpm/xcode/xcshareddata/xcschemes/swift-openai-bits-Package.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 29 | 35 | 36 | 37 | 43 | 49 | 50 | 51 | 57 | 63 | 64 | 65 | 66 | 67 | 73 | 74 | 76 | 82 | 83 | 84 | 85 | 86 | 96 | 97 | 103 | 104 | 110 | 111 | 112 | 113 | 115 | 116 | 119 | 120 | 121 | -------------------------------------------------------------------------------- /Sources/OpenAIBits/Utils/Pattern.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /** 4 | A wrapper for simplified RegEx matching. It does not throw an exception when given a bad pattern, rather it triggers a `preconditionFailure` with the error message. This makes it useful for providing reusable constants which 5 | generally should not fail in the real world. 6 | */ 7 | struct Pattern: CustomStringConvertible { 8 | typealias Options = NSRegularExpression.Options 9 | typealias MatchingOptions = NSRegularExpression.MatchingOptions 10 | 11 | /// The underlying `NSRegularExpression` instance. 12 | let regex: NSRegularExpression 13 | 14 | /// Description of the pattern. 15 | var description: String { 16 | regex.pattern 17 | } 18 | 19 | /// Creates a pattern from a string. 20 | /// 21 | /// - Parameters: 22 | /// - pattern: The pattern to match. 23 | /// - options: The options to use when matching. 24 | init(_ pattern: String, options: Options = []) { 25 | do { 26 | try regex = NSRegularExpression(pattern: pattern, options: options) 27 | } catch { 28 | preconditionFailure("Invalid regular expression '\(pattern)': \(error)") 29 | } 30 | } 31 | 32 | /// Returns whether the pattern matches the given string. 33 | func hasMatch(in text: String, options: MatchingOptions = []) -> Bool { 34 | let range = NSRange(text.startIndex.. 0 36 | } 37 | 38 | /// Returns whether the pattern matches the given ``CustomStringConvertible`` value. 39 | /// 40 | /// - Parameters: 41 | /// - value: The value to match. 42 | /// - options: The options to use when matching. 43 | /// 44 | /// - Returns: Whether the pattern matches the given value. 45 | func hasMatch(in text: CustomStringConvertible, options: MatchingOptions = []) -> Bool { 46 | return hasMatch(in: String(describing: text), options: options) 47 | } 48 | 49 | /// Returns the first match of the pattern in the given string. 50 | /// 51 | /// - Parameters: 52 | /// - value: The value to match. 53 | /// - options: The options to use when matching. 54 | /// 55 | /// - Returns: The first match of the pattern in the given string. 56 | func matchGroups(in text: String, options: MatchingOptions = []) -> Result? { 57 | let range = NSRange(text.startIndex.. Result? { 66 | return matchGroups(in: String(describing: text), options: options) 67 | } 68 | 69 | func findAll(in text: String, options: MatchingOptions = []) -> [Result] { 70 | let results = regex.matches(in: text, options: options, range: NSRange(text.startIndex..., in: text)) 71 | return results.compactMap { 72 | Result(textCheckingResult: $0, original: text) 73 | // Range($0.range, in: value).map { value[$0] } 74 | } 75 | } 76 | 77 | func replace(in text: String, with replacement: String, options: MatchingOptions = []) -> String { 78 | let range = NSRange(text.startIndex.. Substring? { 95 | guard i < textCheckingResult.numberOfRanges else { 96 | return nil 97 | } 98 | 99 | if let group = Range(textCheckingResult.range(at: i), in: original) { 100 | return original[group] 101 | } 102 | return nil 103 | } 104 | 105 | subscript(i: Int, j: Int) -> (Substring, Substring)? { 106 | if let iValue = self[i], 107 | let jValue = self[j] 108 | { 109 | return (iValue, jValue) 110 | } else { 111 | return nil 112 | } 113 | } 114 | 115 | subscript(i: Int, j: Int, k: Int) -> (Substring, Substring, Substring)? { 116 | if let iValue = self[i], 117 | let jValue = self[j], 118 | let kValue = self[k] 119 | { 120 | return (iValue, jValue, kValue) 121 | } else { 122 | return nil 123 | } 124 | } 125 | 126 | subscript(name: String) -> Substring? { 127 | let nsrange = textCheckingResult.range(withName: name) 128 | guard nsrange.location != NSNotFound else { 129 | return nil 130 | } 131 | 132 | if let range = Range(nsrange, in: original) { 133 | return original[range] 134 | } else { 135 | return nil 136 | } 137 | } 138 | 139 | subscript(name1: String, name2: String) -> (Substring, Substring)? { 140 | if let value1 = self[name1], 141 | let value2 = self[name2] 142 | { 143 | return (value1, value2) 144 | } 145 | return nil 146 | } 147 | 148 | subscript(name1: String, name2: String, name3: String) -> (Substring, Substring, Substring)? { 149 | if let value1 = self[name1], 150 | let value2 = self[name2], 151 | let value3 = self[name3] 152 | { 153 | return (value1, value2, value3) 154 | } 155 | return nil 156 | } 157 | 158 | } 159 | } 160 | 161 | extension Pattern: ExpressibleByStringLiteral { 162 | typealias StringLiteralType = String 163 | 164 | init(stringLiteral value: String) { 165 | self.init(value) 166 | } 167 | } 168 | -------------------------------------------------------------------------------- /Sources/OpenAIBits/OpenAI.swift: -------------------------------------------------------------------------------- 1 | /// Represents the connection to the OpenAI API. 2 | /// You must provide at least the `apiKey`, and optionally an `organisation` key and a `log` function. 3 | /// 4 | /// ## Examples 5 | /// 6 | /// ### Print a list of available models 7 | /// 8 | /// ```swift 9 | /// // Get API_KEY from the environment 10 | /// let apiKey = ProcessInfo.processInfo.environment["OPENAI_API_KEY"]! 11 | /// let client = OpenAI(apiKey: apiKey) 12 | /// let models = try await client.call(Models.List()) 13 | /// 14 | /// for model in models { 15 | /// print("- \(model.name)") 16 | /// } 17 | /// ``` 18 | /// 19 | /// ### Authenticate with an organisation key 20 | /// 21 | /// ```swift 22 | /// // Get API_KEY and ORG_KEY from the environment 23 | /// let apiKey = ProcessInfo.processInfo.environment["OPENAI_API_KEY"]! 24 | /// let orgKey = ProcessInfo.processInfo.environment["OPENAI_ORG_KEY"]! 25 | /// let client = OpenAI(apiKey: apiKey, organization: orgKey) 26 | /// ``` 27 | /// 28 | /// ### Log all requests and responses 29 | /// 30 | /// Log all requests and responses to the console, with a date/timestamp: 31 | /// 32 | /// ```swift 33 | /// let client = OpenAI( 34 | /// apiKey: apiKey, 35 | /// log: { message in print("\(Date()): \(message)") } 36 | /// ) 37 | /// ``` 38 | public struct OpenAI { 39 | /// Typealias for a logger function, which takes a `String` and outputs it. 40 | public typealias Logger = (String) -> Void 41 | 42 | /// The OpenAI API Key to use. 43 | let apiKey: String 44 | 45 | /// The OpenAI Organization Key to use (optional). 46 | let organization: String? 47 | 48 | /// A ``Logger`` function, if desired. Used for debug logging if present. Defaults to `nil`. 49 | let log: Logger? 50 | 51 | /// Initializes the ``OpenAI`` client. 52 | /// 53 | /// - Parameters: 54 | /// - apiKey: The OpenAI API Key to use. 55 | /// - organization: The OpenAI Organization Key to use (optional). 56 | /// - log: A ``Logger`` function, if desired. Used for debug logging if present. Defaults to `nil`. 57 | public init(apiKey: String, organization: String? = nil, log: Logger? = nil) { 58 | self.apiKey = apiKey 59 | self.organization = organization 60 | self.log = log 61 | } 62 | } 63 | 64 | extension OpenAI { 65 | /// An error returned by the OpenAI API. 66 | public struct Error: Swift.Error, Codable, Equatable { 67 | /// A message describing the error. 68 | public let message: String 69 | 70 | /// The type of error. 71 | public let type: String 72 | 73 | /// The parameter that caused the error. 74 | public let param: String? 75 | 76 | /// The error code, if any. 77 | public let code: String? 78 | 79 | /// Initializes a new ``OpenAI`` client. 80 | /// - Parameters: 81 | /// - type: The type of the error. 82 | /// - code: The code (optional) 83 | /// - param: The parameter name. 84 | /// - message: The message. 85 | public init(type: String, code: String? = nil, param: String? = nil, message: String) { 86 | self.type = type 87 | self.code = code 88 | self.param = param 89 | self.message = message 90 | } 91 | } 92 | } 93 | 94 | /// The default implementation for ``CallHandler``. 95 | struct ExecutableCallHandler: CallHandler { 96 | /// Executes the call if it implements ``ExecutableCall``. 97 | /// - Parameters: 98 | /// - call: The ``Call``. 99 | /// - client: The ``OpenAI`` instance. 100 | /// - Returns: The ``Call/Response``. 101 | /// - Throws: An ``OpenAI/Error`` if the ``Call`` does not implement ``ExecutableCall``, or if executing the throws an error. 102 | func execute(call: C, with client: OpenAI) async throws -> C.Response where C : Call { 103 | guard let call = call as? any ExecutableCall else { 104 | throw OpenAI.Error.unsupportedCall(C.self) 105 | } 106 | let response = try await call.execute(with: client) 107 | guard let response = response as? C.Response else { 108 | throw OpenAI.Error.unexpectedResponse(String(describing: response)) 109 | } 110 | return response 111 | } 112 | } 113 | 114 | extension OpenAI { 115 | /// The current ``CallHandler``. Defaults to ``HTTPCallHandler``. 116 | static var handler: CallHandler = ExecutableCallHandler() 117 | 118 | /// Execute the specified ``Call``, returning the specified ``Call/Response``. 119 | /// 120 | /// - Parameter call: The ``Call`` to execute. 121 | public func call(_ call: C) async throws -> C.Response { 122 | return try await OpenAI.handler.execute(call: call, with: self) 123 | } 124 | } 125 | 126 | extension OpenAI.Error: CustomStringConvertible, CustomDebugStringConvertible { 127 | public var description: String { 128 | var result = message 129 | if let param = param { 130 | result.append("\nParameter: \(param)") 131 | } 132 | if let code = code { 133 | result.append("\nCode: \(code)") 134 | } 135 | return result 136 | } 137 | 138 | public var debugDescription: String { 139 | description 140 | } 141 | 142 | /// Creates an ``OpenAI/Error`` indicating the provided ``Call`` type is not supported. 143 | /// - Parameter request: The call. 144 | /// - Returns: The ``OpenAI/Error``. 145 | static func unsupportedCall(_ call: C.Type) -> OpenAI.Error { 146 | .init(type: "unsupported_call", message: String(describing: call)) 147 | } 148 | 149 | /// Creates an ``OpenAI/Error`` indicating a URL was invalid. This is usually a bug in the API. 150 | /// - Parameter url: The URL. 151 | /// - Returns: The ``OpenAI/Error``. 152 | static func invalidURL(_ url: String) -> OpenAI.Error { 153 | .init(type: "invalid_url", message: url) 154 | } 155 | 156 | /// Creates an ``OpenAI/Error`` indicating the response was unexpected. 157 | /// - Parameter message: The message. 158 | /// - Returns: The ``OpenAI/Error``. 159 | static func unexpectedResponse(_ message: String) -> OpenAI.Error { 160 | .init(type: "unexpected_response", message: message) 161 | } 162 | } 163 | -------------------------------------------------------------------------------- /Sources/OpenAIBits/Types/Model.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /// Describes a trained model. 4 | public struct Model: Identifiable, JSONResponse, Equatable { 5 | /// The `ID` for a ``Model``. 6 | public struct ID: Identifier { 7 | /// The actual value. 8 | public let value: String 9 | 10 | /// Creates a new ``Model`` `ID`. 11 | /// - Parameter value: The `ID` value. 12 | public init(_ value: String) { 13 | self.value = value 14 | } 15 | } 16 | 17 | /// The unique ``Model/ID-swift.struct``. 18 | public let id: Model.ID 19 | 20 | /// The creation date. 21 | public let created: Date 22 | 23 | /// The owner ID. 24 | public let ownedBy: String 25 | 26 | /// The list of ``Model/Permission-swift.struct``s for the ``Model``. 27 | public let permission: [Model.Permission] 28 | 29 | /// The root ``Model/ID-swift.struct``. 30 | public let root: Model.ID 31 | 32 | /// The parent ``Model/ID-swift.struct``. 33 | public let parent: Model.ID? 34 | 35 | /// Constructs a ``Model``. 36 | /// 37 | /// - Parameters: 38 | /// - id: The ``ID-swift.struct``. 39 | /// - created: The ``created`` date. 40 | /// - ownedBy: The ``ownedBy`` ID. 41 | /// - permission: The ``permission-swift.property`` list. 42 | /// - root: The ``root`` ID. 43 | /// - parent: The ``parent`` ID. 44 | public init( 45 | id: Model.ID, 46 | created: Date, 47 | ownedBy: String, 48 | permission: [Model.Permission], 49 | root: Model.ID, 50 | parent: Model.ID? = nil 51 | ) { 52 | self.id = id 53 | self.created = created 54 | self.ownedBy = ownedBy 55 | self.permission = permission 56 | self.root = root 57 | self.parent = parent 58 | } 59 | } 60 | 61 | extension Model { 62 | /// Describes ther permissions allowed for a model. 63 | public struct Permission: Identifiable, Equatable, Codable { 64 | /// The unique `ID` for the ``Model/Permission-swift.struct``. 65 | public struct ID: Identifier { 66 | public var value: String 67 | 68 | public init(_ value: String) { 69 | self.value = value 70 | } 71 | } 72 | 73 | /// The unique ``Model/Permission-swift.struct/ID-swift.struct``. 74 | public let id: Model.Permission.ID 75 | 76 | /// The creation date. 77 | public let created: Date 78 | 79 | /// Can the model be used for sampling? 80 | public let allowSampling: Bool 81 | 82 | /// Does it support outputting logprobs? 83 | public let allowLogprobs: Bool 84 | 85 | /// Does it allow search indices? 86 | public let allowSearchIndices: Bool 87 | 88 | /// Does it allow viewing? 89 | public let allowView: Bool 90 | 91 | /// Does it allow fine-tuning? 92 | public let allowFineTuning: Bool 93 | 94 | /// What organisations do the permissions apply to? 95 | public let organization: String 96 | 97 | /// What group owns the engine? 98 | public let group: String? 99 | 100 | /// Is it currently blocking? 101 | public let isBlocking: Bool 102 | 103 | /// Constructs permissions. 104 | /// 105 | /// - Parameters: 106 | /// - id: The ``Model/Permission-swift.struct/ID-swift.struct``. 107 | /// - created: The ``created`` date. 108 | /// - allowSampling: Does it ``allowSampling``? 109 | /// - allowLogprobs: Does it ``allowLogprobs``? 110 | /// - allowSearchIndices: Does it ``allowSearchIndices``? 111 | /// - allowView: Does it ``allowView``? 112 | /// - allowFineTuning: Does it ``allowFineTuning``? 113 | /// - organization: What organizations can use it? 114 | /// - group: What group owns it? 115 | /// - isBlocking: Check if it ``isBlocking``. 116 | public init( 117 | id: Model.Permission.ID, 118 | created: Date, 119 | allowSampling: Bool = false, 120 | allowLogprobs: Bool = false, 121 | allowSearchIndices: Bool = false, 122 | allowView: Bool = false, 123 | allowFineTuning: Bool = false, 124 | organization: String, 125 | group: String? = nil, 126 | isBlocking: Bool = false 127 | ) { 128 | self.id = id 129 | self.created = created 130 | self.allowSampling = allowSampling 131 | self.allowLogprobs = allowLogprobs 132 | self.allowSearchIndices = allowSearchIndices 133 | self.allowView = allowView 134 | self.allowFineTuning = allowFineTuning 135 | self.organization = organization 136 | self.group = group 137 | self.isBlocking = isBlocking 138 | } 139 | } 140 | 141 | /// Indicates if the model was trained for working with code. 142 | public var supportsCode: Bool { 143 | return id.supportsCode 144 | } 145 | 146 | /// Indicates if the model was trained to work with the `/v1/edits` feature. 147 | public var supportsEdit: Bool { 148 | return id.supportsEdit 149 | } 150 | 151 | /// Indicates if the model supports text similarty. 152 | public var supportsTextSimilarity: Bool { id.supportsTextSimilarity } 153 | 154 | /// Indicates if the model supports text search. 155 | public var supportsTextSearch: Bool { id.supportsTextSearch } 156 | 157 | /// Indicates if the model supports code search. 158 | public var supportsCodeSearch: Bool { id.supportsCodeSearch } 159 | 160 | public var supportsEmbeddings: Bool { id.supportsEmbeddings } 161 | 162 | public var isFineTune: Bool { 163 | return id.isFineTune 164 | } 165 | 166 | } 167 | 168 | // Common Models: 169 | extension Model.ID { 170 | /// Indicates if the model was trained for working with code. 171 | public var supportsCode: Bool { value.hasPrefix("code-") } 172 | 173 | /// Indicates if the model was trained to work with the `/v1/edits` feature. 174 | public var supportsEdit: Bool { value.contains("-edit-") } 175 | 176 | /// Indicates if the model supports text similarty. 177 | public var supportsTextSimilarity: Bool { value.starts(with: "text-similarity-") } 178 | 179 | /// Indicates if the model supports text search. 180 | public var supportsTextSearch: Bool { value.starts(with: "text-search-") } 181 | 182 | /// Indicates if the model supports code search. 183 | public var supportsCodeSearch: Bool { value.starts(with: "code-search-") } 184 | 185 | public var supportsEmbeddings: Bool { 186 | value.starts(with: "text-embedding-") || supportsTextSimilarity || supportsTextSearch || supportsCodeSearch 187 | } 188 | 189 | /// Indicates if the model is a `fine-tune` of another model. 190 | public var isFineTune: Bool { 191 | return value.contains(":ft-") 192 | } 193 | 194 | /// Indicates if the model supports audio transcription/translation. 195 | public var supportsAudio: Bool { value.starts(with: "whisper-") } 196 | } 197 | -------------------------------------------------------------------------------- /Sources/OpenAIBits/Types/Language.swift: -------------------------------------------------------------------------------- 1 | /// Represents a language code, based on [ISO-639-1](https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes). 2 | /// The code is either 2 or 3 letters, from `a` to `z`, and is case-insensitive (the ``code`` property is always lowercased). 3 | public struct Language: Codable, Hashable { 4 | public let code: String 5 | 6 | /// Creates a new language code. 7 | /// 8 | /// - Parameter rawValue: The language code, in two or three letters. 9 | public init?(_ code: String) { 10 | let count = code.count 11 | guard count == 2 || count == 3 else { return nil } 12 | guard code.allSatisfy({ $0.isLetter }) else { return nil } 13 | 14 | self.code = code.lowercased() 15 | } 16 | } 17 | 18 | // MARK: - Utility protocol extensions. 19 | 20 | extension Language: RawRepresentable { 21 | 22 | public var rawValue: String { code } 23 | 24 | public init?(rawValue: String) { 25 | self.init(rawValue) 26 | } 27 | } 28 | 29 | extension Language: CustomStringConvertible { 30 | public var description: String { rawValue } 31 | } 32 | 33 | extension Language: CustomDebugStringConvertible { 34 | public var debugDescription: String { rawValue } 35 | } 36 | 37 | extension Language: ExpressibleByStringLiteral { 38 | public init(stringLiteral value: String) { 39 | guard let instance = Language(rawValue: value) else { 40 | fatalError("Invalid language code: \(value)") 41 | } 42 | self = instance 43 | } 44 | } 45 | 46 | // MARK: Officially supported languages. 47 | 48 | extension Language { 49 | public static let english: Language = "en" 50 | public static let chinese: Language = "zh" 51 | public static let german: Language = "de" 52 | public static let spanish: Language = "es" 53 | public static let russian: Language = "ru" 54 | public static let korean: Language = "ko" 55 | public static let french: Language = "fr" 56 | public static let japanese: Language = "ja" 57 | public static let portuguese: Language = "pt" 58 | public static let turkish: Language = "tr" 59 | public static let polish: Language = "pl" 60 | public static let catalan: Language = "ca" 61 | public static let dutch: Language = "nl" 62 | public static let arabic: Language = "ar" 63 | public static let swedish: Language = "sv" 64 | public static let italian: Language = "it" 65 | public static let indonesian: Language = "id" 66 | public static let hindi: Language = "hi" 67 | public static let finnish: Language = "fi" 68 | public static let vietnamese: Language = "vi" 69 | public static let hebrew: Language = "he" 70 | public static let ukrainian: Language = "uk" 71 | public static let greek: Language = "el" 72 | public static let malay: Language = "ms" 73 | public static let czech: Language = "cs" 74 | public static let romanian: Language = "ro" 75 | public static let danish: Language = "da" 76 | public static let hungarian: Language = "hu" 77 | public static let tamil: Language = "ta" 78 | public static let norwegian: Language = "no" 79 | public static let thai: Language = "th" 80 | public static let urdu: Language = "ur" 81 | public static let croatian: Language = "hr" 82 | public static let bulgarian: Language = "bg" 83 | public static let lithuanian: Language = "lt" 84 | public static let latin: Language = "la" 85 | public static let maori: Language = "mi" 86 | public static let malayalam: Language = "ml" 87 | public static let welsh: Language = "cy" 88 | public static let slovak: Language = "sk" 89 | public static let telugu: Language = "te" 90 | public static let persian: Language = "fa" 91 | public static let latvian: Language = "lv" 92 | public static let bengali: Language = "bn" 93 | public static let serbian: Language = "sr" 94 | public static let azerbaijani: Language = "az" 95 | public static let slovenian: Language = "sl" 96 | public static let kannada: Language = "kn" 97 | public static let estonian: Language = "et" 98 | public static let macedonian: Language = "mk" 99 | public static let breton: Language = "br" 100 | public static let basque: Language = "eu" 101 | public static let icelandic: Language = "is" 102 | public static let armenian: Language = "hy" 103 | public static let nepali: Language = "ne" 104 | public static let mongolian: Language = "mn" 105 | public static let bosnian: Language = "bs" 106 | public static let kazakh: Language = "kk" 107 | public static let albanian: Language = "sq" 108 | public static let swahili: Language = "sw" 109 | public static let galician: Language = "gl" 110 | public static let marathi: Language = "mr" 111 | public static let punjabi: Language = "pa" 112 | public static let sinhala: Language = "si" 113 | public static let khmer: Language = "km" 114 | public static let shona: Language = "sn" 115 | public static let yoruba: Language = "yo" 116 | public static let somali: Language = "so" 117 | public static let afrikaans: Language = "af" 118 | public static let occitan: Language = "oc" 119 | public static let georgian: Language = "ka" 120 | public static let belarusian: Language = "be" 121 | public static let tajik: Language = "tg" 122 | public static let sindhi: Language = "sd" 123 | public static let gujarati: Language = "gu" 124 | public static let amharic: Language = "am" 125 | public static let yiddish: Language = "yi" 126 | public static let lao: Language = "lo" 127 | public static let uzbek: Language = "uz" 128 | public static let faroese: Language = "fo" 129 | public static let haitianCreole: Language = "ht" 130 | public static let pashto: Language = "ps" 131 | public static let turkmen: Language = "tk" 132 | public static let nynorsk: Language = "nn" 133 | public static let maltese: Language = "mt" 134 | public static let sanskrit: Language = "sa" 135 | public static let luxembourgish: Language = "lb" 136 | public static let myanmar: Language = "my" 137 | public static let tibetan: Language = "bo" 138 | public static let tagalog: Language = "tl" 139 | public static let malagasy: Language = "mg" 140 | public static let assamese: Language = "as" 141 | public static let tatar: Language = "tt" 142 | public static let hawaiian: Language = "haw" 143 | public static let lingala: Language = "ln" 144 | public static let hausa: Language = "ha" 145 | public static let bashkir: Language = "ba" 146 | public static let javanese: Language = "jw" 147 | public static let sundanese: Language = "su" 148 | } 149 | 150 | // MARK: Common language aliases 151 | 152 | extension Language { 153 | public static let burmese: Language = .myanmar 154 | public static let valencian: Language = .catalan 155 | public static let flemish: Language = .dutch 156 | public static let haitian: Language = .haitianCreole 157 | public static let letzeburgesch: Language = .luxembourgish 158 | public static let pushto: Language = .pashto 159 | public static let panjabi: Language = .punjabi 160 | public static let moldavian: Language = .romanian 161 | public static let moldovan: Language = .romanian 162 | public static let sinhalese: Language = .sinhala 163 | public static let castilian: Language = .spanish 164 | } 165 | -------------------------------------------------------------------------------- /Sources/OpenAIBits/Calls/Files.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import MultipartForm 3 | 4 | /// Files are used to upload documents that can be used with features like ``FineTunes``. 5 | /// 6 | /// ## Calls 7 | /// 8 | /// - ``Files/List`` - Lists available ``File``s. 9 | /// - ``Files/Upload`` - Uploads a new ``File`` with a specific ``Files/Upload/Purpose-swift.enum``. 10 | /// - ``Files/Delete`` - Deletes a specific ``File``. 11 | /// - ``Files/Detail`` - Retrieves details of a specific ``File``. 12 | /// - ``Files/Content`` - Downloads the content of a ``File``. 13 | /// 14 | /// ## See Also 15 | /// 16 | /// - [OpenAI API](https://platform.openai.com/docs/api-reference/files) 17 | public enum Files {} 18 | 19 | // MARK: List 20 | 21 | extension Files { 22 | /// Returns a list of files that belong to the user's organization. 23 | /// 24 | /// ## See Also 25 | /// 26 | /// - [OpenAI API](https://platform.openai.com/docs/api-reference/files/list) 27 | public struct List: GetCall { 28 | /// Results in a ``ListOf`` ``File``s. 29 | public typealias Response = ListOf 30 | 31 | var path: String { "files" } 32 | 33 | /// Initializes a ``Files/List`` call. 34 | public init() {} 35 | } 36 | } 37 | 38 | // MARK: Upload 39 | 40 | extension Files { 41 | /// Upload a file that contains document(s) to be used across various endpoints/features. 42 | /// Currently, the size of all the files uploaded by one organization can be up to 1 GB. 43 | /// Please contact OpenAI if you need to increase the storage limit. 44 | /// 45 | /// ## See Also 46 | /// 47 | /// - [OpenAI API](https://platform.openai.com/docs/api-reference/files/upload) 48 | public struct Upload: MultipartPostCall { 49 | /// The purpose of the file 50 | public enum Purpose: String, Equatable, Codable { 51 | case fineTune = "fine-tune" 52 | case answers 53 | case search 54 | case classifications 55 | } 56 | 57 | /// Responds with a ``File``. 58 | public typealias Response = File 59 | 60 | /// The path. 61 | var path: String { "files" } 62 | 63 | /// Name of the [JSON Lines](https://jsonlines.readthedocs.io/en/latest/) file to be uploaded. 64 | /// 65 | /// If the purpose is set to `.fineTune`, each line is a JSON record with "prompt" and "completion" fields representing your [training examples](https://platform.openai.com/docs/guides/fine-tuning/prepare-training-data). 66 | public let filename: String 67 | 68 | /// The intended purpose of the uploaded documents. 69 | /// 70 | /// Use `.fineTune` for [Fine-tuning](https://platform.openai.com/docs/api-reference/fine-tunes). 71 | /// This allows OpenAI to validate the format of the uploaded file. 72 | public let purpose: Purpose 73 | 74 | /// The file `Data`. 75 | public let data: Data 76 | 77 | /// Create a new upload call. 78 | /// 79 | /// - Parameter filename: The filename. 80 | /// - Parameter purpose: The ``Files/Upload/Purpose-swift.enum`` of the upload. 81 | /// - Parameter data: The `Data` to the file to upload. 82 | public init( 83 | filename: String, 84 | purpose: Purpose, 85 | data: Data 86 | ) { 87 | self.filename = filename 88 | self.purpose = purpose 89 | self.data = data 90 | } 91 | 92 | /// Create a new upload call from a `URL`. 93 | /// - Parameters: 94 | /// - filename: The filename. Uses the `URL`s `lastPathComponent` if not provided. 95 | /// - purpose: The ``purpose-swift.property``. 96 | /// - url: The `URL` to load the file from. 97 | public init( 98 | filename: String? = nil, 99 | purpose: Purpose, 100 | url: URL 101 | ) throws { 102 | self.filename = filename ?? url.lastPathComponent 103 | self.purpose = purpose 104 | self.data = try Data(contentsOf: url) 105 | } 106 | 107 | /// Create a new upload call. 108 | /// 109 | /// - Parameter file: The filename. If not specified, it uses the 110 | 111 | /// Returns a `MultipartForm` based on the purpose and file. 112 | /// 113 | /// - Returns the form. 114 | /// - Throws an error if unable to load the file. 115 | public func getForm() throws -> MultipartForm { 116 | return MultipartForm( 117 | parts: [ 118 | .init(name: "purpose", value: purpose.rawValue), 119 | .init(name: "file", data: data, filename: filename), 120 | ], 121 | boundary: boundary 122 | ) 123 | } 124 | } 125 | } 126 | 127 | // MARK: Delete 128 | 129 | extension Files { 130 | /// Attempts to delete the nominated file, if one exists with the provided ``File/ID-swift.struct``. 131 | /// 132 | /// ## See Also 133 | /// 134 | /// - [OpenAI API](https://platform.openai.com/docs/api-reference/files/delete) 135 | public struct Delete: DeleteCall { 136 | /// The `Response` to the ``Files/Delete`` call. 137 | public struct Response: JSONResponse { 138 | /// The ``File/ID-swift.struct`` of the filedeleted. 139 | public let id: File.ID 140 | 141 | /// Indicates if the file was successfully deleted. 142 | public let deleted: Bool 143 | 144 | /// Initializes the ``Files/Delete`` call. 145 | /// 146 | /// - Parameter id: The `File` ``File/ID` to delete. 147 | /// - Parameter deleted: Indicates if the delete was successful. 148 | public init(id: File.ID, deleted: Bool) { 149 | self.id = id 150 | self.deleted = deleted 151 | } 152 | } 153 | 154 | /// The path to the file deletion `URL`. 155 | var path: String { "files/\(id)" } 156 | 157 | /// The ``File/ID-swift.struct`` of the file to use for this request. 158 | public let id: File.ID 159 | 160 | /// Creates a new `Delete File` call, providing the ``File`` `ID` to delete. 161 | /// 162 | /// - Parameter id: The ``File`` `ID`. 163 | public init(id: File.ID) { 164 | self.id = id 165 | } 166 | } 167 | } 168 | 169 | // MARK: Detail 170 | 171 | extension Files { 172 | /// Returns information about a specific file ``File/ID-swift.struct``. 173 | /// 174 | /// ## See Also 175 | /// 176 | /// - [OpenAI API](https://platform.openai.com/docs/api-reference/files/retrieve) 177 | public struct Detail: GetCall { 178 | public typealias Response = File 179 | 180 | var path: String { "files/\(id)" } 181 | 182 | /// The ``File/ID-swift.struct`` of the file to use for this request. 183 | public let id: File.ID 184 | 185 | /// Creates a call to request information about a specific file ``File/ID-swift.struct``. 186 | /// 187 | /// - Parameter id: The ``File/ID-swift.struct`` of the file to use for this request. 188 | public init(id: File.ID) { 189 | self.id = id 190 | } 191 | } 192 | } 193 | 194 | // MARK: Content 195 | 196 | extension Files { 197 | /// Returns the contents of the specified file ``File/ID-swift.struct``. 198 | /// 199 | /// ## See Also 200 | /// 201 | /// - [OpenAI API](https://platform.openai.com/docs/api-reference/files/retrieve-content) 202 | public struct Content: GetCall { 203 | /// Responds with ``BinaryData``. 204 | public typealias Response = BinaryData 205 | 206 | /// The path to the request. 207 | var path: String { "files/\(id)/content" } 208 | 209 | /// The ``File/ID-swift.struct`` of the file to use for this request. 210 | public let id: File.ID 211 | 212 | /// Creates a call to return the contents of the specified file ``File/ID-swift.struct``. 213 | /// 214 | /// - Parameter id: The ``File/ID-swift.struct``. 215 | public init(id: File.ID) { 216 | self.id = id 217 | } 218 | } 219 | } 220 | -------------------------------------------------------------------------------- /Sources/OpenAIBits/Types/FineTune.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /// Represents a ``FineTune`` job, which may be in progress or complete. See ``FineTunes`` for details. 4 | /// 5 | /// ## Related Calls 6 | /// 7 | /// - ``FineTunes/Create`` 8 | /// - ``FineTunes/List`` 9 | /// - ``FineTunes/Detail`` 10 | /// - ``FineTunes/Cancel`` 11 | /// - ``FineTunes/Delete`` 12 | /// 13 | /// ## See Also 14 | /// 15 | /// - [OpenAI API](https://platform.openai.com/docs/api-reference/fine-tunes) 16 | /// - [Fine-tuning guide](https://platform.openai.com/docs/guides/fine-tuning) 17 | public struct FineTune: Identifiable, JSONResponse { 18 | /// A unique ID for a ``FineTune`` job. 19 | public struct ID: Identifier { 20 | public let value: String 21 | 22 | public init(_ value: String) { 23 | self.value = value 24 | } 25 | } 26 | 27 | /// The ``FineTune/ID-swift.struct`` for the fine-tune job. 28 | public let id: ID 29 | 30 | /// The original ``FineTune/Model-swift.enum`` being fine-tuned. 31 | public let model: Model 32 | 33 | /// The ``OpenAIBits/Model/ID-swift.struct`` of the resulting fine-tuned ``Model``. 34 | public let fineTunedModel: OpenAIBits.Model.ID? 35 | 36 | /// The creation date. 37 | public let created: Date 38 | 39 | /// The list of ``Event``s related to the job. 40 | public let events: [Event]? 41 | 42 | /// The ``Hyperparams-swift.struct`` 43 | public let hyperparams: Hyperparams 44 | 45 | /// The owning organization ID. 46 | public let organizationId: String 47 | 48 | /// The list of ``File``s that resulted from the job. 49 | public let resultFiles: [File] 50 | 51 | /// The current status. 52 | public let status: String 53 | 54 | /// The list of ``File``s used for validation of the training. 55 | public let validationFiles: [File] 56 | 57 | /// The list of ``File``s used for training. 58 | public let trainingFiles: [File] 59 | 60 | /// The last update date. 61 | public let updated: Date 62 | 63 | /// Constructs a `fine-tune` job description. 64 | /// 65 | /// - Parameters: 66 | /// - id: The unique ``FineTune/ID-swift.struct``. 67 | /// - model: The ``Model-swift.enum`` being fine-tuned. 68 | /// - created: The creation date. 69 | /// - events: The list of ``Event``s. 70 | /// - fineTunedModel: The ``Model/ID-swift.struct`` of the resulting fine-tuned ``Model``. 71 | /// - hyperparams: The ``Hyperparams-swift.struct``. 72 | /// - organizationId: The owning organization ID. 73 | /// - resultFiles: The list of ``File``s that resulted from the job. 74 | /// - status: The current status. 75 | /// - validationFiles: The list of ``File``s used for validation of the training. 76 | /// - trainingFiles: The list of ``File``s used for training. 77 | /// - updated: The last update date. 78 | public init(id: ID, model: Model, created: Date, events: [Event]?, fineTunedModel: OpenAIBits.Model.ID?, hyperparams: Hyperparams, organizationId: String, resultFiles: [File], status: String, validationFiles: [File], trainingFiles: [File], updated: Date) { 79 | self.id = id 80 | self.model = model 81 | self.created = created 82 | self.events = events 83 | self.fineTunedModel = fineTunedModel 84 | self.hyperparams = hyperparams 85 | self.organizationId = organizationId 86 | self.resultFiles = resultFiles 87 | self.status = status 88 | self.validationFiles = validationFiles 89 | self.trainingFiles = trainingFiles 90 | self.updated = updated 91 | } 92 | } 93 | 94 | extension FineTune { 95 | enum CodingKeys: String, CodingKey { 96 | case id 97 | case model 98 | case created = "createdAt" 99 | case events 100 | case fineTunedModel 101 | case hyperparams 102 | case organizationId 103 | case resultFiles 104 | case status 105 | case validationFiles 106 | case trainingFiles 107 | case updated = "updatedAt" 108 | } 109 | } 110 | 111 | extension FineTune { 112 | /// Options for fine tune models. 113 | public enum Model: RawRepresentable, Equatable, Codable, CustomStringConvertible { 114 | /// The `Ada` model. 115 | case ada 116 | 117 | /// The `Babbage` model. 118 | case babbage 119 | 120 | /// The `Curie` model. 121 | case curie 122 | 123 | /// The `Davindi` model. 124 | case davinci 125 | 126 | /// Another fine-tuned ``OpenAIBits/Model``. 127 | case fineTuned(_ id: OpenAIBits.Model.ID) 128 | 129 | public init?(rawValue: String) { 130 | switch rawValue { 131 | case "ada": self = .ada 132 | case "babbage": self = .babbage 133 | case "curie": self = .curie 134 | case "davinci": self = .davinci 135 | default: 136 | let id = OpenAIBits.Model.ID(rawValue) 137 | if id.isFineTune { 138 | self = .fineTuned(id) 139 | } else { 140 | return nil 141 | } 142 | } 143 | } 144 | 145 | public var rawValue: String { 146 | switch self { 147 | case .ada: return "ada" 148 | case .babbage: return "babbage" 149 | case .curie: return "curie" 150 | case .davinci: return "davinci" 151 | case .fineTuned(let id): 152 | return id.value 153 | } 154 | } 155 | 156 | public var description: String { rawValue } 157 | } 158 | } 159 | 160 | extension FineTune { 161 | /// Event 162 | public struct Event: Equatable, Codable { 163 | public let created: Date 164 | public let level: String 165 | public let message: String 166 | 167 | enum CodingKeys: String, CodingKey { 168 | case created = "createdAt" 169 | case level 170 | case message 171 | } 172 | } 173 | } 174 | 175 | extension FineTune { 176 | /// Details about the training parameters. 177 | public struct Hyperparams: Equatable, Codable { 178 | /// The number of epochs to train the model for. An epoch refers to one full cycle through the training dataset. 179 | public let nEpochs: Int 180 | 181 | /// The batch size to use for training. The batch size is the number of training examples used to train a single forward and backward pass. 182 | /// 183 | /// By default, the batch size will be dynamically configured to be ~0.2% of the number of examples in the training set, capped at `256` - in general, we've found that larger batch sizes tend to work better for larger datasets. 184 | public let batchSize: Int 185 | 186 | /// The learning rate multiplier to use for training. The fine-tuning learning rate is the original learning rate used for pretraining multiplied by this value. 187 | /// 188 | /// By default, the learning rate multiplier is the 0.05, 0.1, or 0.2 depending on final batch_size (larger learning rates tend to perform better with larger batch sizes). We recommend experimenting with values in the range 0.02 to 0.2 to see what produces the best results. 189 | public let learningRateMultiplier: Double 190 | 191 | /// The weight to use for loss on the prompt tokens. This controls how much the model tries to learn to generate the prompt (as compared to the completion which always has a weight of `1.0`), and can add a stabilizing effect to training when completions are short. 192 | /// 193 | /// If prompts are extremely long (relative to completions), it may make sense to reduce this weight so as to avoid over-prioritizing learning the prompt. 194 | public let promptLossWeight: Double 195 | } 196 | } 197 | 198 | extension FineTune { 199 | /// Represents a ``FineTune`` job status. 200 | public enum Status: RawRepresentable, Equatable, Codable, CustomStringConvertible { 201 | /// The job has not yet started. 202 | case pending 203 | /// The job succeeded. 204 | case succeeded 205 | 206 | // TODO: Figure out other valid statuses 207 | 208 | /// Any other status. 209 | case other(String) 210 | 211 | public var rawValue: String { 212 | switch self { 213 | case .pending: return "pending" 214 | case .succeeded: return "succeeded" 215 | case .other(let value): return value 216 | } 217 | } 218 | 219 | public init?(rawValue: String) { 220 | switch rawValue { 221 | case "pending": self = .pending 222 | case "succeeded": self = .succeeded 223 | default: self = .other(rawValue) 224 | } 225 | } 226 | 227 | public var description: String { rawValue } 228 | } 229 | } 230 | -------------------------------------------------------------------------------- /Tests/OpenAIBitsTests/AudioTests.swift: -------------------------------------------------------------------------------- 1 | import CustomDump 2 | import XCTest 3 | 4 | @testable import OpenAIBits 5 | 6 | // ## json 7 | // Content-Type: application/json 8 | // Sample Output: 9 | let jsonSample = """ 10 | {"text":"You sure not to lock us on my way, huh? Ha ha! Pickly good thought. That's our home for the next couple of hours. We want to take good care of it. Finally, that one foot up in."} 11 | """ 12 | 13 | // ## verbose_json 14 | // Content-Type: application/json 15 | // Sample Output: 16 | let verboseJsonSample = """ 17 | {"task":"transcribe","language":"english","duration":14.63,"segments":[{"id":0,"seek":0,"start":0.0,"end":2.32,"text":" You sure not to lock us on my way, huh?","tokens":[509,988,406,281,4017,505,322,452,636,11,7020,30],"temperature":0.0,"avgLogprob":-0.6381065731956845,"compressionRatio":1.3175675675675675,"noSpeechProb":0.19806183874607086,"transient":false},{"id":1,"seek":0,"start":2.32,"end":3.3200000000000003,"text":" Ha ha!","tokens":[4064,324,0],"temperature":0.0,"avgLogprob":-0.6381065731956845,"compressionRatio":1.3175675675675675,"noSpeechProb":0.19806183874607086,"transient":false},{"id":2,"seek":0,"start":5.32,"end":6.5200000000000005,"text":" Pickly good thought.","tokens":[14129,356,665,1194,13],"temperature":0.0,"avgLogprob":-0.6381065731956845,"compressionRatio":1.3175675675675675,"noSpeechProb":0.19806183874607086,"transient":false},{"id":3,"seek":0,"start":8.52,"end":11.52,"text":" That's our home for the next couple of hours. We want to take good care of it.","tokens":[663,311,527,1280,337,264,958,1916,295,2496,13,492,528,281,747,665,1127,295,309,13],"temperature":0.0,"avgLogprob":-0.6381065731956845,"compressionRatio":1.3175675675675675,"noSpeechProb":0.19806183874607086,"transient":false},{"id":4,"seek":1152,"start":11.52,"end":29.04,"text":" Finally, that one foot up in.","tokens":[50364,6288,11,300,472,2671,493,294,13,51240],"temperature":0.0,"avgLogprob":-0.7234281626614657,"compressionRatio":0.7837837837837838,"noSpeechProb":0.011953424662351608,"transient":false}],"text":"You sure not to lock us on my way, huh? Ha ha! Pickly good thought. That's our home for the next couple of hours. We want to take good care of it. Finally, that one foot up in."} 18 | """ 19 | 20 | let verboseJsonInstance = VerboseJSONTranscription( 21 | task: "transcribe", 22 | language: "english", 23 | duration: 14.63, 24 | segments: [ 25 | .init( 26 | id: 0, 27 | seek: 0, 28 | start: 0.0, 29 | end: 2.32, 30 | text: " You sure not to lock us on my way, huh?", 31 | tokens: [509, 988, 406, 281, 4017, 505, 322, 452, 636, 11, 7020, 30], 32 | temperature: 0.0, 33 | avgLogprob: -0.6381065731956845, 34 | compressionRatio: 1.3175675675675675, 35 | noSpeechProb: 0.19806183874607086, 36 | transient: false 37 | ), 38 | .init( 39 | id: 1, 40 | seek: 0, 41 | start: 2.32, 42 | end: 3.3200000000000003, 43 | text: " Ha ha!", 44 | tokens: [4064, 324, 0], 45 | temperature: 0.0, 46 | avgLogprob: -0.6381065731956845, 47 | compressionRatio: 1.3175675675675675, 48 | noSpeechProb: 0.19806183874607086, 49 | transient: false 50 | ), 51 | .init( 52 | id: 2, 53 | seek: 0, 54 | start: 5.32, 55 | end: 6.5200000000000005, 56 | text: " Pickly good thought.", 57 | tokens: [14129, 356, 665, 1194, 13], 58 | temperature: 0.0, 59 | avgLogprob: -0.6381065731956845, 60 | compressionRatio: 1.3175675675675675, 61 | noSpeechProb: 0.19806183874607086, 62 | transient: false 63 | ), 64 | .init( 65 | id: 3, 66 | seek: 0, 67 | start: 8.52, 68 | end: 11.52, 69 | text: " That's our home for the next couple of hours. We want to take good care of it.", 70 | tokens: [ 71 | 663, 311, 527, 1280, 337, 264, 958, 1916, 295, 2496, 13, 492, 528, 281, 747, 665, 1127, 295, 72 | 309, 13, 73 | ], 74 | temperature: 0.0, 75 | avgLogprob: -0.6381065731956845, 76 | compressionRatio: 1.3175675675675675, 77 | noSpeechProb: 0.19806183874607086, 78 | transient: false 79 | ), 80 | .init( 81 | id: 4, 82 | seek: 1152, 83 | start: 11.52, 84 | end: 29.04, 85 | text: " Finally, that one foot up in.", 86 | tokens: [50364, 6288, 11, 300, 472, 2671, 493, 294, 13, 51240], 87 | temperature: 0.0, 88 | avgLogprob: -0.7234281626614657, 89 | compressionRatio: 0.7837837837837838, 90 | noSpeechProb: 0.011953424662351608, 91 | transient: false 92 | ), 93 | ], 94 | text: textSample 95 | ) 96 | 97 | // ## text 98 | // Content-Type: text/plain; charset=utf-8 99 | // Sample Output: 100 | let textSample = """ 101 | You sure not to lock us on my way, huh? Ha ha! Pickly good thought. That's our home for the next couple of hours. We want to take good care of it. Finally, that one foot up in. 102 | """ 103 | 104 | // ## srt 105 | // Content-Type: text/plain; charset=utf-8 106 | // Sample Output: 107 | let srtSample = """ 108 | 1 109 | 00:00:00,000 --> 00:00:02,320 110 | You sure not to lock us on my way, huh? 111 | 112 | 2 113 | 00:00:02,320 --> 00:00:03,320 114 | Ha ha! 115 | 116 | 3 117 | 00:00:05,320 --> 00:00:06,520 118 | Pickly good thought. 119 | 120 | 4 121 | 00:00:08,520 --> 00:00:11,520 122 | That's our home for the next couple of hours. We want to take good care of it. 123 | 124 | 5 125 | 00:00:11,520 --> 00:00:29,040 126 | Finally, that one foot up in. 127 | 128 | """ 129 | 130 | // ## vtt 131 | // Content-Type: text/plain; charset=utf-8 132 | // Sample Output: 133 | let vttSample = """ 134 | WEBVTT 135 | 136 | 00:00:00.000 --> 00:00:02.320 137 | You sure not to lock us on my way, huh? 138 | 139 | 00:00:02.320 --> 00:00:03.320 140 | Ha ha! 141 | 142 | 00:00:05.320 --> 00:00:06.520 143 | Pickly good thought. 144 | 145 | 00:00:08.520 --> 00:00:11.520 146 | That's our home for the next couple of hours. We want to take good care of it. 147 | 148 | 00:00:11.520 --> 00:00:29.040 149 | Finally, that one foot up in. 150 | 151 | """ 152 | 153 | final class AudioTests: XCTestCase { 154 | 155 | func testAudioTranscriptionRequestToJSON() throws { 156 | let file = Data("audio data".utf8) 157 | let value = Audio.Transcriptions( 158 | file: file, 159 | fileName: "file.wav", 160 | model: .whisper_1, 161 | prompt: "prompt", 162 | responseFormat: .text, 163 | temperature: 0.5, 164 | language: .japanese 165 | ) 166 | 167 | let form = try value.getForm() 168 | 169 | XCTAssertEqual(form.parts.count, 6) 170 | XCTAssertEqual(form.parts[0].data, file) 171 | XCTAssertEqual(form.parts[0].filename, "file.wav") 172 | XCTAssertEqual(form.parts[1].value, "whisper-1") 173 | XCTAssertEqual(form.parts[2].value, "prompt") 174 | XCTAssertEqual(form.parts[3].value, "text") 175 | XCTAssertEqual(form.parts[4].value, "0.5") 176 | XCTAssertEqual(form.parts[5].value, "ja") 177 | } 178 | 179 | func testJSONTranscriptionResponse() throws { 180 | XCTAssertNoDifference( 181 | JSONTranscription(text: textSample), 182 | try jsonDecode(jsonSample) 183 | ) 184 | } 185 | 186 | func testJSONTranscriptionEnum() throws { 187 | XCTAssertNoDifference( 188 | Transcription.json(JSONTranscription(text: textSample)), 189 | try Transcription.init(data: Data(jsonSample.utf8), contentType: APPLICATION_JSON) 190 | ) 191 | } 192 | 193 | func testVerboseJSONTranscriptionResponse() throws { 194 | XCTAssertNoDifference( 195 | verboseJsonInstance, 196 | try jsonDecode(verboseJsonSample) 197 | ) 198 | } 199 | 200 | func testVerboseJSONTransactionEnum() throws { 201 | XCTAssertNoDifference( 202 | Transcription.verboseJson(verboseJsonInstance), 203 | try Transcription.init(data: Data(verboseJsonSample.utf8), contentType: APPLICATION_JSON) 204 | ) 205 | } 206 | 207 | func testSRTTranscriptionEnum() throws { 208 | XCTAssertNoDifference( 209 | Transcription.srt(SRTTranscription(value: srtSample)), 210 | try Transcription.init(data: Data(srtSample.utf8), contentType: TEXT_PLAIN) 211 | ) 212 | } 213 | 214 | func testVTTTranscriptionEnum() throws { 215 | XCTAssertNoDifference( 216 | Transcription.vtt(VTTTranscription(value: vttSample)), 217 | try Transcription.init(data: Data(vttSample.utf8), contentType: TEXT_PLAIN) 218 | ) 219 | } 220 | 221 | func testTranscriptionText() throws { 222 | XCTAssertEqual( 223 | Transcription.text(TextTranscription(value: textSample)), 224 | try Transcription.init(data: Data(textSample.utf8), contentType: TEXT_PLAIN) 225 | ) 226 | } 227 | } 228 | --------------------------------------------------------------------------------