diff --git a/ReactNativeOpenAI.podspec b/ReactNativeOpenAI.podspec index 7df9b64..49b4998 100644 --- a/ReactNativeOpenAI.podspec +++ b/ReactNativeOpenAI.podspec @@ -11,13 +11,12 @@ Pod::Spec.new do |s| s.license = package["license"] s.authors = package["author"] - s.platforms = { :ios => "13.0" } + s.platforms = { :ios => "15.0" } s.source = { :git => "https://github.com/candlefinance/react-native-openai.git", :tag => "#{s.version}" } s.source_files = "ios/**/*.{h,m,mm,swift}" s.dependency "React-Core" - s.dependency "OpenAIKit" # Don't install the dependencies when we run `pod install` in the old architecture. if ENV['RCT_NEW_ARCH_ENABLED'] == '1' then diff --git a/example/ios/Podfile b/example/ios/Podfile index 0e21f3e..3c68976 100644 --- a/example/ios/Podfile +++ b/example/ios/Podfile @@ -5,7 +5,7 @@ require Pod::Executable.execute_command('node', ['-p', {paths: [process.argv[1]]}, )', __dir__]).strip -platform :ios, '13.0' +platform :ios, '15.0' prepare_react_native_project! # If you are using a `react-native-flipper` your iOS build will fail when `NO_FLIPPER=1` is set. diff --git a/example/ios/Podfile.lock b/example/ios/Podfile.lock index 9130835..b62cf7c 100644 --- a/example/ios/Podfile.lock +++ b/example/ios/Podfile.lock @@ -71,7 +71,6 @@ PODS: - fmt (6.2.1) - glog (0.3.5) - libevent (2.1.12) - - OpenAIKit (1.5.0) - OpenSSL-Universal (1.1.1100) - RCT-Folly (2021.07.22.00): - boost @@ -450,7 +449,6 @@ PODS: - React-logger (= 0.72.3) - React-perflogger (= 0.72.3) - ReactNativeOpenAI (0.1.0): - - OpenAIKit - React-Core - SocketRocket (0.6.1) - Yoga (1.14.0) @@ -534,7 +532,6 @@ SPEC REPOS: - FlipperKit - fmt - libevent - - OpenAIKit - OpenSSL-Universal - SocketRocket - YogaKit @@ -636,7 +633,6 @@ SPEC CHECKSUMS: fmt: ff9d55029c625d3757ed641535fd4a75fedc7ce9 glog: 04b94705f318337d7ead9e6d17c019bd9b1f6b1b libevent: 4049cae6c81cdb3654a443be001fb9bdceff7913 - OpenAIKit: cd69fa5a5585e13ab416042cb98fd31897dcf3f8 OpenSSL-Universal: ebc357f1e6bc71fa463ccb2fe676756aff50e88c RCT-Folly: 424b8c9a7a0b9ab2886ffe9c3b041ef628fd4fb1 RCTRequired: a2faf4bad4e438ca37b2040cb8f7799baa065c18 @@ -670,11 +666,11 @@ SPEC CHECKSUMS: React-runtimescheduler: af0b24628c1d543a3f87251c9efa29c5a589e08a React-utils: bcb57da67eec2711f8b353f6e3d33bd8e4b2efa3 ReactCommon: d7d63a5b3c3ff29304a58fc8eb3b4f1b077cd789 - ReactNativeOpenAI: 2bf70756d77ed1565720a14144e953abe7d155bd + ReactNativeOpenAI: 93da4285ad4a32ab357523643ec91e8d4d1513e8 SocketRocket: f32cd54efbe0f095c4d7594881e52619cfe80b17 Yoga: 8796b55dba14d7004f980b54bcc9833ee45b28ce YogaKit: f782866e155069a2cca2517aafea43200b01fd5a -PODFILE CHECKSUM: de38e1abd726248643b04e967f81da73b9a3b577 +PODFILE CHECKSUM: 19d89d5a431df00f3ce6b4f3999bfda93d6ad385 COCOAPODS: 1.12.1 diff --git a/ios/OpenAIKit/API.swift b/ios/OpenAIKit/API.swift new file mode 100644 index 0000000..0d0385a --- /dev/null +++ b/ios/OpenAIKit/API.swift @@ -0,0 +1,36 @@ +import Foundation + +public struct API { + public let scheme: Scheme + public let host: String + public let path: String? + + public init( + scheme: API.Scheme, + host: String, + pathPrefix path: String? = nil + ) { + self.scheme = scheme + self.host = host + self.path = path + } +} + +extension API { + public enum Scheme { + case http + case https + case custom(String) + + var value: String { + switch self { + case .http: + return "http" + case .https: + return "https" + case .custom(let scheme): + return scheme + } + } + } +} diff --git a/ios/OpenAIKit/Audio/AudioProvider.swift b/ios/OpenAIKit/Audio/AudioProvider.swift new file mode 100644 index 0000000..e1c9b2e --- /dev/null +++ b/ios/OpenAIKit/Audio/AudioProvider.swift @@ -0,0 +1,81 @@ +// +// AudioProvider.swift +// +// +// Created by Dylan Shine on 3/19/23. +// + +import Foundation + +public struct AudioProvider { + + private let requestHandler: RequestHandler + + init(requestHandler: RequestHandler) { + self.requestHandler = requestHandler + } + + /** + Create transcription BETA + POST + + https://api.openai.com/v1/audio/transcriptions + + Transcribes audio into the input language. + */ + public func transcribe( + file: Data, + fileName: String, + mimeType: MIMEType.Audio, + model: ModelID = Model.Whisper.whisper1, + prompt: String? = nil, + responseFormat: String? = nil, + temperature: Double? = nil, + language: Language? = nil + ) async throws -> Transcription { + + let request = CreateTranscriptionRequest( + file: file, + fileName: fileName, + mimeType: mimeType, + model: model, + prompt: prompt, + responseFormat: responseFormat, + temperature: temperature, + language: language + ) + + return try await requestHandler.perform(request: request) + } + + /** + Create translation BETA + POST + + https://api.openai.com/v1/audio/translations + + Translates audio into into English. + */ + public func translate( + file: Data, + fileName: String, + mimeType: MIMEType.Audio, + model: ModelID = Model.Whisper.whisper1, + prompt: String? = nil, + responseFormat: String? = nil, + temperature: Double? = nil + ) async throws -> Translation { + + let request = CreateTranslationRequest( + file: file, + fileName: fileName, + mimeType: mimeType, + model: model, + prompt: prompt, + responseFormat: responseFormat, + temperature: temperature + ) + + return try await requestHandler.perform(request: request) + } +} diff --git a/ios/OpenAIKit/Audio/CreateTranscriptionRequest.swift b/ios/OpenAIKit/Audio/CreateTranscriptionRequest.swift new file mode 100644 index 0000000..73d1425 --- /dev/null +++ b/ios/OpenAIKit/Audio/CreateTranscriptionRequest.swift @@ -0,0 +1,55 @@ +import Foundation + +struct CreateTranscriptionRequest: Request { + let method: HTTPMethod = .post + let path = "/v1/audio/transcriptions" + let body: Data? + private let boundary = UUID().uuidString + + var headers: HTTPHeaders { + var headers = HTTPHeaders() + headers.add(name: .contentType, value: "multipart/form-data; boundary=\(boundary)") + return headers + } + + init( + file: Data, + fileName: String, + mimeType: MIMEType.Audio, + model: ModelID, + prompt: String?, + responseFormat: String?, + temperature: Double?, + language: Language? + ) { + let builder = MultipartFormDataBuilder(boundary: boundary) + + builder.addDataField( + fieldName: "file", + fileName: fileName, + data: file, + mimeType: mimeType.rawValue + ) + + builder.addTextField(named: "model", value: model.id) + + if let prompt = prompt { + builder.addTextField(named: "prompt", value: prompt) + } + + if let responseFormat = responseFormat { + builder.addTextField(named: "response_format", value: responseFormat) + } + + if let temperature = temperature { + builder.addTextField(named: "temperature", value: String(temperature)) + } + + if let language = language { + builder.addTextField(named: "language", value: language.rawValue) + } + + self.body = builder.build() + } +} + diff --git a/ios/OpenAIKit/Audio/CreateTranslationRequest.swift b/ios/OpenAIKit/Audio/CreateTranslationRequest.swift new file mode 100644 index 0000000..4dc17e8 --- /dev/null +++ b/ios/OpenAIKit/Audio/CreateTranslationRequest.swift @@ -0,0 +1,49 @@ +import Foundation + +struct CreateTranslationRequest: Request { + let method: HTTPMethod = .post + let path = "/v1/audio/translations" + let body: Data? + private let boundary = UUID().uuidString + + var headers: HTTPHeaders { + var headers = HTTPHeaders() + headers.add(name: .contentType, value: "multipart/form-data; boundary=\(boundary)") + return headers + } + + init( + file: Data, + fileName: String, + mimeType: MIMEType.Audio, + model: ModelID, + prompt: String?, + responseFormat: String?, + temperature: Double? + ) { + let builder = MultipartFormDataBuilder(boundary: boundary) + + builder.addDataField( + fieldName: "file", + fileName: fileName, + data: file, + mimeType: mimeType.rawValue + ) + + builder.addTextField(named: "model", value: model.id) + + if let prompt = prompt { + builder.addTextField(named: "prompt", value: prompt) + } + + if let responseFormat = responseFormat { + builder.addTextField(named: "response_format", value: responseFormat) + } + + if let temperature = temperature { + builder.addTextField(named: "temperature", value: String(temperature)) + } + + self.body = builder.build() + } +} diff --git a/ios/OpenAIKit/Audio/Transcription.swift b/ios/OpenAIKit/Audio/Transcription.swift new file mode 100644 index 0000000..26a3137 --- /dev/null +++ b/ios/OpenAIKit/Audio/Transcription.swift @@ -0,0 +1,22 @@ +// +// Transcription.swift +// +// +// Created by Joshua Galvan on 6/12/23. +// + + +import Foundation + +/** + Audio + Learn how to turn audio into text. + + Related guide: https://platform.openai.com/docs/guides/speech-to-text + */ + +public struct Transcription { + public let text: String +} + +extension Transcription: Codable {} diff --git a/ios/OpenAIKit/Audio/Translation.swift b/ios/OpenAIKit/Audio/Translation.swift new file mode 100644 index 0000000..58585c2 --- /dev/null +++ b/ios/OpenAIKit/Audio/Translation.swift @@ -0,0 +1,22 @@ +// +// Translation.swift +// +// +// Created by Joshua Galvan on 6/12/23. +// + + +import Foundation + +/** + Audio + Learn how to turn audio into text. + + Related guide: https://platform.openai.com/docs/guides/speech-to-text + */ + +public struct Translation { + public let text: String +} + +extension Translation: Codable {} diff --git a/ios/OpenAIKit/Chat/Chat.swift b/ios/OpenAIKit/Chat/Chat.swift new file mode 100644 index 0000000..f0dde58 --- /dev/null +++ b/ios/OpenAIKit/Chat/Chat.swift @@ -0,0 +1,89 @@ +import Foundation + +/** + Given a prompt, the model will return one or more predicted chat completions, and can also return the probabilities of alternative tokens at each position. + */ +public struct Chat { + public let id: String + public let object: String + public let created: Date + public let model: String + public let choices: [Choice] + public let usage: Usage +} + +extension Chat: Codable {} + +extension Chat { + public struct Choice { + public let index: Int + public let message: Message + public let finishReason: FinishReason? + } +} + +extension Chat.Choice: Codable {} + +extension Chat { + public enum Message { + case system(content: String) + case user(content: String) + case assistant(content: String) + } +} + +extension Chat.Message: Codable { + private enum CodingKeys: String, CodingKey { + case role + case content + } + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + let role = try container.decode(String.self, forKey: .role) + let content = try container.decode(String.self, forKey: .content) + switch role { + case "system": + self = .system(content: content) + case "user": + self = .user(content: content) + case "assistant": + self = .assistant(content: content) + default: + throw DecodingError.dataCorruptedError(forKey: .role, in: container, debugDescription: "Invalid type") + } + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + switch self { + case .system(let content): + try container.encode("system", forKey: .role) + try container.encode(content, forKey: .content) + case .user(let content): + try container.encode("user", forKey: .role) + try container.encode(content, forKey: .content) + case .assistant(let content): + try container.encode("assistant", forKey: .role) + try container.encode(content, forKey: .content) + } + } +} + +extension Chat.Message { + public var content: String { + get { + switch self { + case .system(let content), .user(let content), .assistant(let content): + return content + } + } + set { + switch self { + case .system: self = .system(content: newValue) + case .user: self = .user(content: newValue) + case .assistant: self = .assistant(content: newValue) + } + } + } +} diff --git a/ios/OpenAIKit/Chat/ChatProvider.swift b/ios/OpenAIKit/Chat/ChatProvider.swift new file mode 100644 index 0000000..5a6a236 --- /dev/null +++ b/ios/OpenAIKit/Chat/ChatProvider.swift @@ -0,0 +1,95 @@ +public struct ChatProvider { + + private let requestHandler: RequestHandler + + init(requestHandler: RequestHandler) { + self.requestHandler = requestHandler + } + + /** + Create chat completion + POST + + https://api.openai.com/v1/chat/completions + + Creates a chat completion for the provided prompt and parameters + */ + public func create( + model: ModelID, + messages: [Chat.Message] = [], + temperature: Double = 1.0, + topP: Double = 1.0, + n: Int = 1, + stops: [String] = [], + maxTokens: Int? = nil, + presencePenalty: Double = 0.0, + frequencyPenalty: Double = 0.0, + logitBias: [String : Int] = [:], + user: String? = nil + ) async throws -> Chat { + + let request = try CreateChatRequest( + model: model.id, + messages: messages, + temperature: temperature, + topP: topP, + n: n, + stream: false, + stops: stops, + maxTokens: maxTokens, + presencePenalty: presencePenalty, + frequencyPenalty: frequencyPenalty, + logitBias: logitBias, + user: user + ) + + return try await requestHandler.perform(request: request) + + } + + /** + Create chat completion + POST + + https://api.openai.com/v1/chat/completions + + Creates a chat completion for the provided prompt and parameters + + stream If set, partial message deltas will be sent, like in ChatGPT. + Tokens will be sent as data-only server-sent events as they become available, with the stream terminated by a data: [DONE] message. + + https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events#event_stream_format + */ + public func stream( + model: ModelID, + messages: [Chat.Message] = [], + temperature: Double = 1.0, + topP: Double = 1.0, + n: Int = 1, + stops: [String] = [], + maxTokens: Int? = nil, + presencePenalty: Double = 0.0, + frequencyPenalty: Double = 0.0, + logitBias: [String : Int] = [:], + user: String? = nil + ) async throws -> AsyncThrowingStream { + + let request = try CreateChatRequest( + model: model.id, + messages: messages, + temperature: temperature, + topP: topP, + n: n, + stream: true, + stops: stops, + maxTokens: maxTokens, + presencePenalty: presencePenalty, + frequencyPenalty: frequencyPenalty, + logitBias: logitBias, + user: user + ) + + return try await requestHandler.stream(request: request) + + } +} diff --git a/ios/OpenAIKit/Chat/ChatStream.swift b/ios/OpenAIKit/Chat/ChatStream.swift new file mode 100644 index 0000000..f832039 --- /dev/null +++ b/ios/OpenAIKit/Chat/ChatStream.swift @@ -0,0 +1,32 @@ +import Foundation + +public struct ChatStream { + public let id: String + public let object: String + public let created: Date + public let model: String + public let choices: [ChatStream.Choice] +} + +extension ChatStream: Codable {} + +extension ChatStream { + public struct Choice { + public let index: Int + public let finishReason: FinishReason? + public let delta: ChatStream.Choice.Message + } +} + +extension ChatStream.Choice: Codable {} + +extension ChatStream.Choice { + public struct Message { + public let content: String? + public let role: String? + } +} + +extension ChatStream.Choice.Message: Codable {} + + diff --git a/ios/OpenAIKit/Chat/CreateChatRequest.swift b/ios/OpenAIKit/Chat/CreateChatRequest.swift new file mode 100644 index 0000000..40023ac --- /dev/null +++ b/ios/OpenAIKit/Chat/CreateChatRequest.swift @@ -0,0 +1,103 @@ +import Foundation + +struct CreateChatRequest: Request { + let method: HTTPMethod = .post + let path = "/v1/chat/completions" + let body: Data? + + init( + model: String, + messages: [Chat.Message], + temperature: Double, + topP: Double, + n: Int, + stream: Bool, + stops: [String], + maxTokens: Int?, + presencePenalty: Double, + frequencyPenalty: Double, + logitBias: [String: Int], + user: String? + ) throws { + + let body = Body( + model: model, + messages: messages, + temperature: temperature, + topP: topP, + n: n, + stream: stream, + stops: stops, + maxTokens: maxTokens, + presencePenalty: presencePenalty, + frequencyPenalty: frequencyPenalty, + logitBias: logitBias, + user: user + ) + + self.body = try Self.encoder.encode(body) + } +} + +extension CreateChatRequest { + struct Body: Encodable { + let model: String + let messages: [Chat.Message] + let temperature: Double + let topP: Double + let n: Int + let stream: Bool + let stops: [String] + let maxTokens: Int? + let presencePenalty: Double + let frequencyPenalty: Double + let logitBias: [String: Int] + let user: String? + + enum CodingKeys: CodingKey { + case model + case messages + case temperature + case topP + case n + case stream + case stop + case maxTokens + case presencePenalty + case frequencyPenalty + case logitBias + case user + } + + func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(model, forKey: .model) + + if !messages.isEmpty { + try container.encode(messages, forKey: .messages) + } + + try container.encode(temperature, forKey: .temperature) + try container.encode(topP, forKey: .topP) + try container.encode(n, forKey: .n) + try container.encode(stream, forKey: .stream) + + if !stops.isEmpty { + try container.encode(stops, forKey: .stop) + } + + if let maxTokens { + try container.encode(maxTokens, forKey: .maxTokens) + } + + try container.encode(presencePenalty, forKey: .presencePenalty) + try container.encode(frequencyPenalty, forKey: .frequencyPenalty) + + if !logitBias.isEmpty { + try container.encode(logitBias, forKey: .logitBias) + } + + try container.encodeIfPresent(user, forKey: .user) + } + } +} diff --git a/ios/OpenAIKit/Chat/FinishReason.swift b/ios/OpenAIKit/Chat/FinishReason.swift new file mode 100644 index 0000000..b77ed1c --- /dev/null +++ b/ios/OpenAIKit/Chat/FinishReason.swift @@ -0,0 +1,14 @@ +import Foundation + +public enum FinishReason: String { + /// API returned complete model output + case stop + + /// Incomplete model output due to max_tokens parameter or token limit + case length + + /// Omitted content due to a flag from our content filters + case contentFilter = "content_filter" +} + +extension FinishReason: Codable {} diff --git a/ios/OpenAIKit/Client/Client.swift b/ios/OpenAIKit/Client/Client.swift new file mode 100644 index 0000000..13f70a7 --- /dev/null +++ b/ios/OpenAIKit/Client/Client.swift @@ -0,0 +1,38 @@ +import Foundation + +@available(iOS 15.0, *) +public struct Client { + + public let audio: AudioProvider + public let chats: ChatProvider + public let completions: CompletionProvider + public let edits: EditProvider + public let embeddings: EmbeddingProvider + public let files: FileProvider + public let images: ImageProvider + public let models: ModelProvider + public let moderations: ModerationProvider + + init(requestHandler: RequestHandler) { + self.audio = AudioProvider(requestHandler: requestHandler) + self.models = ModelProvider(requestHandler: requestHandler) + self.completions = CompletionProvider(requestHandler: requestHandler) + self.chats = ChatProvider(requestHandler: requestHandler) + self.edits = EditProvider(requestHandler: requestHandler) + self.images = ImageProvider(requestHandler: requestHandler) + self.embeddings = EmbeddingProvider(requestHandler: requestHandler) + self.files = FileProvider(requestHandler: requestHandler) + self.moderations = ModerationProvider(requestHandler: requestHandler) + } + + public init( + session: URLSession, + configuration: Configuration + ) { + let requestHandler = URLSessionRequestHandler( + session: session, + configuration: configuration + ) + self.init(requestHandler: requestHandler) + } +} diff --git a/ios/OpenAIKit/Completion/Completion.swift b/ios/OpenAIKit/Completion/Completion.swift new file mode 100644 index 0000000..7e78775 --- /dev/null +++ b/ios/OpenAIKit/Completion/Completion.swift @@ -0,0 +1,45 @@ +import Foundation + +/** + Given a prompt, the model will return one or more predicted completions, and can also return the probabilities of alternative tokens at each position. + */ +public struct Completion { + public let id: String + public let object: String + public let created: Date + public let model: String + public let choices: [Choice] + public let usage: Usage +} + +extension Completion: Codable {} + +extension Completion { + public struct Choice { + public let text: String + public let index: Int + public let logprobs: Logprobs? + public let finishReason: String? + } +} + +extension Completion.Choice: Codable {} + +extension Completion.Choice { + public struct Logprobs { + public let tokens: [String] + public let tokenLogprobs: [Float] + public let topLogprobs: [String: Float] + public let textOffset: [Int] + } +} + +extension Completion.Choice.Logprobs: Codable { + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + self.tokens = try container.decodeIfPresent([String].self, forKey: .tokens) ?? [] + self.tokenLogprobs = try container.decodeIfPresent([Float].self, forKey: .tokenLogprobs) ?? [] + self.topLogprobs = try container.decodeIfPresent([String: Float].self, forKey: .topLogprobs) ?? [:] + self.textOffset = try container.decodeIfPresent([Int].self, forKey: .textOffset) ?? [] + } +} diff --git a/ios/OpenAIKit/Completion/CompletionProvider.swift b/ios/OpenAIKit/Completion/CompletionProvider.swift new file mode 100644 index 0000000..b332536 --- /dev/null +++ b/ios/OpenAIKit/Completion/CompletionProvider.swift @@ -0,0 +1,58 @@ +public struct CompletionProvider { + + private let requestHandler: RequestHandler + + init(requestHandler: RequestHandler) { + self.requestHandler = requestHandler + } + + /** + Create completion + POST + + https://api.openai.com/v1/completions + + Creates a completion for the provided prompt and parameters + */ + public func create( + model: ModelID, + prompts: [String] = [], + suffix: String? = nil, + maxTokens: Int = 16, + temperature: Double = 1.0, + topP: Double = 1.0, + n: Int = 1, + stream: Bool = false, + logprobs: Int? = nil, + echo: Bool = false, + stops: [String] = [], + presencePenalty: Double = 0.0, + frequencyPenalty: Double = 0.0, + bestOf: Int = 1, + logitBias: [String : Int] = [:], + user: String? = nil + ) async throws -> Completion { + + let request = try CreateCompletionRequest( + model: model.id, + prompts: prompts, + suffix: suffix, + maxTokens: maxTokens, + temperature: temperature, + topP: topP, + n: n, + stream: stream, + logprobs: logprobs, + echo: echo, + stops: stops, + presencePenalty: presencePenalty, + frequencyPenalty: frequencyPenalty, + bestOf: bestOf, + logitBias: logitBias, + user: user + ) + + return try await requestHandler.perform(request: request) + + } +} diff --git a/ios/OpenAIKit/Completion/CreateCompletionRequest.swift b/ios/OpenAIKit/Completion/CreateCompletionRequest.swift new file mode 100644 index 0000000..561096e --- /dev/null +++ b/ios/OpenAIKit/Completion/CreateCompletionRequest.swift @@ -0,0 +1,120 @@ +import Foundation + +struct CreateCompletionRequest: Request { + let method: HTTPMethod = .post + let path = "/v1/completions" + let body: Data? + + init( + model: String, + prompts: [String], + suffix: String?, + maxTokens: Int, + temperature: Double, + topP: Double, + n: Int, + stream: Bool, + logprobs: Int?, + echo: Bool, + stops: [String], + presencePenalty: Double, + frequencyPenalty: Double, + bestOf: Int, + logitBias: [String: Int], + user: String? + ) throws { + + let body = Body( + model: model, + prompts: prompts, + suffix: suffix, + maxTokens: maxTokens, + temperature: temperature, + topP: topP, + n: n, + stream: stream, + logprobs: logprobs, + echo: echo, + stops: stops, + presencePenalty: presencePenalty, + frequencyPenalty: frequencyPenalty, + bestOf: bestOf, + logitBias: logitBias, + user: user + ) + + self.body = try Self.encoder.encode(body) + } +} + +extension CreateCompletionRequest { + struct Body: Encodable { + let model: String + let prompts: [String] + let suffix: String? + let maxTokens: Int + let temperature: Double + let topP: Double + let n: Int + let stream: Bool + let logprobs: Int? + let echo: Bool + let stops: [String] + let presencePenalty: Double + let frequencyPenalty: Double + let bestOf: Int + let logitBias: [String: Int] + let user: String? + + enum CodingKeys: CodingKey { + case model + case prompt + case suffix + case maxTokens + case temperature + case topP + case n + case stream + case logprobs + case echo + case stop + case presencePenalty + case frequencyPenalty + case bestOf + case logitBias + case user + } + + func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(model, forKey: .model) + + if !prompts.isEmpty { + try container.encode(prompts, forKey: .prompt) + } + + try container.encodeIfPresent(suffix, forKey: .suffix) + try container.encode(maxTokens, forKey: .maxTokens) + try container.encode(temperature, forKey: .temperature) + try container.encode(topP, forKey: .topP) + try container.encode(n, forKey: .n) + try container.encode(stream, forKey: .stream) + try container.encodeIfPresent(logprobs, forKey: .logprobs) + try container.encode(echo, forKey: .echo) + + if !stops.isEmpty { + try container.encode(stops, forKey: .stop) + } + + try container.encode(presencePenalty, forKey: .presencePenalty) + try container.encode(frequencyPenalty, forKey: .frequencyPenalty) + try container.encode(bestOf, forKey: .bestOf) + + if !logitBias.isEmpty { + try container.encode(logitBias, forKey: .logitBias) + } + + try container.encodeIfPresent(user, forKey: .user) + } + } +} diff --git a/ios/OpenAIKit/Configuration.swift b/ios/OpenAIKit/Configuration.swift new file mode 100644 index 0000000..58f4a8f --- /dev/null +++ b/ios/OpenAIKit/Configuration.swift @@ -0,0 +1,27 @@ +public struct Configuration { + public let apiKey: String + public let organization: String? + public let api: API? + + var headers: HTTPHeaders { + var headers = HTTPHeaders() + headers.add(name: .authorization, value: "Bearer \(apiKey)") + + if let organization = organization { + headers.add(name: .openAIOrganization, value: organization) + } + + return headers + } + + public init( + apiKey: String, + organization: String? = nil, + api: API? = nil + ) { + self.apiKey = apiKey + self.organization = organization + self.api = api + } + +} diff --git a/ios/OpenAIKit/Edit/CreateEditRequest.swift b/ios/OpenAIKit/Edit/CreateEditRequest.swift new file mode 100644 index 0000000..73f10fe --- /dev/null +++ b/ios/OpenAIKit/Edit/CreateEditRequest.swift @@ -0,0 +1,39 @@ +import Foundation + +struct CreateEditRequest: Request { + let method: HTTPMethod = .post + let path = "/v1/edits" + let body: Data? + + init( + model: String, + input: String, + instruction: String, + n: Int, + temperature: Double, + topP: Double + ) throws { + + let body = Body( + model: model, + input: input, + instruction: instruction, + n: n, + temperature: temperature, + topP: topP + ) + + self.body = try Self.encoder.encode(body) + } +} + +extension CreateEditRequest { + struct Body: Encodable { + let model: String + let input: String + let instruction: String + let n: Int + let temperature: Double + let topP: Double + } +} diff --git a/ios/OpenAIKit/Edit/Edit.swift b/ios/OpenAIKit/Edit/Edit.swift new file mode 100644 index 0000000..f00f15c --- /dev/null +++ b/ios/OpenAIKit/Edit/Edit.swift @@ -0,0 +1,21 @@ +import Foundation + +/** + Given a prompt and an instruction, the model will return an edited version of the prompt. + */ +public struct Edit { + public let object: String + public let created: Date + public let usage: Usage + public let choices: [Choice] +} + +extension Edit { + public struct Choice { + public let text: String + public let index: Int + } +} + +extension Edit: Codable {} +extension Edit.Choice: Codable {} diff --git a/ios/OpenAIKit/Edit/EditProvider.swift b/ios/OpenAIKit/Edit/EditProvider.swift new file mode 100644 index 0000000..9dceae1 --- /dev/null +++ b/ios/OpenAIKit/Edit/EditProvider.swift @@ -0,0 +1,38 @@ +public struct EditProvider { + + private let requestHandler: RequestHandler + + init(requestHandler: RequestHandler) { + self.requestHandler = requestHandler + } + + /** + Create edit + POST + + https://api.openai.com/v1/edits + + Creates a new edit for the provided input, instruction, and parameters + */ + public func create( + model: ModelID = Model.GPT3.textDavinciEdit001, + input: String = "", + instruction: String, + n: Int = 1, + temperature: Double = 1.0, + topP: Double = 1.0 + ) async throws -> Edit { + + let request = try CreateEditRequest( + model: model.id, + input: input, + instruction: instruction, + n: n, + temperature: temperature, + topP: topP + ) + + return try await requestHandler.perform(request: request) + } + +} diff --git a/ios/OpenAIKit/Embedding/CreateEmbeddingRequest.swift b/ios/OpenAIKit/Embedding/CreateEmbeddingRequest.swift new file mode 100644 index 0000000..117b747 --- /dev/null +++ b/ios/OpenAIKit/Embedding/CreateEmbeddingRequest.swift @@ -0,0 +1,30 @@ +import Foundation + +struct CreateEmbeddingRequest: Request { + let method: HTTPMethod = .post + let path = "/v1/embeddings" + let body: Data? + + init( + model: String, + input: [String], + user: String? + ) throws { + + let body = Body( + model: model, + input: input, + user: user + ) + + self.body = try Self.encoder.encode(body) + } +} + +extension CreateEmbeddingRequest { + struct Body: Encodable { + let model: String + let input: [String] + let user: String? + } +} diff --git a/ios/OpenAIKit/Embedding/CreateEmbeddingResponse.swift b/ios/OpenAIKit/Embedding/CreateEmbeddingResponse.swift new file mode 100644 index 0000000..0679fe4 --- /dev/null +++ b/ios/OpenAIKit/Embedding/CreateEmbeddingResponse.swift @@ -0,0 +1,10 @@ +import Foundation + +public struct CreateEmbeddingResponse { + public let object: String + public let data: [Embedding] + public let model: String + public let usage: Usage +} + +extension CreateEmbeddingResponse: Decodable {} diff --git a/ios/OpenAIKit/Embedding/Embedding.swift b/ios/OpenAIKit/Embedding/Embedding.swift new file mode 100644 index 0000000..5275653 --- /dev/null +++ b/ios/OpenAIKit/Embedding/Embedding.swift @@ -0,0 +1,12 @@ +import Foundation + +/** + Get a vector representation of a given input that can be easily consumed by machine learning models and algorithms. + */ +public struct Embedding { + public let object: String + public let embedding: [Float] + public let index: Int +} + +extension Embedding: Codable {} diff --git a/ios/OpenAIKit/Embedding/EmbeddingProvider.swift b/ios/OpenAIKit/Embedding/EmbeddingProvider.swift new file mode 100644 index 0000000..521981f --- /dev/null +++ b/ios/OpenAIKit/Embedding/EmbeddingProvider.swift @@ -0,0 +1,48 @@ +public struct EmbeddingProvider { + + private let requestHandler: RequestHandler + + init(requestHandler: RequestHandler) { + self.requestHandler = requestHandler + } + + /** + Create embeddings + POST + + https://api.openai.com/v1/embeddings + + Creates an embedding vector representing the input text. + */ + public func create( + model: ModelID = Model.GPT3.textEmbeddingAda002, + input: [String], + user: String? = nil + ) async throws -> CreateEmbeddingResponse { + + let request = try CreateEmbeddingRequest( + model: model.id, + input: input, + user: user + ) + + return try await requestHandler.perform(request: request) + } + + + /** + Create embeddings + POST + + https://api.openai.com/v1/embeddings + + Creates an embedding vector representing the input text. + */ + public func create( + model: ModelID = Model.GPT3.textEmbeddingAda002, + input: String, + user: String? = nil + ) async throws -> CreateEmbeddingResponse { + try await create(model: model, input: [input], user: user) + } +} diff --git a/ios/OpenAIKit/Errors/APIError.swift b/ios/OpenAIKit/Errors/APIError.swift new file mode 100644 index 0000000..ba5b55c --- /dev/null +++ b/ios/OpenAIKit/Errors/APIError.swift @@ -0,0 +1,14 @@ +import Foundation + +public struct APIError: Error, Decodable { + public let message: String + public let type: String + public let param: String? + public let code: String? +} + +public struct APIErrorResponse: Error, Decodable { + public let error: APIError +} + + diff --git a/ios/OpenAIKit/Errors/RequestHandler+Error.swift b/ios/OpenAIKit/Errors/RequestHandler+Error.swift new file mode 100644 index 0000000..ae42c39 --- /dev/null +++ b/ios/OpenAIKit/Errors/RequestHandler+Error.swift @@ -0,0 +1,4 @@ +enum RequestHandlerError: Error { + case invalidURLGenerated + case responseBodyMissing +} diff --git a/ios/OpenAIKit/File/File.swift b/ios/OpenAIKit/File/File.swift new file mode 100644 index 0000000..3d0df7a --- /dev/null +++ b/ios/OpenAIKit/File/File.swift @@ -0,0 +1,26 @@ +import Foundation + +/** + Files are used to upload documents that can be used with features like Fine-tuning. + */ +public struct File { + public let id: String + public let bytes: Int + public let createdAt: Date + public let filename: String + public let object: String + public let owner: String? + public let purpose: Purpose +} + +extension File { + public enum Purpose: String { + case fineTune = "fine-tune" + case answers + case search + case classifications + } +} + +extension File: Codable {} +extension File.Purpose: Codable {} diff --git a/ios/OpenAIKit/File/FileProvider.swift b/ios/OpenAIKit/File/FileProvider.swift new file mode 100644 index 0000000..aa50343 --- /dev/null +++ b/ios/OpenAIKit/File/FileProvider.swift @@ -0,0 +1,92 @@ +import Foundation + +public struct FileProvider { + + private let requestHandler: RequestHandler + + init(requestHandler: RequestHandler) { + self.requestHandler = requestHandler + } + + /** + List files + GET + + https://api.openai.com/v1/files + + Returns a list of files that belong to the user's organization. + */ + public func list() async throws -> [File] { + let request = ListFilesRequest() + + let response: ListFilesResponse = try await requestHandler.perform(request: request) + + return response.data + } + + /** + Upload file + POST + + https://api.openai.com/v1/files + + Upload a file that contains document(s) to be used across various endpoints/features. Currently, the size of all the files uploaded by one organization can be up to 1 GB. Please contact us if you need to increase the storage limit. + */ + public func upload( + file: Data, + fileName: String = "data.jsonl", + purpose: File.Purpose + ) async throws -> File { + + let request = UploadFileRequest( + file: file, + fileName: fileName, + purpose: purpose + ) + + return try await requestHandler.perform(request: request) + } + + /** + Delete file + DELETE + + https://api.openai.com/v1/files/{file_id} + + Delete a file. + */ + public func delete(id: String) async throws -> DeleteFileResponse { + let request = DeleteFileRequest(id: id) + + return try await requestHandler.perform(request: request) + } + + /** + Retrieve file + GET + + https://api.openai.com/v1/files/{file_id} + + Returns information about a specific file. + */ + public func retrieve(id: String) async throws -> File { + let request = RetrieveFileRequest(id: id) + + return try await requestHandler.perform(request: request) + } + + /** + Retrieve file content + GET + + https://api.openai.com/v1/files/{file_id}/content + + Returns the contents of the specified file + */ + public func retrieveFileContent(id: String) async throws -> T { + let request = RetrieveFileContentRequest(id: id) + + return try await requestHandler.perform(request: request) + } + +} diff --git a/ios/OpenAIKit/File/Request/DeleteFileRequest.swift b/ios/OpenAIKit/File/Request/DeleteFileRequest.swift new file mode 100644 index 0000000..252d0bd --- /dev/null +++ b/ios/OpenAIKit/File/Request/DeleteFileRequest.swift @@ -0,0 +1,11 @@ +import Foundation + +struct DeleteFileRequest: Request { + let method: HTTPMethod = .delete + let path: String + + init(id: String) { + self.path = "/v1/files/\(id)" + } +} + diff --git a/ios/OpenAIKit/File/Request/ListFilesRequest.swift b/ios/OpenAIKit/File/Request/ListFilesRequest.swift new file mode 100644 index 0000000..1ceb96c --- /dev/null +++ b/ios/OpenAIKit/File/Request/ListFilesRequest.swift @@ -0,0 +1,7 @@ +import Foundation + +struct ListFilesRequest: Request { + let method: HTTPMethod = .get + let path = "/v1/files" +} + diff --git a/ios/OpenAIKit/File/Request/RetrieveFileContentRequest.swift b/ios/OpenAIKit/File/Request/RetrieveFileContentRequest.swift new file mode 100644 index 0000000..70bafbd --- /dev/null +++ b/ios/OpenAIKit/File/Request/RetrieveFileContentRequest.swift @@ -0,0 +1,11 @@ +import Foundation + +struct RetrieveFileContentRequest: Request { + let method: HTTPMethod = .get + let path: String + + init(id: String) { + self.path = "/v1/files/\(id)/content" + } +} + diff --git a/ios/OpenAIKit/File/Request/RetrieveFileRequest.swift b/ios/OpenAIKit/File/Request/RetrieveFileRequest.swift new file mode 100644 index 0000000..eded987 --- /dev/null +++ b/ios/OpenAIKit/File/Request/RetrieveFileRequest.swift @@ -0,0 +1,12 @@ +import Foundation + +struct RetrieveFileRequest: Request { + let method: HTTPMethod = .get + let path: String + + init(id: String) { + self.path = "/v1/files/\(id)" + } +} + + diff --git a/ios/OpenAIKit/File/Request/UploadFileRequest.swift b/ios/OpenAIKit/File/Request/UploadFileRequest.swift new file mode 100644 index 0000000..232328f --- /dev/null +++ b/ios/OpenAIKit/File/Request/UploadFileRequest.swift @@ -0,0 +1,34 @@ +import Foundation + +struct UploadFileRequest: Request { + let method: HTTPMethod = .post + let path = "/v1/files" + let body: Data? + private let boundary = UUID().uuidString + + var headers: HTTPHeaders { + var headers = HTTPHeaders() + headers.add(name: .contentType, value: "multipart/form-data; boundary=\(boundary)") + return headers + } + + init( + file: Data, + fileName: String, + purpose: File.Purpose + ) { + let builder = MultipartFormDataBuilder(boundary: boundary) + + builder.addDataField( + fieldName: "file", + fileName: fileName, + data: file, + mimeType: MIMEType.File.json.rawValue + ) + + builder.addTextField(named: "purpose", value: purpose.rawValue) + + self.body = builder.build() + } +} + diff --git a/ios/OpenAIKit/File/Response/DeleteFileResponse.swift b/ios/OpenAIKit/File/Response/DeleteFileResponse.swift new file mode 100644 index 0000000..362f491 --- /dev/null +++ b/ios/OpenAIKit/File/Response/DeleteFileResponse.swift @@ -0,0 +1,9 @@ +import Foundation + +public struct DeleteFileResponse { + public let id: String + public let object: String + public let deleted: Bool +} + +extension DeleteFileResponse: Decodable {} diff --git a/ios/OpenAIKit/File/Response/ListFilesResponse.swift b/ios/OpenAIKit/File/Response/ListFilesResponse.swift new file mode 100644 index 0000000..cf16e07 --- /dev/null +++ b/ios/OpenAIKit/File/Response/ListFilesResponse.swift @@ -0,0 +1,5 @@ +import Foundation + +struct ListFilesResponse: Decodable { + let data: [File] +} diff --git a/ios/OpenAIKit/Image/Image.swift b/ios/OpenAIKit/Image/Image.swift new file mode 100644 index 0000000..de9e18b --- /dev/null +++ b/ios/OpenAIKit/Image/Image.swift @@ -0,0 +1,17 @@ +import Foundation + +public struct Image { + public let url: String +} + +extension Image: Decodable {} + +extension Image { + public enum Size: String { + case twoFiftySix = "256x256" + case fiveTwelve = "512x512" + case tenTwentyFour = "1024x1024" + } +} + +extension Image.Size: Codable {} diff --git a/ios/OpenAIKit/Image/ImageProvider.swift b/ios/OpenAIKit/Image/ImageProvider.swift new file mode 100644 index 0000000..94af098 --- /dev/null +++ b/ios/OpenAIKit/Image/ImageProvider.swift @@ -0,0 +1,90 @@ +import Foundation + +public struct ImageProvider { + + private let requestHandler: RequestHandler + + init(requestHandler: RequestHandler) { + self.requestHandler = requestHandler + } + + /** + Create image + POST + + https://api.openai.com/v1/images/generations + + Creates an image given a prompt. + */ + public func create( + prompt: String, + n: Int = 1, + size: Image.Size = .tenTwentyFour, + user: String? = nil + ) async throws -> ImageResponse { + + let request = try CreateImageRequest( + prompt: prompt, + n: n, + size: size, + user: user + ) + + return try await requestHandler.perform(request: request) + } + + /** + Create image edit + POST + + https://api.openai.com/v1/images/edits + + Creates an edited or extended image given an original image and a prompt. + */ + public func createEdit( + image: Data, + mask: Data? = nil, + prompt: String, + n: Int = 1, + size: Image.Size = .tenTwentyFour, + user: String? = nil + ) async throws -> ImageResponse { + + let request = try CreateImageEditRequest( + image: image, + mask: mask, + prompt: prompt, + n: n, + size: size, + user: user + ) + + return try await requestHandler.perform(request: request) + } + + /** + Create image variation + POST + + https://api.openai.com/v1/images/variations + + Creates a variation of a given image. + */ + public func createVariation( + image: Data, + n: Int = 1, + size: Image.Size = .tenTwentyFour, + user: String? = nil + ) async throws -> ImageResponse { + + let request = try CreateImageVariationRequest( + image: image, + n: n, + size: size, + user: user + ) + + return try await requestHandler.perform(request: request) + } + +} diff --git a/ios/OpenAIKit/Image/ImageResponse.swift b/ios/OpenAIKit/Image/ImageResponse.swift new file mode 100644 index 0000000..e11c0ce --- /dev/null +++ b/ios/OpenAIKit/Image/ImageResponse.swift @@ -0,0 +1,8 @@ +import Foundation + +public struct ImageResponse { + public let created: Date + public let data: [Image] +} + +extension ImageResponse: Decodable {} diff --git a/ios/OpenAIKit/Image/Requests/CreateImageEditRequest.swift b/ios/OpenAIKit/Image/Requests/CreateImageEditRequest.swift new file mode 100644 index 0000000..4a0777a --- /dev/null +++ b/ios/OpenAIKit/Image/Requests/CreateImageEditRequest.swift @@ -0,0 +1,52 @@ +import Foundation + +struct CreateImageEditRequest: Request { + let method: HTTPMethod = .post + let path = "/v1/images/edits" + let body: Data? + private let boundary = UUID().uuidString + + var headers: HTTPHeaders { + var headers = HTTPHeaders() + headers.add(name: .contentType, value: "multipart/form-data; boundary=\(boundary)") + return headers + } + + init( + image: Data, + mask: Data?, + prompt: String, + n: Int, + size: Image.Size, + user: String? + ) throws { + + let builder = MultipartFormDataBuilder(boundary: boundary) + + builder.addDataField( + fieldName: "image", + fileName: "image.png", + data: image, + mimeType: "image/png" + ) + + if let mask = mask { + builder.addDataField( + fieldName: "mask", + fileName: "mask.png", + data: mask, + mimeType: "image/png" + ) + } + + builder.addTextField(named: "prompt", value: prompt) + builder.addTextField(named: "n", value: "\(n)") + builder.addTextField(named: "size", value: size.rawValue) + + if let user = user { + builder.addTextField(named: "user", value: user) + } + + self.body = builder.build() + } +} diff --git a/ios/OpenAIKit/Image/Requests/CreateImageRequest.swift b/ios/OpenAIKit/Image/Requests/CreateImageRequest.swift new file mode 100644 index 0000000..33b0d2d --- /dev/null +++ b/ios/OpenAIKit/Image/Requests/CreateImageRequest.swift @@ -0,0 +1,33 @@ +import Foundation + +struct CreateImageRequest: Request { + let method: HTTPMethod = .post + let path = "/v1/images/generations" + let body: Data? + + init( + prompt: String, + n: Int, + size: Image.Size, + user: String? + ) throws { + + let body = Body( + prompt: prompt, + n: n, + size: size, + user: user + ) + + self.body = try Self.encoder.encode(body) + } +} + +extension CreateImageRequest { + struct Body: Encodable { + let prompt: String + let n: Int + let size: Image.Size + let user: String? + } +} diff --git a/ios/OpenAIKit/Image/Requests/CreateImageVariationRequest.swift b/ios/OpenAIKit/Image/Requests/CreateImageVariationRequest.swift new file mode 100644 index 0000000..332f20f --- /dev/null +++ b/ios/OpenAIKit/Image/Requests/CreateImageVariationRequest.swift @@ -0,0 +1,40 @@ +import Foundation + +struct CreateImageVariationRequest: Request { + let method: HTTPMethod = .post + let path = "/v1/images/variations" + let body: Data? + private let boundary = UUID().uuidString + + var headers: HTTPHeaders { + var headers = HTTPHeaders() + headers.add(name: .contentType, value: "multipart/form-data; boundary=\(boundary)") + return headers + } + + init( + image: Data, + n: Int, + size: Image.Size, + user: String? + ) throws { + + let builder = MultipartFormDataBuilder(boundary: boundary) + + builder.addDataField( + fieldName: "image", + fileName: "image.png", + data: image, + mimeType: "image/png" + ) + + builder.addTextField(named: "n", value: "\(n)") + builder.addTextField(named: "size", value: size.rawValue) + + if let user = user { + builder.addTextField(named: "user", value: user) + } + + self.body = builder.build() + } +} diff --git a/ios/OpenAIKit/Language.swift b/ios/OpenAIKit/Language.swift new file mode 100644 index 0000000..302ed39 --- /dev/null +++ b/ios/OpenAIKit/Language.swift @@ -0,0 +1,360 @@ +// +// Language.swift +// +// +// Created by Dylan Shine on 3/19/23. +// + +import Foundation + +// List of languages in ISO-639-1 format. + +/// Please note all languages may not be supported by the OpenAI API currently. + +public enum Language: String { + + case abkhazian = "ab" + + case afar = "aa" + + case afrikaans = "af" + + case akan = "ak" + + case albanian = "sq" + + case amharic = "am" + + case arabic = "ar" + + case aragonese = "an" + + case armenian = "hy" + + case assamese = "as" + + case avaric = "av" + + case avestan = "ae" + + case aymara = "ay" + + case azerbaijani = "az" + + case bambara = "bm" + + case bashkir = "ba" + + case basque = "eu" + + case belarusian = "be" + + case bengali = "bn" + + case bislama = "bi" + + case bosnian = "bs" + + case breton = "br" + + case bulgarian = "bg" + + case burmese = "my" + + case catalan = "ca" + + case chamorro = "ch" + + case chechen = "ce" + + case chichewa = "ny" + + case chinese = "zh" + + case chuvash = "cv" + + case cornish = "kw" + + case corsican = "co" + + case cree = "cr" + + case croatian = "hr" + + case czech = "cs" + + case danish = "da" + + case dutch = "nl" + + case dzongkha = "dz" + + case english = "en" + + case esperanto = "eo" + + case estonian = "et" + + case ewe = "ee" + + case faroese = "fo" + + case fijian = "fj" + + case finnish = "fi" + + case french = "fr" + + case fulah = "ff" + + case gaelic = "gd" + + case galician = "gl" + + case ganda = "lg" + + case georgian = "ka" + + case german = "de" + + case greek = "el" + + case kalaallisut = "kl" + + case guarani = "gn" + + case gujarati = "gu" + + case haitian = "ht" + + case hausa = "ha" + + case hebrew = "he" + + case herero = "hz" + + case hindi = "hi" + + case hungarian = "hu" + + case icelandic = "is" + + case ido = "io" + + case igbo = "ig" + + case indonesian = "id" + + case interlingue = "ie" + + case inuktitut = "iu" + + case inupiaq = "ik" + + case irish = "ga" + + case italian = "it" + + case japanese = "ja" + + case javanese = "jv" + + case kannada = "kn" + + case kanuri = "kr" + + case kashmiri = "ks" + + case kazakh = "kk" + + case kikuyu = "ki" + + case kinyarwanda = "rw" + + case kirghiz = "ky" + + case komi = "kv" + + case kongo = "kg" + + case korean = "ko" + + case kuanyama = "kj" + + case kurdish = "ku" + + case lao = "lo" + + case latin = "la" + + case latvian = "lv" + + case limburgen = "li" + + case lingala = "ln" + + case lithuanian = "lt" + + case lubaKatanga = "lu" + + case luxembourgish = "lb" + + case macedonian = "mk" + + case malagasy = "mg" + + case malay = "ms" + + case malayalam = "ml" + + case maltese = "mt" + + case manx = "gv" + + case maori = "mi" + + case marathi = "mr" + + case marshallese = "mh" + + case mongolian = "mn" + + case nauru = "na" + + case navajo = "nv" + + case ndonga = "ng" + + case nepali = "ne" + + case norwegian = "no" + + case sichuanYi = "ii" + + case occitan = "oc" + + case ojibwa = "oj" + + case oriya = "or" + + case oromo = "om" + + case ossetian = "os" + + case pali = "pi" + + case pashto = "ps" + + case persian = "fa" + + case polish = "pl" + + case portuguese = "pt" + + case punjabi = "pa" + + case quechua = "qu" + + case romanian = "ro" + + case romansh = "rm" + + case rundi = "rn" + + case russian = "ru" + + case samoan = "sm" + + case sango = "sg" + + case sanskrit = "sa" + + case sardinian = "sc" + + case serbian = "sr" + + case shona = "sn" + + case sindhi = "sd" + + case sinhala = "si" + + case slovak = "sk" + + case slovenian = "sl" + + case somali = "so" + + case spanish = "es" + + case sundanese = "su" + + case swahili = "sw" + + case swati = "ss" + + case swedish = "sv" + + case tagalog = "tl" + + case tahitian = "ty" + + case tajik = "tg" + + case tamil = "ta" + + case tatar = "tt" + + case telugu = "te" + + case thai = "th" + + case tibetan = "bo" + + case tigrinya = "ti" + + case tsonga = "ts" + + case tswana = "tn" + + case turkish = "tr" + + case turkmen = "tk" + + case twi = "tw" + + case uighur = "ug" + + case ukrainian = "uk" + + case urdu = "ur" + + case uzbek = "uz" + + case venda = "ve" + + case vietnamese = "vi" + + case volapük = "vo" + + case walloon = "wa" + + case welsh = "cy" + + case wolof = "wo" + + case xhosa = "xh" + + case yiddish = "yi" + + case yoruba = "yo" + + case zhuang = "za" + + case zulu = "zu" + +} + + + +extension Language: Codable {} diff --git a/ios/OpenAIKit/MIMEType.swift b/ios/OpenAIKit/MIMEType.swift new file mode 100644 index 0000000..d12a822 --- /dev/null +++ b/ios/OpenAIKit/MIMEType.swift @@ -0,0 +1,28 @@ +// +// MIMEType.swift +// +// +// Created by Dylan Shine on 3/19/23. +// + +import Foundation + +public enum MIMEType { + + public enum File: String { + case json = "application/json" + } + + public enum Audio: String { + case mpeg = "audio/mpeg" + case mp4 = "audio/mp4" + case wav = "audio/wav" + case webm = "audio/webm" + case m4a = "audio/m4a" + } + +} + +extension MIMEType.File: Codable {} + +extension MIMEType.Audio: Codable {} diff --git a/ios/OpenAIKit/Model/Model.swift b/ios/OpenAIKit/Model/Model.swift new file mode 100644 index 0000000..f7cc45c --- /dev/null +++ b/ios/OpenAIKit/Model/Model.swift @@ -0,0 +1,81 @@ +import Foundation + +/** + List and describe the various models available in the API. + */ +public struct Model: Codable { + public let id: String + public let object: String + public let created: Date + public let ownedBy: String + public let permission: [Permission] + public let root: String + public let parent: String? +} + +extension Model { + public struct Permission: Codable { + public let id: String + public let object: String + public let created: Date + public let allowCreateEngine: Bool + public let allowSampling: Bool + public let allowLogprobs: Bool + public let allowSearchIndices: Bool + public let allowView: Bool + public let allowFineTuning: Bool + public let organization: String + public let group: String? + public let isBlocking: Bool + } +} + +public protocol ModelID { + var id: String { get } +} + +extension Model { + public enum GPT4: String, ModelID { + case gpt4 = "gpt-4" + case gpt40314 = "gpt-4-0314" + case gpt4_32k = "gpt-4-32k" + case gpt4_32k0314 = "gpt-4-32k-0314" + } + + public enum GPT3: String, ModelID { + case gpt3_5Turbo = "gpt-3.5-turbo" + case gpt3_5Turbo16K = "gpt-3.5-turbo-16k" + case gpt3_5Turbo0301 = "gpt-3.5-turbo-0301" + case textDavinci003 = "text-davinci-003" + case textDavinci002 = "text-davinci-002" + case textCurie001 = "text-curie-001" + case textBabbage001 = "text-babbage-001" + case textAda001 = "text-ada-001" + case textEmbeddingAda002 = "text-embedding-ada-002" + case textDavinci001 = "text-davinci-001" + case textDavinciEdit001 = "text-davinci-edit-001" + case davinciInstructBeta = "davinci-instruct-beta" + case davinci + case curieInstructBeta = "curie-instruct-beta" + case curie + case ada + case babbage + } + + public enum Codex: String, ModelID { + case codeDavinci002 = "code-davinci-002" + case codeCushman001 = "code-cushman-001" + case codeDavinci001 = "code-davinci-001" + case codeDavinciEdit001 = "code-davinci-edit-001" + } + + public enum Whisper: String, ModelID { + case whisper1 = "whisper-1" + } +} + +extension RawRepresentable where RawValue == String { + public var id: String { + rawValue + } +} diff --git a/ios/OpenAIKit/Model/Provider/ModelProvider.swift b/ios/OpenAIKit/Model/Provider/ModelProvider.swift new file mode 100644 index 0000000..81e0914 --- /dev/null +++ b/ios/OpenAIKit/Model/Provider/ModelProvider.swift @@ -0,0 +1,39 @@ +public struct ModelProvider { + + private let requestHandler: RequestHandler + + init(requestHandler: RequestHandler) { + self.requestHandler = requestHandler + } + + /** + List models + GET + + https://api.openai.com/v1/models + + Lists the currently available models, and provides basic information about each one such as the owner and availability. + */ + public func list() async throws -> [Model] { + let request = ListModelsRequest() + + let response: ListModelsResponse = try await requestHandler.perform(request: request) + + return response.data + } + + /** + Retrieve model + GET + + https://api.openai.com/v1/models/{model} + + Retrieves a model instance, providing basic information about the model such as the owner and permissioning. + */ + public func retrieve(id: String) async throws -> Model { + let request = RetrieveModelRequest(id: id) + + return try await requestHandler.perform(request: request) + } + +} diff --git a/ios/OpenAIKit/Model/Request/ListModelsRequest.swift b/ios/OpenAIKit/Model/Request/ListModelsRequest.swift new file mode 100644 index 0000000..93dbede --- /dev/null +++ b/ios/OpenAIKit/Model/Request/ListModelsRequest.swift @@ -0,0 +1,7 @@ +import Foundation + +struct ListModelsRequest: Request { + let method: HTTPMethod = .get + let path = "/v1/models" +} + diff --git a/ios/OpenAIKit/Model/Request/RetrieveModelRequest.swift b/ios/OpenAIKit/Model/Request/RetrieveModelRequest.swift new file mode 100644 index 0000000..dd3aeb0 --- /dev/null +++ b/ios/OpenAIKit/Model/Request/RetrieveModelRequest.swift @@ -0,0 +1,12 @@ +import Foundation + +struct RetrieveModelRequest: Request { + let method: HTTPMethod = .get + let path: String + + init(id: String) { + self.path = "/v1/models/\(id)" + } +} + + diff --git a/ios/OpenAIKit/Model/Response/ListModelsResponse.swift b/ios/OpenAIKit/Model/Response/ListModelsResponse.swift new file mode 100644 index 0000000..717ed3e --- /dev/null +++ b/ios/OpenAIKit/Model/Response/ListModelsResponse.swift @@ -0,0 +1,5 @@ +import Foundation + +struct ListModelsResponse: Decodable { + let data: [Model] +} diff --git a/ios/OpenAIKit/Moderation/CreateModerationRequest.swift b/ios/OpenAIKit/Moderation/CreateModerationRequest.swift new file mode 100644 index 0000000..c16ab7e --- /dev/null +++ b/ios/OpenAIKit/Moderation/CreateModerationRequest.swift @@ -0,0 +1,27 @@ +import Foundation + +struct CreateModerationRequest: Request { + let method: HTTPMethod = .post + let path = "/v1/moderations" + let body: Data? + + init( + input: String, + model: Moderation.Model + ) throws { + + let body = Body( + input: input, + model: model + ) + + self.body = try Self.encoder.encode(body) + } +} + +extension CreateModerationRequest { + struct Body: Encodable { + let input: String + let model: Moderation.Model + } +} diff --git a/ios/OpenAIKit/Moderation/Moderation.swift b/ios/OpenAIKit/Moderation/Moderation.swift new file mode 100644 index 0000000..4e11963 --- /dev/null +++ b/ios/OpenAIKit/Moderation/Moderation.swift @@ -0,0 +1,86 @@ +import Foundation + +/** + Moderations + Given a input text, outputs if the model classifies it as violating OpenAI's content policy. + */ + +public struct Moderation { + public let id: String + public let model: String + public let results: [Moderation.Result] +} + +extension Moderation { + public struct Result { + public let categories: Categories + public let categoryScores: CategoryScores + public let flagged: Bool + } + + public enum Model: String, Codable { + case latest = "text-moderation-latest" + case stable = "text-moderation-stable" + } +} + +extension Moderation.Result { + public struct Categories { + public let hate: Bool + public let hateThreatening: Bool + public let selfHarm: Bool + public let sexual: Bool + public let sexualMinors: Bool + public let violence: Bool + public let violenceGraphic: Bool + } + + public struct CategoryScores { + public let hate: Float + public let hateThreatening: Float + public let selfHarm: Float + public let sexual: Float + public let sexualMinors: Float + public let violence: Float + public let violenceGraphic: Float + } +} + +extension Moderation: Decodable {} +extension Moderation.Result: Decodable {} + +enum ModerationCategoryCodingKeys: String, CodingKey { + case hate + case hateThreatening = "hate/threatening" + case selfHarm = "self-harm" + case sexual + case sexualMinors = "sexual/minors" + case violence + case violenceGraphic = "violence/graphic" +} + +extension Moderation.Result.Categories: Decodable { + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: ModerationCategoryCodingKeys.self) + self.hate = try container.decode(Bool.self, forKey: .hate) + self.hateThreatening = try container.decode(Bool.self, forKey: .hateThreatening) + self.selfHarm = try container.decode(Bool.self, forKey: .selfHarm) + self.sexual = try container.decode(Bool.self, forKey: .sexual) + self.sexualMinors = try container.decode(Bool.self, forKey: .sexualMinors) + self.violence = try container.decode(Bool.self, forKey: .violence) + self.violenceGraphic = try container.decode(Bool.self, forKey: .violenceGraphic) + } +} + +extension Moderation.Result.CategoryScores: Decodable { + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: ModerationCategoryCodingKeys.self) + self.hate = try container.decode(Float.self, forKey: .hate) + self.hateThreatening = try container.decode(Float.self, forKey: .hateThreatening) + self.selfHarm = try container.decode(Float.self, forKey: .selfHarm) + self.sexual = try container.decode(Float.self, forKey: .sexual) + self.sexualMinors = try container.decode(Float.self, forKey: .sexualMinors) + self.violence = try container.decode(Float.self, forKey: .violence) + self.violenceGraphic = try container.decode(Float.self, forKey: .violenceGraphic) + } +} diff --git a/ios/OpenAIKit/Moderation/ModerationProvider.swift b/ios/OpenAIKit/Moderation/ModerationProvider.swift new file mode 100644 index 0000000..896c8d1 --- /dev/null +++ b/ios/OpenAIKit/Moderation/ModerationProvider.swift @@ -0,0 +1,29 @@ +public struct ModerationProvider { + + private let requestHandler: RequestHandler + + init(requestHandler: RequestHandler) { + self.requestHandler = requestHandler + } + + /** + Create moderation + POST + + https://api.openai.com/v1/moderations + + Classifies if text violates OpenAI's Content Policy + */ + public func createModeration( + input: String, + model: Moderation.Model = .latest + ) async throws -> Moderation { + + let request = try CreateModerationRequest( + input: input, + model: model + ) + + return try await requestHandler.perform(request: request) + } +} diff --git a/ios/OpenAIKit/RequestHandler/ContentType.swift b/ios/OpenAIKit/RequestHandler/ContentType.swift new file mode 100644 index 0000000..ae4c969 --- /dev/null +++ b/ios/OpenAIKit/RequestHandler/ContentType.swift @@ -0,0 +1,147 @@ +/** + Common MIME types + Generated from https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types/Common_types + */ +public enum ContentType: String { + + /// AAC audio + case aac = "audio/aac" + + /// AVI: Audio Video Interleave + case avi = "video/x-msvideo" + + /// Any kind of binary data + case octetStream = "application/octet-stream" + + /// Bitmap Graphics + case bmp = "image/bmp" + + /// BZip archive + case bzip = "application/x-bzip" + + /// BZip2 archive + case bzip2 = "application/x-bzip2" + + /// Cascading Style Sheets (CSS) + case css = "text/css" + + /// Comma-separated values (CSV) + case csv = "text/csv" + + /// Microsoft Word + case word = "application/msword" + + /// Electronic publication (EPUB) + case epub = "application/epub+zip" + + /// GZip Compressed Archive + case gzip = "application/gzip" + + /// Graphics Interchange Format (GIF) + case gif = "image/gif" + + /// HyperText Markup Language (HTML) + case html = "text/html" + + /// iCalendar format + case iCal = "text/calendar" + + /// JPEG images + case jpeg = "image/jpeg" + + /// JavaScript + case javascript = "text/javascript" + + /// JSON format + case json = "application/json" + + /// Musical Instrument Digital Interface (MIDI) + case midi = "audio/midi" + + /// MP3 audio + case mpegAudio = "audio/mpeg" + + /// MPEG Video + case mpegVideo = "video/mpeg" + + /// OGG audio + case oggAudio = "audio/ogg" + + /// OGG video + case oggVideo = "video/ogg" + + /// OGG + case ogg = "application/ogg" + + /// Opus audio + case opusAudio = "audio/opus" + + /// OpenType font + case openType = "font/otf" + + /// Portable Network Graphics + case png = "image/png" + + /// Adobe Portable Document Format (PDF) + case pdf = "application/pdf" + + /// Rich Text Format (RTF) + case rtf = "application/rtf" + + /// Scalable Vector Graphics (SVG) + case svg = "image/svg+xml" + + /// Small web format (SWF) or Adobe Flash document + case swf = "application/x-shockwave-flash" + + /// Tape Archive (TAR) + case tar = "application/x-tar" + + /// Tagged Image File Format (TIFF) + case tiff = "image/tiff" + + /// MPEG transport stream + case mpegStream = "video/mp2t" + + /// TrueType Font + case ttf = "font/ttf" + + /// Text + case text = "text/plain" + + /// Waveform Audio Format + case wav = "audio/wav" + + /// WEBM audio + case webmAudio = "audio/webm" + + /// WEBM video + case webmVideo = "video/webm" + + /// WEBP image + case webp = "image/webp" + + /// Web Open Font Format (WOFF) + case woff = "font/woff" + + /// Web Open Font Format (WOFF) + case woff2 = "font/woff2" + + /// XHTML + case xhtml = "application/xhtml+xml" + + /// XML + case xml = "application/xml" + + /// XML + case xmlText = "text/xml" + + /// ZIP archive + case zip = "application/zip" + + // HTTP form: keys and values URL-encoded in key-value tuples separated by '&' + case formUrlEncoded = "application/x-www-form-urlencoded" + + // HTTP form: each value is sent as a block of data + case formDataMultipart = "multipart/form-data" +} diff --git a/ios/OpenAIKit/RequestHandler/HTTPHeaders.swift b/ios/OpenAIKit/RequestHandler/HTTPHeaders.swift new file mode 100644 index 0000000..68ff397 --- /dev/null +++ b/ios/OpenAIKit/RequestHandler/HTTPHeaders.swift @@ -0,0 +1,186 @@ +/** + HTTP header fields + Generated from: https://en.wikipedia.org/wiki/List_of_HTTP_header_fields#Standard_request_fields + */ + +public enum HTTPHeader: String { + + /// Acceptable instance-manipulations for the request. + /// For example: `A-IM: feed` + case aIM = "A-IM" + + /// Media type(s) that is/are acceptable for the response. See Content negotiation. + /// For example: `Accept: text/html` + case accept = "Accept" + + /// Character sets that are acceptable. + /// For example: `Accept-Charset: utf-8` + case acceptCharset = "Accept-Charset" + + /// Acceptable version in time. + /// For example: `Accept-Datetime: Thu, 31 May 2007 20:35:00 GMT` + case acceptDatetime = "Accept-Datetime" + + /// List of acceptable encodings. See HTTP compression. + /// For example: `Accept-Encoding: gzip, deflate` + case acceptEncoding = "Accept-Encoding" + + /// List of acceptable human languages for response. See Content negotiation. + /// For example: `Accept-Language: en-US` + case acceptLanguage = "Accept-Language" + + /// Initiates a request for cross-origin resource sharing with Origin (below). + /// For example: `Access-Control-Request-Method: GET` + case accessControlRequestMethod = "Access-Control-Request-Method" + case accessControlRequestHeaders = "Access-Control-Request-Headers" + + /// Authentication credentials for HTTP authentication. + /// For example: `Authorization: Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==` + case authorization = "Authorization" + + /// Used to specify directives that must be obeyed by all caching mechanisms along the request-response chain. + /// For example: `Cache-Control: no-cache` + case cacheControl = "Cache-Control" + + /// Control options for the current connection and list of hop-by-hop request fields. Must not be used with HTTP/2. + /// For example: `Connection: keep-alive Connection: Upgrade` + case connection = "Connection" + + /// The type of encoding used on the data. See HTTP compression. + /// For example: `Content-Encoding: gzip` + case contentEncoding = "Content-Encoding" + + /// The length of the request body in octets (8-bit bytes). + /// For example: `Content-Length: 348` + case contentLength = "Content-Length" + + /// A Base64-encoded binary MD5 sum of the content of the request body. + /// For example: `Content-MD5: Q2hlY2sgSW50ZWdyaXR5IQ==` + case contentMD5 = "Content-MD5" + + /// The Media type of the body of the request (used with POST and PUT requests). + /// For example: `Content-Type: application/x-www-form-urlencoded` + case contentType = "Content-Type" + + /// An HTTP cookie previously sent by the server with Set-Cookie (below). + /// For example: `Cookie: $Version=1; Skin=new;` + case cookie = "Cookie" + + /// The date and time at which the message was originated (in "HTTP-date" format as defined by RFC 7231 Date/Time Formats). + /// For example: `Date: Tue, 15 Nov 1994 08:12:31 GMT` + case date = "Date" + + /// Indicates that particular server behaviors are required by the client. + /// For example: `Expect: 100-continue` + case expect = "Expect" + + /// Disclose original information of a client connecting to a web server through an HTTP proxy. + /// For example: `Forwarded: for=192.0.2.60;proto=http;by=203.0.113.43 Forwarded: for=192.0.2.43, for=198.51.100.17` + case forwarded = "Forwarded" + + /// The email address of the user making the request. + /// For example: `From: user@example.com` + case from = "From" + + /// The domain name of the server (for virtual hosting), and the TCP port number on which the server is listening. The port number may be omitted if the port is the standard port for the service requested. Mandatory since HTTP/1.1. If the request is generated directly in HTTP/2, it should not be used. + /// For example: `Host: en.wikipedia.org:8080 Host: en.wikipedia.org` + case host = "Host" + + /// A request that upgrades from HTTP/1.1 to HTTP/2 MUST include exactly one HTTP2-Setting header field. The HTTP2-Settings header field is a connection-specific header field that includes parameters that govern the HTTP/2 connection, provided in anticipation of the server accepting the request to upgrade. + /// For example: `HTTP2-Settings: token64` + case http2Settings = "HTTP2-Settings" + + /// Only perform the action if the client supplied entity matches the same entity on the server. This is mainly for methods like PUT to only update a resource if it has not been modified since the user last updated it. + /// For example: `If-Match: "737060cd8c284d8af7ad3082f209582d"` + case ifMatch = "If-Match" + + /// Allows a 304 Not Modified to be returned if content is unchanged. + /// For example: `If-Modified-Since: Sat, 29 Oct 1994 19:43:31 GMT` + case ifModifiedSince = "If-Modified-Since" + + /// Allows a 304 Not Modified to be returned if content is unchanged, see HTTP ETag. + /// For example: `If-None-Match: "737060cd8c284d8af7ad3082f209582d"` + case ifNoneMatch = "If-None-Match" + + /// If the entity is unchanged, send me the part(s) that I am missing; otherwise, send me the entire new entity. + /// For example: `If-Range: "737060cd8c284d8af7ad3082f209582d"` + case ifRange = "If-Range" + + /// Only send the response if the entity has not been modified since a specific time. + /// For example: `If-Unmodified-Since: Sat, 29 Oct 1994 19:43:31 GMT` + case ifUnmodifiedSince = "If-Unmodified-Since" + + /// Limit the number of times the message can be forwarded through proxies or gateways. + /// For example: `Max-Forwards: 10` + case maxForwards = "Max-Forwards" + + /// Initiates a request for cross-origin resource sharing (asks server for Access-Control-* response fields). + /// For example: `Origin: http://www.example-social-network.com` + case origin = "Origin" + + /// Implementation-specific fields that may have various effects anywhere along the request-response chain. + /// For example: `Pragma: no-cache` + case pragma = "Pragma" + + /// Authorization credentials for connecting to a proxy. + /// For example: `Proxy-Authorization: Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==` + case proxyAuthorization = "Proxy-Authorization" + + /// Request only part of an entity. Bytes are numbered from 0. See Byte serving. + /// For example: `Range: bytes=500-999` + case range = "Range" + + /// This is the address of the previous web page from which a link to the currently requested page was followed. (The word "referrer" has been misspelled in the RFC as well as in most implementations to the point that it has become standard usage and is considered correct terminology) + /// For example: `Referer: http://en.wikipedia.org/wiki/Main_Page` + case referer = "Referer" + + /// The transfer encodings the user agent is willing to accept: the same values as for the response header field Transfer-Encoding can be used, plus the "trailers" value (related to the "chunked" transfer method) to notify the server it expects to receive additional fields in the trailer after the last, zero-sized, chunk. Only trailers is supported in HTTP/2. + /// For example: `TE: trailers, deflate` + case te = "TE" + + /// The Trailer general field value indicates that the given set of header fields is present in the trailer of a message encoded with chunked transfer coding. + /// For example: `Trailer: Max-Forwards` + case trailer = "Trailer" + + /// The form of encoding used to safely transfer the entity to the user. Currently defined methods are: chunked, compress, deflate, gzip, identity. Must not be used with HTTP/2. + /// For example: `Transfer-Encoding: chunked` + case transferEncoding = "Transfer-Encoding" + + /// The user agent string of the user agent. + /// For example: `User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:12.0) Gecko/20100101 Firefox/12.0` + case userAgent = "User-Agent" + + /// Ask the server to upgrade to another protocol. Must not be used in HTTP/2. + /// For example: `Upgrade: h2c, HTTPS/1.3, IRC/6.9, RTA/x11, websocket` + case upgrade = "Upgrade" + + /// Informs the server of proxies through which the request was sent. + /// For example: `Via: 1.0 fred, 1.1 example.com (Apache/1.1)` + case via = "Via" + + /// A general warning about possible problems with the entity body. + /// For example: `Warning: 199 Miscellaneous warning` + case warning = "Warning" + + /// Custom OpenAI header + case openAIOrganization = "OpenAI-Organization" +} + +public struct HTTPHeaders { + + public var header: [HTTPHeader: String] = [:] + + mutating func add(name: HTTPHeader, value: String) { + header[name] = value + } +} + +extension HTTPHeaders: Sequence { + public func makeIterator() -> DictionaryIterator { + return header.makeIterator() + } +} + + + + diff --git a/ios/OpenAIKit/RequestHandler/HTTPMethod.swift b/ios/OpenAIKit/RequestHandler/HTTPMethod.swift new file mode 100644 index 0000000..08eb222 --- /dev/null +++ b/ios/OpenAIKit/RequestHandler/HTTPMethod.swift @@ -0,0 +1,51 @@ +/** + Enum to represent a HTTP request method. + + Generated from https://developer.mozilla.org/de/docs/Web/HTTP/Methods + */ +public enum HTTPMethod: String { + + /** The GET method requests a representation of the specified resource. Requests using GET should only retrieve data. */ + case get = "GET" + + /** + The HEAD method asks for a response identical to that of a GET request, but without the response body. + */ + case head = "HEAD" + + /** + The POST method is used to submit an entity to the specified resource, often causing a change in state or side effects on the server. + */ + case post = "POST" + + /** + The PUT method replaces all current representations of the target resource with the request payload. + */ + case put = "PUT" + + /** + The DELETE method deletes the specified resource. + */ + case delete = "DELETE" + + /** + The CONNECT method establishes a tunnel to the server identified by the target resource. + */ + case connect = "CONNECT" + + /** + The TRACE method performs a message loop-back test along the path to the target resource. + */ + case trace = "TRACE" + + /** + The OPTIONS method is used to describe the communication options for the target resource. + */ + case options = "OPTIONS" + + /** + The PATCH method is used to apply partial modifications to a resource. + */ + case patch = "PATCH" + +} diff --git a/ios/OpenAIKit/RequestHandler/MultipartFormDataBuilder.swift b/ios/OpenAIKit/RequestHandler/MultipartFormDataBuilder.swift new file mode 100644 index 0000000..8b1c40a --- /dev/null +++ b/ios/OpenAIKit/RequestHandler/MultipartFormDataBuilder.swift @@ -0,0 +1,65 @@ +import Foundation + +struct MultipartFormDataBuilder { + private let boundary: String + private var httpBody = NSMutableData() + + init(boundary: String = UUID().uuidString) { + self.boundary = boundary + } + + func addTextField(named name: String, value: String) { + httpBody.append(textFormField(named: name, value: value)) + } + + private func textFormField(named name: String, value: String) -> Data { + var fieldString = Data("--\(boundary)\r\n".utf8) + fieldString += Data("Content-Disposition: form-data; name=\"\(name)\"\r\n".utf8) + fieldString += Data("Content-Type: text/plain; charset=ISO-8859-1\r\n".utf8) + fieldString += Data("\r\n".utf8) + fieldString += Data("\(value)\r\n".utf8) + + return fieldString + } + + func addDataField(fieldName: String, fileName: String, data: Data, mimeType: String) { + httpBody.append( + dataFormField( + fieldName: fieldName, + fileName:fileName, + data: data, + mimeType: mimeType + ) + ) + } + + private func dataFormField( + fieldName: String, + fileName: String, + data: Data, + mimeType: String + ) -> Data { + + var fieldData = Data("--\(boundary)\r\n".utf8) + + fieldData += Data("Content-Disposition: form-data; name=\"\(fieldName)\"; filename=\"\(fileName)\"\r\n".utf8) + fieldData += Data("Content-Type: \(mimeType)\r\n".utf8) + fieldData += Data("\r\n".utf8) + fieldData += data + fieldData += Data("\r\n".utf8) + return fieldData + } + + func build() -> Data { + httpBody.append(Data("--\(boundary)--".utf8)) + return httpBody as Data + } +} + +extension NSMutableData { + func appendString(_ string: String) { + if let data = string.data(using: .utf8) { + self.append(data) + } + } +} diff --git a/ios/OpenAIKit/RequestHandler/Request.swift b/ios/OpenAIKit/RequestHandler/Request.swift new file mode 100644 index 0000000..7bdc12f --- /dev/null +++ b/ios/OpenAIKit/RequestHandler/Request.swift @@ -0,0 +1,37 @@ +import Foundation + +protocol Request { + var method: HTTPMethod { get } + var scheme: API.Scheme { get } + var host: String { get } + var path: String { get } + var body: Data? { get } + var headers: HTTPHeaders { get } + var keyDecodingStrategy: JSONDecoder.KeyDecodingStrategy { get } + var dateDecodingStrategy: JSONDecoder.DateDecodingStrategy { get } +} + +extension Request { + static var encoder: JSONEncoder { .requestEncoder } + + var scheme: API.Scheme { .https } + var host: String { "api.openai.com" } + var body: Data? { nil } + + var headers: HTTPHeaders { + var headers = HTTPHeaders() + headers.add(name: .contentType, value: "application/json") + return headers + } + + var keyDecodingStrategy: JSONDecoder.KeyDecodingStrategy { .convertFromSnakeCase } + var dateDecodingStrategy: JSONDecoder.DateDecodingStrategy { .secondsSince1970 } +} + +extension JSONEncoder { + static var requestEncoder: JSONEncoder { + let encoder = JSONEncoder() + encoder.keyEncodingStrategy = .convertToSnakeCase + return encoder + } +} diff --git a/ios/OpenAIKit/RequestHandler/RequestHandler.swift b/ios/OpenAIKit/RequestHandler/RequestHandler.swift new file mode 100644 index 0000000..7f43859 --- /dev/null +++ b/ios/OpenAIKit/RequestHandler/RequestHandler.swift @@ -0,0 +1,25 @@ +import Foundation + + +protocol RequestHandler { + var configuration: Configuration { get } + func perform(request: Request) async throws -> T + func stream(request: Request) async throws -> AsyncThrowingStream +} + +extension RequestHandler { + func generateURL(for request: Request) throws -> String { + var components = URLComponents() + components.scheme = configuration.api?.scheme.value ?? request.scheme.value + components.host = configuration.api?.host ?? request.host + components.path = [configuration.api?.path, request.path] + .compactMap { $0 } + .joined() + + guard let url = components.url else { + throw RequestHandlerError.invalidURLGenerated + } + + return url.absoluteString + } +} diff --git a/ios/OpenAIKit/RequestHandler/StatusCode.swift b/ios/OpenAIKit/RequestHandler/StatusCode.swift new file mode 100644 index 0000000..be431f3 --- /dev/null +++ b/ios/OpenAIKit/RequestHandler/StatusCode.swift @@ -0,0 +1,288 @@ +import Foundation + +/// This is a list of Hypertext Transfer Protocol (HTTP) response status codes. +/// It includes codes from IETF internet standards, other IETF RFCs, other specifications, and some additional commonly used codes. +/// The first digit of the status code specifies one of five classes of response; an HTTP client must recognise these five classes at a minimum. +/// - Author: Oliver Atkinson (https://gist.github.com/ollieatkinson/322338df8a5220d649ac01ff11e7de12) +public enum HTTPStatusCode: Int, Error { + + /// The response class representation of status codes, these get grouped by their first digit. + public enum ResponseType { + + /// - informational: This class of status code indicates a provisional response, consisting only of the Status-Line and optional headers, and is terminated by an empty line. + case informational + + /// - success: This class of status codes indicates the action requested by the client was received, understood, accepted, and processed successfully. + case success + + /// - redirection: This class of status code indicates the client must take additional action to complete the request. + case redirection + + /// - clientError: This class of status code is intended for situations in which the client seems to have erred. + case clientError + + /// - serverError: This class of status code indicates the server failed to fulfill an apparently valid request. + case serverError + + /// - undefined: The class of the status code cannot be resolved. + case undefined + + /// ResponseType by HTTP status code + public init(httpStatusCode: Int) { + switch httpStatusCode { + + case 100 ..< 200: + self = .informational + + case 200 ..< 300: + self = .success + + case 300 ..< 400: + self = .redirection + + case 400 ..< 500: + self = .clientError + + case 500 ..< 600: + self = .serverError + + default: + self = .undefined + + } + + } + } + + // + // Informational - 1xx + // + + /// - continue: The server has received the request headers and the client should proceed to send the request body. + case `continue` = 100 + + /// - switchingProtocols: The requester has asked the server to switch protocols and the server has agreed to do so. + case switchingProtocols = 101 + + /// - processing: This code indicates that the server has received and is processing the request, but no response is available yet. + case processing = 102 + + // + // Success - 2xx + // + + /// - ok: Standard response for successful HTTP requests. + case ok = 200 + + /// - created: The request has been fulfilled, resulting in the creation of a new resource. + case created = 201 + + /// - accepted: The request has been accepted for processing, but the processing has not been completed. + case accepted = 202 + + /// - nonAuthoritativeInformation: The server is a transforming proxy (e.g. a Web accelerator) that received a 200 OK from its origin, but is returning a modified version of the origin's response. + case nonAuthoritativeInformation = 203 + + /// - noContent: The server successfully processed the request and is not returning any content. + case noContent = 204 + + /// - resetContent: The server successfully processed the request, but is not returning any content. + case resetContent = 205 + + /// - partialContent: The server is delivering only part of the resource (byte serving) due to a range header sent by the client. + case partialContent = 206 + + /// - multiStatus: The message body that follows is an XML message and can contain a number of separate response codes, depending on how many sub-requests were made. + case multiStatus = 207 + + /// - alreadyReported: The members of a DAV binding have already been enumerated in a previous reply to this request, and are not being included again. + case alreadyReported = 208 + + /// - IMUsed: The server has fulfilled a request for the resource, and the response is a representation of the result of one or more instance-manipulations applied to the current instance. + case IMUsed = 226 + + // + // Redirection - 3xx + // + + /// - multipleChoices: Indicates multiple options for the resource from which the client may choose + case multipleChoices = 300 + + /// - movedPermanently: This and all future requests should be directed to the given URI. + case movedPermanently = 301 + + /// - found: The resource was found. + case found = 302 + + /// - seeOther: The response to the request can be found under another URI using a GET method. + case seeOther = 303 + + /// - notModified: Indicates that the resource has not been modified since the version specified by the request headers If-Modified-Since or If-None-Match. + case notModified = 304 + + /// - useProxy: The requested resource is available only through a proxy, the address for which is provided in the response. + case useProxy = 305 + + /// - switchProxy: No longer used. Originally meant "Subsequent requests should use the specified proxy. + case switchProxy = 306 + + /// - temporaryRedirect: The request should be repeated with another URI. + case temporaryRedirect = 307 + + /// - permenantRedirect: The request and all future requests should be repeated using another URI. + case permenantRedirect = 308 + + // + // Client Error - 4xx + // + + /// - badRequest: The server cannot or will not process the request due to an apparent client error. + case badRequest = 400 + + /// - unauthorized: Similar to 403 Forbidden, but specifically for use when authentication is required and has failed or has not yet been provided. + case unauthorized = 401 + + /// - paymentRequired: The content available on the server requires payment. + case paymentRequired = 402 + + /// - forbidden: The request was a valid request, but the server is refusing to respond to it. + case forbidden = 403 + + /// - notFound: The requested resource could not be found but may be available in the future. + case notFound = 404 + + /// - methodNotAllowed: A request method is not supported for the requested resource. e.g. a GET request on a form which requires data to be presented via POST + case methodNotAllowed = 405 + + /// - notAcceptable: The requested resource is capable of generating only content not acceptable according to the Accept headers sent in the request. + case notAcceptable = 406 + + /// - proxyAuthenticationRequired: The client must first authenticate itself with the proxy. + case proxyAuthenticationRequired = 407 + + /// - requestTimeout: The server timed out waiting for the request. + case requestTimeout = 408 + + /// - conflict: Indicates that the request could not be processed because of conflict in the request, such as an edit conflict between multiple simultaneous updates. + case conflict = 409 + + /// - gone: Indicates that the resource requested is no longer available and will not be available again. + case gone = 410 + + /// - lengthRequired: The request did not specify the length of its content, which is required by the requested resource. + case lengthRequired = 411 + + /// - preconditionFailed: The server does not meet one of the preconditions that the requester put on the request. + case preconditionFailed = 412 + + /// - payloadTooLarge: The request is larger than the server is willing or able to process. + case payloadTooLarge = 413 + + /// - URITooLong: The URI provided was too long for the server to process. + case URITooLong = 414 + + /// - unsupportedMediaType: The request entity has a media type which the server or resource does not support. + case unsupportedMediaType = 415 + + /// - rangeNotSatisfiable: The client has asked for a portion of the file (byte serving), but the server cannot supply that portion. + case rangeNotSatisfiable = 416 + + /// - expectationFailed: The server cannot meet the requirements of the Expect request-header field. + case expectationFailed = 417 + + /// - teapot: This HTTP status is used as an Easter egg in some websites. + case teapot = 418 + + /// - misdirectedRequest: The request was directed at a server that is not able to produce a response. + case misdirectedRequest = 421 + + /// - unprocessableEntity: The request was well-formed but was unable to be followed due to semantic errors. + case unprocessableEntity = 422 + + /// - locked: The resource that is being accessed is locked. + case locked = 423 + + /// - failedDependency: The request failed due to failure of a previous request (e.g., a PROPPATCH). + case failedDependency = 424 + + /// - upgradeRequired: The client should switch to a different protocol such as TLS/1.0, given in the Upgrade header field. + case upgradeRequired = 426 + + /// - preconditionRequired: The origin server requires the request to be conditional. + case preconditionRequired = 428 + + /// - tooManyRequests: The user has sent too many requests in a given amount of time. + case tooManyRequests = 429 + + /// - requestHeaderFieldsTooLarge: The server is unwilling to process the request because either an individual header field, or all the header fields collectively, are too large. + case requestHeaderFieldsTooLarge = 431 + + /// - noResponse: Used to indicate that the server has returned no information to the client and closed the connection. + case noResponse = 444 + + /// - unavailableForLegalReasons: A server operator has received a legal demand to deny access to a resource or to a set of resources that includes the requested resource. + case unavailableForLegalReasons = 451 + + /// - SSLCertificateError: An expansion of the 400 Bad Request response code, used when the client has provided an invalid client certificate. + case SSLCertificateError = 495 + + /// - SSLCertificateRequired: An expansion of the 400 Bad Request response code, used when a client certificate is required but not provided. + case SSLCertificateRequired = 496 + + /// - HTTPRequestSentToHTTPSPort: An expansion of the 400 Bad Request response code, used when the client has made a HTTP request to a port listening for HTTPS requests. + case HTTPRequestSentToHTTPSPort = 497 + + /// - clientClosedRequest: Used when the client has closed the request before the server could send a response. + case clientClosedRequest = 499 + + // + // Server Error - 5xx + // + + /// - internalServerError: A generic error message, given when an unexpected condition was encountered and no more specific message is suitable. + case internalServerError = 500 + + /// - notImplemented: The server either does not recognize the request method, or it lacks the ability to fulfill the request. + case notImplemented = 501 + + /// - badGateway: The server was acting as a gateway or proxy and received an invalid response from the upstream server. + case badGateway = 502 + + /// - serviceUnavailable: The server is currently unavailable (because it is overloaded or down for maintenance). Generally, this is a temporary state. + case serviceUnavailable = 503 + + /// - gatewayTimeout: The server was acting as a gateway or proxy and did not receive a timely response from the upstream server. + case gatewayTimeout = 504 + + /// - HTTPVersionNotSupported: The server does not support the HTTP protocol version used in the request. + case HTTPVersionNotSupported = 505 + + /// - variantAlsoNegotiates: Transparent content negotiation for the request results in a circular reference. + case variantAlsoNegotiates = 506 + + /// - insufficientStorage: The server is unable to store the representation needed to complete the request. + case insufficientStorage = 507 + + /// - loopDetected: The server detected an infinite loop while processing the request. + case loopDetected = 508 + + /// - notExtended: Further extensions to the request are required for the server to fulfill it. + case notExtended = 510 + + /// - networkAuthenticationRequired: The client needs to authenticate to gain network access. + case networkAuthenticationRequired = 511 + + /// The class (or group) which the status code belongs to. + public var responseType: ResponseType { + ResponseType(httpStatusCode: self.rawValue) + } + +} + +extension HTTPURLResponse { + + var status: HTTPStatusCode? { + return HTTPStatusCode(rawValue: statusCode) + } + +} diff --git a/ios/OpenAIKit/RequestHandler/URLSessionRequestHandler.swift b/ios/OpenAIKit/RequestHandler/URLSessionRequestHandler.swift new file mode 100644 index 0000000..ac691c9 --- /dev/null +++ b/ios/OpenAIKit/RequestHandler/URLSessionRequestHandler.swift @@ -0,0 +1,78 @@ +import Foundation + +@available(iOS 15.0, *) +struct URLSessionRequestHandler: RequestHandler { + let session: URLSession + let configuration: Configuration + let decoder: JSONDecoder + + init( + session: URLSession, + configuration: Configuration, + decoder: JSONDecoder = JSONDecoder() + ) { + self.session = session + self.configuration = configuration + self.decoder = decoder + } + + func perform(request: Request) async throws -> T where T : Decodable { + let urlRequest = try makeUrlRequest(request: request) + let (data, _) = try await session.data(for: urlRequest) + decoder.keyDecodingStrategy = request.keyDecodingStrategy + decoder.dateDecodingStrategy = request.dateDecodingStrategy + do { + return try decoder.decode(T.self, from: data) + } catch { + throw try decoder.decode(APIErrorResponse.self, from: data) + } + } + + func stream(request: Request) async throws -> AsyncThrowingStream where T : Decodable { + var urlRequest = try makeUrlRequest(request: request) + urlRequest.timeoutInterval = 25 + decoder.keyDecodingStrategy = request.keyDecodingStrategy + decoder.dateDecodingStrategy = request.dateDecodingStrategy + + return AsyncThrowingStream { [urlRequest] continuation in + Task(priority: .userInitiated) { + do { + + let (bytes, _) = try await session.bytes(for: urlRequest) + for try await buffer in bytes.lines { + buffer + .components(separatedBy: "data: ") + .filter { $0 != "data: " } + .compactMap { + guard let data = $0.data(using: .utf8) else { return nil } + return try? decoder.decode(T.self, from: data) + } + .forEach { value in + continuation.yield(value) + } + } + continuation.finish() + } catch { + continuation.finish(throwing: error) + } + } + } + } + + private func makeUrlRequest(request: Request) throws -> URLRequest { + let urlString = try generateURL(for: request) + guard let url = URL(string: urlString) else { + throw RequestHandlerError.invalidURLGenerated + } + var urlRequest = URLRequest(url: url) + for (key, value) in configuration.headers { + urlRequest.addValue(value, forHTTPHeaderField: key.rawValue) + } + for (key, value) in request.headers { + urlRequest.addValue(value, forHTTPHeaderField: key.rawValue) + } + urlRequest.httpMethod = request.method.rawValue + urlRequest.httpBody = request.body + return urlRequest + } +} diff --git a/ios/OpenAIKit/Usage.swift b/ios/OpenAIKit/Usage.swift new file mode 100644 index 0000000..9f18c5e --- /dev/null +++ b/ios/OpenAIKit/Usage.swift @@ -0,0 +1,9 @@ +import Foundation + +public struct Usage { + public let promptTokens: Int + public let completionTokens: Int? + public let totalTokens: Int +} + +extension Usage: Codable {} diff --git a/ios/ReactNativeOpenai.swift b/ios/ReactNativeOpenai.swift index 7d9af8e..16be056 100644 --- a/ios/ReactNativeOpenai.swift +++ b/ios/ReactNativeOpenai.swift @@ -1,6 +1,6 @@ import React -import OpenAIKit +@available(iOS 15.0, *) @objc(ReactNativeOpenai) final class ReactNativeOpenai: RCTEventEmitter { @@ -9,9 +9,11 @@ final class ReactNativeOpenai: RCTEventEmitter { var payload: Any! } - private var openAI: OpenAIKit? + let urlSession = URLSession(configuration: .default) + var configuration: Configuration? + lazy var openAIClient = Client(session: urlSession, configuration: configuration!) - @objc public static var emitter: RCTEventEmitter? + @objc public static var emitter: RCTEventEmitter? private static var isInitialized = false @@ -23,10 +25,10 @@ final class ReactNativeOpenai: RCTEventEmitter { super.init() Self.emitter = self } - + @objc(initialize:organization:) public func initialize(apiKey: String, organization: String) { - self.openAI = OpenAIKit(apiToken: apiKey, organization: organization) + self.configuration = Configuration(apiKey: apiKey, organization: organization) } @objc public override func constantsToExport() -> [AnyHashable : Any]! { @@ -72,29 +74,26 @@ final class ReactNativeOpenai: RCTEventEmitter { } } +@available(iOS 15.0, *) extension ReactNativeOpenai { @objc(stream:) public func stream(prompt: String) { - guard let openAI = self.openAI else { - fatalError("OpenAI is not initialized, add initialize method to your app") - return - } - print("start streaming.....") - openAI.sendStreamChatCompletion( - newMessage: AIMessage(role: .user, content: prompt), - model: .gptV3_5(.gptTurbo), - maxTokens: 2048 - ) { result in - switch result { - case .success(let streamResult): - if let streamMessage = streamResult.message?.choices.first?.message { + Task { + do { + let completion = try await openAIClient.chats.stream( + model: Model.GPT3.gpt3_5Turbo, + messages: [.user(content: prompt)] + ) + for try await chat in completion { + if let streamMessage = chat.choices.first?.delta.content { print("Stream message: \(streamMessage)") Self.dispatch(action: Self.onMessageRecived, payload: [ - "message": streamMessage.content + "message": streamMessage ]) } - case .failure(let error): - print(error) + } + } catch { + print("j",error) } } }