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
107 changes: 107 additions & 0 deletions Sources/OpenAIKit/Chat/Chat.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 {}
Expand All @@ -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 {
Expand Down Expand Up @@ -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
}
}
}
32 changes: 31 additions & 1 deletion Sources/OpenAIKit/Chat/ChatProvider.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}

/**
Expand Down
104 changes: 104 additions & 0 deletions Sources/OpenAIKit/Chat/CreateChatWithImageRequest.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
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: [Chat.MessageWithImage],
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: [Chat.MessageWithImage]
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: 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