From bd3d0ea578099e3cd4fb1e151b63105331e0a3bd Mon Sep 17 00:00:00 2001 From: hefengyu Date: Mon, 27 Nov 2023 17:59:03 +0800 Subject: [PATCH 01/13] Support URL with custom port --- Sources/OpenAIKit/API.swift | 3 +++ .../OpenAIKit/RequestHandler/RequestHandler.swift | 3 ++- Tests/OpenAIKitTests/RequestHandlerTests.swift | 15 +++++++++++++++ 3 files changed, 20 insertions(+), 1 deletion(-) diff --git a/Sources/OpenAIKit/API.swift b/Sources/OpenAIKit/API.swift index 0d0385a..90373a8 100644 --- a/Sources/OpenAIKit/API.swift +++ b/Sources/OpenAIKit/API.swift @@ -3,15 +3,18 @@ import Foundation public struct API { public let scheme: Scheme public let host: String + public let port: Int? public let path: String? public init( scheme: API.Scheme, host: String, + port: Int? = nil, pathPrefix path: String? = nil ) { self.scheme = scheme self.host = host + self.port = port self.path = path } } diff --git a/Sources/OpenAIKit/RequestHandler/RequestHandler.swift b/Sources/OpenAIKit/RequestHandler/RequestHandler.swift index 7f43859..9d5f8a2 100644 --- a/Sources/OpenAIKit/RequestHandler/RequestHandler.swift +++ b/Sources/OpenAIKit/RequestHandler/RequestHandler.swift @@ -15,7 +15,8 @@ extension RequestHandler { components.path = [configuration.api?.path, request.path] .compactMap { $0 } .joined() - + components.port = configuration.api?.port + guard let url = components.url else { throw RequestHandlerError.invalidURLGenerated } diff --git a/Tests/OpenAIKitTests/RequestHandlerTests.swift b/Tests/OpenAIKitTests/RequestHandlerTests.swift index a4803e5..345c21e 100644 --- a/Tests/OpenAIKitTests/RequestHandlerTests.swift +++ b/Tests/OpenAIKitTests/RequestHandlerTests.swift @@ -52,6 +52,21 @@ final class RequestHandlerTests: XCTestCase { XCTAssertEqual(url, "http://chat.openai.com/v1/test") } + func test_generateURL_configWithPort() throws { + let api = API(scheme: .http, host: "llms-se.baidu-int.com:8200", port: 8200) + let configuration = Configuration(apiKey: "TEST", api: api) + + let request = TestRequest( + scheme: .http, + host: "llms-se.baidu-int.com:8200", + path: "/v1/test" + ) + + let url = try requestHandler(configuration: configuration).generateURL(for: request) + + XCTAssertEqual(url, "http://llms-se.baidu-int.com:8200/v1/test") + } + } private struct TestRequest: Request { From d1b126d344e58e60700c2b5a4bcddcb612da9771 Mon Sep 17 00:00:00 2001 From: hefengyu Date: Mon, 27 Nov 2023 19:22:59 +0800 Subject: [PATCH 02/13] Change Default Url to chat/completions --- Sources/OpenAIKit/Completion/CompletionProvider.swift | 2 +- Sources/OpenAIKit/Completion/CreateCompletionRequest.swift | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/OpenAIKit/Completion/CompletionProvider.swift b/Sources/OpenAIKit/Completion/CompletionProvider.swift index b332536..ce51abf 100644 --- a/Sources/OpenAIKit/Completion/CompletionProvider.swift +++ b/Sources/OpenAIKit/Completion/CompletionProvider.swift @@ -10,7 +10,7 @@ public struct CompletionProvider { Create completion POST - https://api.openai.com/v1/completions + https://api.openai.com/chat/completions Creates a completion for the provided prompt and parameters */ diff --git a/Sources/OpenAIKit/Completion/CreateCompletionRequest.swift b/Sources/OpenAIKit/Completion/CreateCompletionRequest.swift index decd04e..b1f39fc 100644 --- a/Sources/OpenAIKit/Completion/CreateCompletionRequest.swift +++ b/Sources/OpenAIKit/Completion/CreateCompletionRequest.swift @@ -4,7 +4,7 @@ import Foundation struct CreateCompletionRequest: Request { let method: HTTPMethod = .POST - let path = "/v1/completions" + let path = "/chat/completions" let body: Data? init( From c24f8cfcd38734be0cdbdd5b04a65303c9978ead Mon Sep 17 00:00:00 2001 From: hefengyu Date: Mon, 27 Nov 2023 19:37:08 +0800 Subject: [PATCH 03/13] Add new GPT-4-Turbo Model --- Sources/OpenAIKit/Model/Model.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/Sources/OpenAIKit/Model/Model.swift b/Sources/OpenAIKit/Model/Model.swift index f7cc45c..0affb20 100644 --- a/Sources/OpenAIKit/Model/Model.swift +++ b/Sources/OpenAIKit/Model/Model.swift @@ -40,6 +40,7 @@ extension Model { case gpt40314 = "gpt-4-0314" case gpt4_32k = "gpt-4-32k" case gpt4_32k0314 = "gpt-4-32k-0314" + case gpt4_Turbo = "gpt-4-1106-preview" } public enum GPT3: String, ModelID { From 9e0af5a55b80327198b3fb70b313c8361b058d82 Mon Sep 17 00:00:00 2001 From: hefengyu Date: Wed, 29 Nov 2023 20:45:07 +0800 Subject: [PATCH 04/13] Add Vision Support --- Sources/OpenAIKit/Chat/ChatWithImage.swift | 128 ++++++++++++++++++ .../Chat/ChatWithImageProvider.swift | 56 ++++++++ .../Chat/CreateChatWithImageRequest.swift | 111 +++++++++++++++ Tests/OpenAIKitTests/MessageTests.swift | 42 ++++++ 4 files changed, 337 insertions(+) create mode 100644 Sources/OpenAIKit/Chat/ChatWithImage.swift create mode 100644 Sources/OpenAIKit/Chat/ChatWithImageProvider.swift create mode 100644 Sources/OpenAIKit/Chat/CreateChatWithImageRequest.swift diff --git a/Sources/OpenAIKit/Chat/ChatWithImage.swift b/Sources/OpenAIKit/Chat/ChatWithImage.swift new file mode 100644 index 0000000..2cc5672 --- /dev/null +++ b/Sources/OpenAIKit/Chat/ChatWithImage.swift @@ -0,0 +1,128 @@ +// +// File.swift +// +// +// Created by 贺峰煜 on 2023/11/29. +// + +import Foundation + +public struct ChatWithImage { + 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 ChatWithImage: Codable {} + +extension ChatWithImage { + public struct Choice { + public let index: Int + public let message: Message + public let finishReason: FinishReason? + } +} + +extension ChatWithImage.Choice: Codable {} + +extension ChatWithImage { + public enum Message { + case system(content: String) + case user(content: [Content]) + case assistant(content: String) + } + + public enum Content { + case text(String) + case imageUrl(String) + } +} + +extension ChatWithImage.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) + switch role { + case "system": + let content = try container.decode(String.self, forKey: .content) + self = .system(content: content) + case "user": + let content = try container.decode([ChatWithImage.Content].self, forKey: .content) + self = .user(content: content) + case "assistant": + let content = try container.decode(String.self, forKey: .content) + self = .assistant(content: content) + default: + throw DecodingError.dataCorruptedError(forKey: .role, in: container, debugDescription: "Invalid role") + } + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + switch self { + case .system(content: let content), .assistant(content: let content): + try container.encode("text", forKey: .role) + try container.encode(content, forKey: .content) + case .user(content: let content): + try container.encode("user", forKey: .role) + try container.encode(content, forKey: .content) + } + } +} + +extension ChatWithImage.Content: Codable { + private enum CodingKeys: String, CodingKey { + case type + case text + case imageUrl = "image_url" + } + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + let type = try container.decode(String.self, forKey: .type) + switch type { + case "text": + let text = try container.decode(String.self, forKey: .text) + self = .text(text) + case "image_url": + let imageUrl = try container.decode(String.self, forKey: .imageUrl) + self = .imageUrl(imageUrl) + default: + throw DecodingError.dataCorruptedError(forKey: .type, in: container, debugDescription: "Invalid type") + } + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + switch self { + case .text(let text): + try container.encode("text", forKey: .type) + try container.encode(text, forKey: .text) + case .imageUrl(let imageUrl): + try container.encode("imageurl", forKey: .type) + try container.encode(imageUrl, forKey: .imageUrl) + } + } +} + +extension ChatWithImage.Content: Equatable { + public static func ==(lhs: ChatWithImage.Content, rhs: ChatWithImage.Content) -> Bool { + switch (lhs, rhs) { + case (.text(let lhsText), .text(let rhsText)): + return lhsText == rhsText + case (.imageUrl(let lhsUrl), .imageUrl(let rhsUrl)): + return lhsUrl == rhsUrl + default: + return false + } + } +} + diff --git a/Sources/OpenAIKit/Chat/ChatWithImageProvider.swift b/Sources/OpenAIKit/Chat/ChatWithImageProvider.swift new file mode 100644 index 0000000..da64056 --- /dev/null +++ b/Sources/OpenAIKit/Chat/ChatWithImageProvider.swift @@ -0,0 +1,56 @@ +// +// ChatWithImageProvider.swift +// +// +// Created by 贺峰煜 on 2023/11/28. +// + +import Foundation + +public struct ChatWithImageProvider { + 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, + message: [ChatWithImage.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 -> ChatWithImage { + let request = try CreateChatWithImageRequest( + model: model.id, + messages: message, + 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) + } +} diff --git a/Sources/OpenAIKit/Chat/CreateChatWithImageRequest.swift b/Sources/OpenAIKit/Chat/CreateChatWithImageRequest.swift new file mode 100644 index 0000000..57b7d1c --- /dev/null +++ b/Sources/OpenAIKit/Chat/CreateChatWithImageRequest.swift @@ -0,0 +1,111 @@ +// +// File.swift +// +// +// Created by 贺峰煜 on 2023/11/29. +// + +import AsyncHTTPClient +import NIOHTTP1 +import Foundation + +struct CreateChatWithImageRequest: Request { + let method: HTTPMethod = .POST + let path: String = "/v1/chat/completions" + let body: Data? + + init( + model: String, + messages: [ChatWithImage.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 CreateChatWithImageRequest { + struct Body: Encodable { + let model: String + let messages: [ChatWithImage.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/Tests/OpenAIKitTests/MessageTests.swift b/Tests/OpenAIKitTests/MessageTests.swift index 230c018..5ad7116 100644 --- a/Tests/OpenAIKitTests/MessageTests.swift +++ b/Tests/OpenAIKitTests/MessageTests.swift @@ -70,6 +70,48 @@ final class MessageTests: XCTestCase { XCTFail("incorrect role") } } + + func testDecodingProvidedExample() throws { + let json = """ + [ + { + "role": "system", + "content": "You are Malcolm Tucker from The Thick of It, an unfriendly assistant for writing mail and explaining science and history. You write text in your voice for me." + }, + { + "role": "user", + "content": [ + { + "type": "text", + "text": "What’s in this image?" + }, + { + "type": "image_url", + "image_url": "https://upload.wikimedia.org/wikipedia/commons/thumb/d/dd/Gfp-wisconsin-madison-the-nature-boardwalk.jpg/2560px-Gfp-wisconsin-madison-the-nature-boardwalk.jpg" + } + ] + } + ] + """.data(using: .utf8)! + + let messages = try JSONDecoder().decode([ChatWithImage.Message].self, from: json) + + XCTAssertEqual(messages.count, 2) + + if case .system(let content) = messages[0] { + XCTAssertEqual(content, "You are Malcolm Tucker from The Thick of It, an unfriendly assistant for writing mail and explaining science and history. You write text in your voice for me.") + } else { + XCTFail("First Message is not a System Message") + } + + if case .user(let content) = messages[1] { + XCTAssertEqual(content.count, 2) + XCTAssertEqual(content[0], .text("What’s in this image?")) + XCTAssertEqual(content[1], .imageUrl("https://upload.wikimedia.org/wikipedia/commons/thumb/d/dd/Gfp-wisconsin-madison-the-nature-boardwalk.jpg/2560px-Gfp-wisconsin-madison-the-nature-boardwalk.jpg")) + } else { + XCTFail("Second Message is not a User Message") + } + } func testMessageRoundtrip() throws { let message = Chat.Message.system(content: "You are a helpful assistant that translates English to French.") From ae3a7d2b981f200af6e01e07b531faf30d3d5399 Mon Sep 17 00:00:00 2001 From: hefengyu Date: Wed, 29 Nov 2023 20:50:26 +0800 Subject: [PATCH 05/13] Add Vision Support --- Sources/OpenAIKit/Client/Client.swift | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Sources/OpenAIKit/Client/Client.swift b/Sources/OpenAIKit/Client/Client.swift index c848095..ff62ae0 100644 --- a/Sources/OpenAIKit/Client/Client.swift +++ b/Sources/OpenAIKit/Client/Client.swift @@ -14,6 +14,7 @@ public struct Client { public let images: ImageProvider public let models: ModelProvider public let moderations: ModerationProvider + public let chatsWithImage: ChatWithImageProvider init(requestHandler: RequestHandler) { self.audio = AudioProvider(requestHandler: requestHandler) @@ -25,6 +26,7 @@ public struct Client { self.embeddings = EmbeddingProvider(requestHandler: requestHandler) self.files = FileProvider(requestHandler: requestHandler) self.moderations = ModerationProvider(requestHandler: requestHandler) + self.chatsWithImage = ChatWithImageProvider(requesthandler: requestHandler) } public init( From dc43fb7c312429bf08f7f38e264e54eba245410b Mon Sep 17 00:00:00 2001 From: hefengyu Date: Wed, 29 Nov 2023 20:53:10 +0800 Subject: [PATCH 06/13] Add Vision Support --- Sources/OpenAIKit/Model/Model.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/Sources/OpenAIKit/Model/Model.swift b/Sources/OpenAIKit/Model/Model.swift index 0affb20..25e5e34 100644 --- a/Sources/OpenAIKit/Model/Model.swift +++ b/Sources/OpenAIKit/Model/Model.swift @@ -41,6 +41,7 @@ extension Model { case gpt4_32k = "gpt-4-32k" case gpt4_32k0314 = "gpt-4-32k-0314" case gpt4_Turbo = "gpt-4-1106-preview" + case gpt4_vision = "gpt-4-vision-preview" } public enum GPT3: String, ModelID { From 8f9076e1ff61d0805530f77835a89454ba62ce2a Mon Sep 17 00:00:00 2001 From: hefengyu Date: Wed, 29 Nov 2023 21:01:54 +0800 Subject: [PATCH 07/13] Add Vision Support --- Sources/OpenAIKit/Chat/ChatWithImage.swift | 12 +++- Tests/OpenAIKitTests/MessageTests.swift | 73 +++++++++++----------- 2 files changed, 47 insertions(+), 38 deletions(-) diff --git a/Sources/OpenAIKit/Chat/ChatWithImage.swift b/Sources/OpenAIKit/Chat/ChatWithImage.swift index 2cc5672..a570dfc 100644 --- a/Sources/OpenAIKit/Chat/ChatWithImage.swift +++ b/Sources/OpenAIKit/Chat/ChatWithImage.swift @@ -28,6 +28,12 @@ extension ChatWithImage { extension ChatWithImage.Choice: Codable {} +extension ChatWithImage { + public struct ImageUrl: Codable { + let url: String + } +} + extension ChatWithImage { public enum Message { case system(content: String) @@ -37,7 +43,7 @@ extension ChatWithImage { public enum Content { case text(String) - case imageUrl(String) + case imageUrl(ImageUrl) } } @@ -93,7 +99,7 @@ extension ChatWithImage.Content: Codable { let text = try container.decode(String.self, forKey: .text) self = .text(text) case "image_url": - let imageUrl = try container.decode(String.self, forKey: .imageUrl) + let imageUrl = try container.decode(ChatWithImage.ImageUrl.self, forKey: .imageUrl) self = .imageUrl(imageUrl) default: throw DecodingError.dataCorruptedError(forKey: .type, in: container, debugDescription: "Invalid type") @@ -119,7 +125,7 @@ extension ChatWithImage.Content: Equatable { case (.text(let lhsText), .text(let rhsText)): return lhsText == rhsText case (.imageUrl(let lhsUrl), .imageUrl(let rhsUrl)): - return lhsUrl == rhsUrl + return lhsUrl.url == rhsUrl.url default: return false } diff --git a/Tests/OpenAIKitTests/MessageTests.swift b/Tests/OpenAIKitTests/MessageTests.swift index 5ad7116..295cea0 100644 --- a/Tests/OpenAIKitTests/MessageTests.swift +++ b/Tests/OpenAIKitTests/MessageTests.swift @@ -72,46 +72,49 @@ final class MessageTests: XCTestCase { } func testDecodingProvidedExample() throws { - let json = """ - [ - { - "role": "system", - "content": "You are Malcolm Tucker from The Thick of It, an unfriendly assistant for writing mail and explaining science and history. You write text in your voice for me." - }, - { - "role": "user", - "content": [ - { - "type": "text", - "text": "What’s in this image?" - }, - { - "type": "image_url", - "image_url": "https://upload.wikimedia.org/wikipedia/commons/thumb/d/dd/Gfp-wisconsin-madison-the-nature-boardwalk.jpg/2560px-Gfp-wisconsin-madison-the-nature-boardwalk.jpg" + let json = """ + [ + { + "role": "system", + "content": "You are Malcolm Tucker from The Thick of It, an unfriendly assistant for writing mail and explaining science and history. You write text in your voice for me." + }, + { + "role": "user", + "content": [ + { + "type": "text", + "text": "What’s in this image?" + }, + { + "type": "image_url", + "image_url": { + "url": "https://upload.wikimedia.org/wikipedia/commons/thumb/d/dd/Gfp-wisconsin-madison-the-nature-boardwalk.jpg/2560px-Gfp-wisconsin-madison-the-nature-boardwalk.jpg" } - ] - } - ] - """.data(using: .utf8)! + } + ] + } + ] + """.data(using: .utf8)! - let messages = try JSONDecoder().decode([ChatWithImage.Message].self, from: json) + let messages = try JSONDecoder().decode([ChatWithImage.Message].self, from: json) - XCTAssertEqual(messages.count, 2) - - if case .system(let content) = messages[0] { - XCTAssertEqual(content, "You are Malcolm Tucker from The Thick of It, an unfriendly assistant for writing mail and explaining science and history. You write text in your voice for me.") - } else { - XCTFail("First Message is not a System Message") - } + XCTAssertEqual(messages.count, 2) + + if case .system(let content) = messages[0] { + XCTAssertEqual(content, "You are Malcolm Tucker from The Thick of It, an unfriendly assistant for writing mail and explaining science and history. You write text in your voice for me.") + } else { + XCTFail("First Message is not a System Message") + } - if case .user(let content) = messages[1] { - XCTAssertEqual(content.count, 2) - XCTAssertEqual(content[0], .text("What’s in this image?")) - XCTAssertEqual(content[1], .imageUrl("https://upload.wikimedia.org/wikipedia/commons/thumb/d/dd/Gfp-wisconsin-madison-the-nature-boardwalk.jpg/2560px-Gfp-wisconsin-madison-the-nature-boardwalk.jpg")) - } else { - XCTFail("Second Message is not a User Message") - } + if case .user(let content) = messages[1] { + XCTAssertEqual(content.count, 2) + XCTAssertEqual(content[0], .text("What’s in this image?")) + XCTAssertEqual(content[1], .imageUrl(ChatWithImage.ImageUrl(url: "https://upload.wikimedia.org/wikipedia/commons/thumb/d/dd/Gfp-wisconsin-madison-the-nature-boardwalk.jpg/2560px-Gfp-wisconsin-madison-the-nature-boardwalk.jpg"))) + } else { + XCTFail("Second Message is not a User Message") } + } + func testMessageRoundtrip() throws { let message = Chat.Message.system(content: "You are a helpful assistant that translates English to French.") From 9b72207af0adac26c51934ab1ca50fde22d9f8fd Mon Sep 17 00:00:00 2001 From: hefengyu Date: Wed, 29 Nov 2023 21:16:43 +0800 Subject: [PATCH 08/13] Add Vision Support --- Sources/OpenAIKit/Chat/ChatWithImage.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/OpenAIKit/Chat/ChatWithImage.swift b/Sources/OpenAIKit/Chat/ChatWithImage.swift index a570dfc..d91e512 100644 --- a/Sources/OpenAIKit/Chat/ChatWithImage.swift +++ b/Sources/OpenAIKit/Chat/ChatWithImage.swift @@ -30,7 +30,7 @@ extension ChatWithImage.Choice: Codable {} extension ChatWithImage { public struct ImageUrl: Codable { - let url: String + public tlet url: String } } From f6eb7d2ae6f9a06a7f9dc30dd3907984e4866120 Mon Sep 17 00:00:00 2001 From: hefengyu Date: Wed, 29 Nov 2023 21:17:23 +0800 Subject: [PATCH 09/13] Add Vision Support --- Sources/OpenAIKit/Chat/ChatWithImage.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/OpenAIKit/Chat/ChatWithImage.swift b/Sources/OpenAIKit/Chat/ChatWithImage.swift index d91e512..284b118 100644 --- a/Sources/OpenAIKit/Chat/ChatWithImage.swift +++ b/Sources/OpenAIKit/Chat/ChatWithImage.swift @@ -30,7 +30,7 @@ extension ChatWithImage.Choice: Codable {} extension ChatWithImage { public struct ImageUrl: Codable { - public tlet url: String + public let url: String } } From aea0ce665e45a378c4a89e5799aae49647b0cc7a Mon Sep 17 00:00:00 2001 From: hefengyu Date: Wed, 29 Nov 2023 21:26:51 +0800 Subject: [PATCH 10/13] Add Vision Support --- Sources/OpenAIKit/Chat/ChatWithImage.swift | 4 ++++ Tests/OpenAIKitTests/MessageTests.swift | 2 ++ 2 files changed, 6 insertions(+) diff --git a/Sources/OpenAIKit/Chat/ChatWithImage.swift b/Sources/OpenAIKit/Chat/ChatWithImage.swift index 284b118..2f31639 100644 --- a/Sources/OpenAIKit/Chat/ChatWithImage.swift +++ b/Sources/OpenAIKit/Chat/ChatWithImage.swift @@ -31,6 +31,10 @@ extension ChatWithImage.Choice: Codable {} extension ChatWithImage { public struct ImageUrl: Codable { public let url: String + + public init(url: String) { + self.url = url + } } } diff --git a/Tests/OpenAIKitTests/MessageTests.swift b/Tests/OpenAIKitTests/MessageTests.swift index 295cea0..08cadbf 100644 --- a/Tests/OpenAIKitTests/MessageTests.swift +++ b/Tests/OpenAIKitTests/MessageTests.swift @@ -113,6 +113,8 @@ final class MessageTests: XCTestCase { } else { XCTFail("Second Message is not a User Message") } + + ChatWithImage.ImageUrl(url: <#T##String#>) } From 551db739b0861a15609eceb33739573b7d559410 Mon Sep 17 00:00:00 2001 From: hefengyu Date: Wed, 29 Nov 2023 21:44:02 +0800 Subject: [PATCH 11/13] Add Vision Support --- Sources/OpenAIKit/Chat/ChatWithImage.swift | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/Sources/OpenAIKit/Chat/ChatWithImage.swift b/Sources/OpenAIKit/Chat/ChatWithImage.swift index 2f31639..a57b738 100644 --- a/Sources/OpenAIKit/Chat/ChatWithImage.swift +++ b/Sources/OpenAIKit/Chat/ChatWithImage.swift @@ -78,8 +78,11 @@ extension ChatWithImage.Message: Codable { public func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) switch self { - case .system(content: let content), .assistant(content: let content): - try container.encode("text", forKey: .role) + case .system(content: let content): + try container.encode("system", forKey: .role) + try container.encode(content, forKey: .content) + case .assistant(content: let content): + try container.encode("assistant", forKey: .role) try container.encode(content, forKey: .content) case .user(content: let content): try container.encode("user", forKey: .role) From 7f54fdcda311e5f82ead5bf9cd673fc3999d34b5 Mon Sep 17 00:00:00 2001 From: hefengyu Date: Tue, 19 Dec 2023 21:38:38 +0800 Subject: [PATCH 12/13] fix: optimize code structure --- Sources/OpenAIKit/Chat/Chat.swift | 107 +++++++++++++ Sources/OpenAIKit/Chat/ChatProvider.swift | 32 +++- Sources/OpenAIKit/Chat/ChatWithImage.swift | 141 ------------------ .../Chat/ChatWithImageProvider.swift | 56 ------- .../Chat/CreateChatWithImageRequest.swift | 4 +- Sources/OpenAIKit/Client/Client.swift | 2 - Tests/OpenAIKitTests/MessageTests.swift | 28 +--- .../OpenAIKitTests/RequestHandlerTests.swift | 6 +- 8 files changed, 145 insertions(+), 231 deletions(-) delete mode 100644 Sources/OpenAIKit/Chat/ChatWithImage.swift delete mode 100644 Sources/OpenAIKit/Chat/ChatWithImageProvider.swift diff --git a/Sources/OpenAIKit/Chat/Chat.swift b/Sources/OpenAIKit/Chat/Chat.swift index f0dde58..b181a92 100644 --- a/Sources/OpenAIKit/Chat/Chat.swift +++ b/Sources/OpenAIKit/Chat/Chat.swift @@ -20,6 +20,14 @@ extension Chat { public let message: Message public let finishReason: FinishReason? } + + public struct ImageUrl: Codable { + public let url: String + + public init(url: String) { + self.url = url + } + } } extension Chat.Choice: Codable {} @@ -30,6 +38,17 @@ extension Chat { case user(content: String) case assistant(content: String) } + + public enum MessageWithImage { + case system(content: String) + case user(content: [Content]) + case assistant(content: String) + } + + public enum Content { + case text(String) + case imageUrl(ImageUrl) + } } extension Chat.Message: Codable { @@ -87,3 +106,91 @@ extension Chat.Message { } } } + +extension Chat.MessageWithImage: 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) + switch role { + case "system": + let content = try container.decode(String.self, forKey: .content) + self = .system(content: content) + case "user": + let content = try container.decode([Chat.Content].self, forKey: .content) + self = .user(content: content) + case "assistant": + let content = try container.decode(String.self, forKey: .content) + self = .assistant(content: content) + default: + throw DecodingError.dataCorruptedError(forKey: .role, in: container, debugDescription: "Invalid role") + } + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + switch self { + case .system(content: let content): + try container.encode("system", forKey: .role) + try container.encode(content, forKey: .content) + case .assistant(content: let content): + try container.encode("assistant", forKey: .role) + try container.encode(content, forKey: .content) + case .user(content: let content): + try container.encode("user", forKey: .role) + try container.encode(content, forKey: .content) + } + } +} + +extension Chat.Content: Codable { + private enum CodingKeys: String, CodingKey { + case type + case text + case imageUrl = "image_url" + } + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + let type = try container.decode(String.self, forKey: .type) + switch type { + case "text": + let text = try container.decode(String.self, forKey: .text) + self = .text(text) + case "image_url": + let imageUrl = try container.decode(Chat.ImageUrl.self, forKey: .imageUrl) + self = .imageUrl(imageUrl) + default: + throw DecodingError.dataCorruptedError(forKey: .type, in: container, debugDescription: "Invalid type") + } + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + switch self { + case .text(let text): + try container.encode("text", forKey: .type) + try container.encode(text, forKey: .text) + case .imageUrl(let imageUrl): + try container.encode("image_url", forKey: .type) + try container.encode(imageUrl, forKey: .imageUrl) + } + } +} + +extension Chat.Content: Equatable { + public static func ==(lhs: Chat.Content, rhs: Chat.Content) -> Bool { + switch (lhs, rhs) { + case (.text(let lhsText), .text(let rhsText)): + return lhsText == rhsText + case (.imageUrl(let lhsUrl), .imageUrl(let rhsUrl)): + return lhsUrl.url == rhsUrl.url + default: + return false + } + } +} diff --git a/Sources/OpenAIKit/Chat/ChatProvider.swift b/Sources/OpenAIKit/Chat/ChatProvider.swift index 5a6a236..7e65c70 100644 --- a/Sources/OpenAIKit/Chat/ChatProvider.swift +++ b/Sources/OpenAIKit/Chat/ChatProvider.swift @@ -44,7 +44,37 @@ public struct ChatProvider { ) return try await requestHandler.perform(request: request) - + } + + public func createWithImage( + model: ModelID, + message: [Chat.MessageWithImage] = [], + 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 CreateChatWithImageRequest( + model: model.id, + messages: message, + 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) } /** diff --git a/Sources/OpenAIKit/Chat/ChatWithImage.swift b/Sources/OpenAIKit/Chat/ChatWithImage.swift deleted file mode 100644 index a57b738..0000000 --- a/Sources/OpenAIKit/Chat/ChatWithImage.swift +++ /dev/null @@ -1,141 +0,0 @@ -// -// File.swift -// -// -// Created by 贺峰煜 on 2023/11/29. -// - -import Foundation - -public struct ChatWithImage { - 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 ChatWithImage: Codable {} - -extension ChatWithImage { - public struct Choice { - public let index: Int - public let message: Message - public let finishReason: FinishReason? - } -} - -extension ChatWithImage.Choice: Codable {} - -extension ChatWithImage { - public struct ImageUrl: Codable { - public let url: String - - public init(url: String) { - self.url = url - } - } -} - -extension ChatWithImage { - public enum Message { - case system(content: String) - case user(content: [Content]) - case assistant(content: String) - } - - public enum Content { - case text(String) - case imageUrl(ImageUrl) - } -} - -extension ChatWithImage.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) - switch role { - case "system": - let content = try container.decode(String.self, forKey: .content) - self = .system(content: content) - case "user": - let content = try container.decode([ChatWithImage.Content].self, forKey: .content) - self = .user(content: content) - case "assistant": - let content = try container.decode(String.self, forKey: .content) - self = .assistant(content: content) - default: - throw DecodingError.dataCorruptedError(forKey: .role, in: container, debugDescription: "Invalid role") - } - } - - public func encode(to encoder: Encoder) throws { - var container = encoder.container(keyedBy: CodingKeys.self) - switch self { - case .system(content: let content): - try container.encode("system", forKey: .role) - try container.encode(content, forKey: .content) - case .assistant(content: let content): - try container.encode("assistant", forKey: .role) - try container.encode(content, forKey: .content) - case .user(content: let content): - try container.encode("user", forKey: .role) - try container.encode(content, forKey: .content) - } - } -} - -extension ChatWithImage.Content: Codable { - private enum CodingKeys: String, CodingKey { - case type - case text - case imageUrl = "image_url" - } - - public init(from decoder: Decoder) throws { - let container = try decoder.container(keyedBy: CodingKeys.self) - let type = try container.decode(String.self, forKey: .type) - switch type { - case "text": - let text = try container.decode(String.self, forKey: .text) - self = .text(text) - case "image_url": - let imageUrl = try container.decode(ChatWithImage.ImageUrl.self, forKey: .imageUrl) - self = .imageUrl(imageUrl) - default: - throw DecodingError.dataCorruptedError(forKey: .type, in: container, debugDescription: "Invalid type") - } - } - - public func encode(to encoder: Encoder) throws { - var container = encoder.container(keyedBy: CodingKeys.self) - switch self { - case .text(let text): - try container.encode("text", forKey: .type) - try container.encode(text, forKey: .text) - case .imageUrl(let imageUrl): - try container.encode("imageurl", forKey: .type) - try container.encode(imageUrl, forKey: .imageUrl) - } - } -} - -extension ChatWithImage.Content: Equatable { - public static func ==(lhs: ChatWithImage.Content, rhs: ChatWithImage.Content) -> Bool { - switch (lhs, rhs) { - case (.text(let lhsText), .text(let rhsText)): - return lhsText == rhsText - case (.imageUrl(let lhsUrl), .imageUrl(let rhsUrl)): - return lhsUrl.url == rhsUrl.url - default: - return false - } - } -} - diff --git a/Sources/OpenAIKit/Chat/ChatWithImageProvider.swift b/Sources/OpenAIKit/Chat/ChatWithImageProvider.swift deleted file mode 100644 index da64056..0000000 --- a/Sources/OpenAIKit/Chat/ChatWithImageProvider.swift +++ /dev/null @@ -1,56 +0,0 @@ -// -// ChatWithImageProvider.swift -// -// -// Created by 贺峰煜 on 2023/11/28. -// - -import Foundation - -public struct ChatWithImageProvider { - 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, - message: [ChatWithImage.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 -> ChatWithImage { - let request = try CreateChatWithImageRequest( - model: model.id, - messages: message, - 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) - } -} diff --git a/Sources/OpenAIKit/Chat/CreateChatWithImageRequest.swift b/Sources/OpenAIKit/Chat/CreateChatWithImageRequest.swift index 57b7d1c..5ae5e36 100644 --- a/Sources/OpenAIKit/Chat/CreateChatWithImageRequest.swift +++ b/Sources/OpenAIKit/Chat/CreateChatWithImageRequest.swift @@ -16,7 +16,7 @@ struct CreateChatWithImageRequest: Request { init( model: String, - messages: [ChatWithImage.Message], + messages: [Chat.MessageWithImage], temperature: Double, topP: Double, n: Int, @@ -50,7 +50,7 @@ struct CreateChatWithImageRequest: Request { extension CreateChatWithImageRequest { struct Body: Encodable { let model: String - let messages: [ChatWithImage.Message] + let messages: [Chat.MessageWithImage] let temperature: Double let topP: Double let n: Int diff --git a/Sources/OpenAIKit/Client/Client.swift b/Sources/OpenAIKit/Client/Client.swift index ff62ae0..c848095 100644 --- a/Sources/OpenAIKit/Client/Client.swift +++ b/Sources/OpenAIKit/Client/Client.swift @@ -14,7 +14,6 @@ public struct Client { public let images: ImageProvider public let models: ModelProvider public let moderations: ModerationProvider - public let chatsWithImage: ChatWithImageProvider init(requestHandler: RequestHandler) { self.audio = AudioProvider(requestHandler: requestHandler) @@ -26,7 +25,6 @@ public struct Client { self.embeddings = EmbeddingProvider(requestHandler: requestHandler) self.files = FileProvider(requestHandler: requestHandler) self.moderations = ModerationProvider(requestHandler: requestHandler) - self.chatsWithImage = ChatWithImageProvider(requesthandler: requestHandler) } public init( diff --git a/Tests/OpenAIKitTests/MessageTests.swift b/Tests/OpenAIKitTests/MessageTests.swift index 08cadbf..51fcb22 100644 --- a/Tests/OpenAIKitTests/MessageTests.swift +++ b/Tests/OpenAIKitTests/MessageTests.swift @@ -96,7 +96,7 @@ final class MessageTests: XCTestCase { ] """.data(using: .utf8)! - let messages = try JSONDecoder().decode([ChatWithImage.Message].self, from: json) + let messages = try JSONDecoder().decode([Chat.MessageWithImage].self, from: json) XCTAssertEqual(messages.count, 2) @@ -109,12 +109,10 @@ final class MessageTests: XCTestCase { if case .user(let content) = messages[1] { XCTAssertEqual(content.count, 2) XCTAssertEqual(content[0], .text("What’s in this image?")) - XCTAssertEqual(content[1], .imageUrl(ChatWithImage.ImageUrl(url: "https://upload.wikimedia.org/wikipedia/commons/thumb/d/dd/Gfp-wisconsin-madison-the-nature-boardwalk.jpg/2560px-Gfp-wisconsin-madison-the-nature-boardwalk.jpg"))) + XCTAssertEqual(content[1], .imageUrl(Chat.ImageUrl(url: "https://upload.wikimedia.org/wikipedia/commons/thumb/d/dd/Gfp-wisconsin-madison-the-nature-boardwalk.jpg/2560px-Gfp-wisconsin-madison-the-nature-boardwalk.jpg"))) } else { XCTFail("Second Message is not a User Message") } - - ChatWithImage.ImageUrl(url: <#T##String#>) } @@ -175,26 +173,4 @@ final class MessageTests: XCTestCase { XCTFail() } } - - func testChatRequest() throws { - let request = try CreateChatRequest( - model: "gpt-3.5-turbo", //.gpt3_5Turbo, - messages: [ - .system(content: "You are Malcolm Tucker from The Thick of It, an unfriendly assistant for writing mail and explaining science and history. You write text in your voice for me."), - .user(content: "tell me a joke"), - ], - temperature: 1.0, - topP: 1.0, - n: 1, - stream: false, - stops: [], - maxTokens: nil, - presencePenalty: 0.0, - frequencyPenalty: 0.0, - logitBias: [:], - user: nil - ) - - print(request.body) - } } diff --git a/Tests/OpenAIKitTests/RequestHandlerTests.swift b/Tests/OpenAIKitTests/RequestHandlerTests.swift index 345c21e..a4dbbd0 100644 --- a/Tests/OpenAIKitTests/RequestHandlerTests.swift +++ b/Tests/OpenAIKitTests/RequestHandlerTests.swift @@ -53,18 +53,18 @@ final class RequestHandlerTests: XCTestCase { } func test_generateURL_configWithPort() throws { - let api = API(scheme: .http, host: "llms-se.baidu-int.com:8200", port: 8200) + let api = API(scheme: .http, host: "test.com", port: 8200) let configuration = Configuration(apiKey: "TEST", api: api) let request = TestRequest( scheme: .http, - host: "llms-se.baidu-int.com:8200", + host: "some-host", path: "/v1/test" ) let url = try requestHandler(configuration: configuration).generateURL(for: request) - XCTAssertEqual(url, "http://llms-se.baidu-int.com:8200/v1/test") + XCTAssertEqual(url, "http://test.com:8200/v1/test") } } From 377152d3c381ea71117ba3de17b9044a1edd5aaf Mon Sep 17 00:00:00 2001 From: hefengyu Date: Tue, 19 Dec 2023 21:40:29 +0800 Subject: [PATCH 13/13] fix: remove generated comments --- Sources/OpenAIKit/Chat/CreateChatWithImageRequest.swift | 7 ------- 1 file changed, 7 deletions(-) diff --git a/Sources/OpenAIKit/Chat/CreateChatWithImageRequest.swift b/Sources/OpenAIKit/Chat/CreateChatWithImageRequest.swift index 5ae5e36..06b5e6c 100644 --- a/Sources/OpenAIKit/Chat/CreateChatWithImageRequest.swift +++ b/Sources/OpenAIKit/Chat/CreateChatWithImageRequest.swift @@ -1,10 +1,3 @@ -// -// File.swift -// -// -// Created by 贺峰煜 on 2023/11/29. -// - import AsyncHTTPClient import NIOHTTP1 import Foundation