Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add new GPT-4-Vision Support and support custom URL with port. #66

Open
wants to merge 13 commits into
base: main
Choose a base branch
from
3 changes: 3 additions & 0 deletions Sources/OpenAIKit/API.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}
Expand Down
141 changes: 141 additions & 0 deletions Sources/OpenAIKit/Chat/ChatWithImage.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
//
// File.swift
//
//
// Created by 贺峰煜 on 2023/11/29.
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove these generated comments.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed

//

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 {}
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This doesn't seem right, you should be able to extend the pre-existing Chat model to support the inclusion of an image.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I revised the redundant parts of the code.


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
}
}
}

56 changes: 56 additions & 0 deletions Sources/OpenAIKit/Chat/ChatWithImageProvider.swift
Original file line number Diff line number Diff line change
@@ -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)
}
}
111 changes: 111 additions & 0 deletions Sources/OpenAIKit/Chat/CreateChatWithImageRequest.swift
Original file line number Diff line number Diff line change
@@ -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)
}
}
}
2 changes: 2 additions & 0 deletions Sources/OpenAIKit/Client/Client.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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(
Expand Down
2 changes: 1 addition & 1 deletion Sources/OpenAIKit/Completion/CompletionProvider.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
*/
Expand Down
2 changes: 1 addition & 1 deletion Sources/OpenAIKit/Completion/CreateCompletionRequest.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
2 changes: 2 additions & 0 deletions Sources/OpenAIKit/Model/Model.swift
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@ 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"
case gpt4_vision = "gpt-4-vision-preview"
}

public enum GPT3: String, ModelID {
Expand Down
3 changes: 2 additions & 1 deletion Sources/OpenAIKit/RequestHandler/RequestHandler.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
Loading