Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 0 additions & 17 deletions Chato.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@
/* Begin PBXBuildFile section */
EB1B46DF2EDAF0AD00FD053A /* AIProxy in Frameworks */ = {isa = PBXBuildFile; productRef = EB1B46DE2EDAF0AD00FD053A /* AIProxy */; };
EB4A0EE52B6F908D0065BCDC /* OpenAI in Frameworks */ = {isa = PBXBuildFile; productRef = EB4A0EE42B6F908D0065BCDC /* OpenAI */; };
EB80897D2CE12A100003BACD /* SwiftOpenAI in Frameworks */ = {isa = PBXBuildFile; productRef = EB80897C2CE12A100003BACD /* SwiftOpenAI */; };
EB8FB4F32C3940D60041833B /* MarkdownUI in Frameworks */ = {isa = PBXBuildFile; productRef = EB8FB4F22C3940D60041833B /* MarkdownUI */; };
EB9E72042BCAB7DF00D5B110 /* ConfettiSwiftUI in Frameworks */ = {isa = PBXBuildFile; productRef = EB9E72032BCAB7DF00D5B110 /* ConfettiSwiftUI */; };
EBA73E2F2CDD3006007E480C /* VisualEffectView in Frameworks */ = {isa = PBXBuildFile; productRef = EBA73E2E2CDD3006007E480C /* VisualEffectView */; };
Expand Down Expand Up @@ -62,7 +61,6 @@
files = (
EBC1F9092D1853F50008D5E7 /* Fakery in Frameworks */,
EBCFF3952B6FF4CC00498E1E /* Throttler in Frameworks */,
EB80897D2CE12A100003BACD /* SwiftOpenAI in Frameworks */,
EB4A0EE52B6F908D0065BCDC /* OpenAI in Frameworks */,
EBA73E2F2CDD3006007E480C /* VisualEffectView in Frameworks */,
EBB4EF292CDDC02200F38178 /* VisualEffectView in Frameworks */,
Expand Down Expand Up @@ -114,7 +112,6 @@
EB8FB4F22C3940D60041833B /* MarkdownUI */,
EBA73E2E2CDD3006007E480C /* VisualEffectView */,
EBB4EF282CDDC02200F38178 /* VisualEffectView */,
EB80897C2CE12A100003BACD /* SwiftOpenAI */,
EBC1F9082D1853F50008D5E7 /* Fakery */,
EB1B46DE2EDAF0AD00FD053A /* AIProxy */,
);
Expand Down Expand Up @@ -154,7 +151,6 @@
EB9E72022BCAB7DF00D5B110 /* XCRemoteSwiftPackageReference "ConfettiSwiftUI" */,
EB8FB4F12C3940D60041833B /* XCRemoteSwiftPackageReference "swift-markdown-ui" */,
EBB4EF272CDDC02200F38178 /* XCRemoteSwiftPackageReference "VisualEffectView" */,
EB80897B2CE12A100003BACD /* XCRemoteSwiftPackageReference "SwiftOpenAI" */,
EBC1F9072D1853F50008D5E7 /* XCRemoteSwiftPackageReference "Fakery" */,
EB1B46DD2EDAF0AD00FD053A /* XCRemoteSwiftPackageReference "AIProxySwift" */,
);
Expand Down Expand Up @@ -430,14 +426,6 @@
version = 0.2.6;
};
};
EB80897B2CE12A100003BACD /* XCRemoteSwiftPackageReference "SwiftOpenAI" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/moderato-app/SwiftOpenAI.git";
requirement = {
kind = revision;
revision = a9aa7214d6fdb113da56fb2be6d44b755ed4686c;
};
};
EB8FB4F12C3940D60041833B /* XCRemoteSwiftPackageReference "swift-markdown-ui" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/gonzalezreal/swift-markdown-ui.git";
Expand Down Expand Up @@ -499,11 +487,6 @@
package = EB6422702B6F7D1800119DE6 /* XCRemoteSwiftPackageReference "OpenAI" */;
productName = OpenAI;
};
EB80897C2CE12A100003BACD /* SwiftOpenAI */ = {
isa = XCSwiftPackageProductDependency;
package = EB80897B2CE12A100003BACD /* XCRemoteSwiftPackageReference "SwiftOpenAI" */;
productName = SwiftOpenAI;
};
EB8FB4F22C3940D60041833B /* MarkdownUI */ = {
isa = XCSwiftPackageProductDependency;
package = EB8FB4F12C3940D60041833B /* XCRemoteSwiftPackageReference "swift-markdown-ui" */;
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

13 changes: 7 additions & 6 deletions Chato/Extensions/OpenAIExt.swift
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
import Foundation
import SwiftOpenAI
import AIProxy

fileprivate let timeout: UInt = 60
extension OpenAIService {
func hello(model: String) async throws -> String {
let msgs: [ChatCompletionParameters.Message] = [.init(role: .user, content: .text("Hello"))]
let parameters = ChatCompletionParameters(
messages: msgs,
model: .custom(model)
let msgs: [OpenAIChatCompletionRequestBody.Message] = [.user(content: .text("Hello"))]
let parameters = OpenAIChatCompletionRequestBody(
model: model,
messages: msgs
)

let result = try await self.startChat(parameters: parameters)
let result = try await self.chatCompletionRequest(body: parameters, secondsToWait: timeout)
return result.choices[0].message.content ?? ""
}
}
2 changes: 1 addition & 1 deletion Chato/Models/ChatOption.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ final class ChatOption {
@Attribute(originalName: "frequency_penalty") var frequencyPenalty: Double = 0
// An alternative to sampling with temperature, called nucleus sampling, where the model considers the results of the tokens with top_p probability mass. So 0.1 means only the tokens comprising the top 10% probability mass are considered.

init(model: String = defaultGPTModel.value, contextLength: Int = 2, prompt: Prompt? = nil) {
init(model: String = "gpt-4o-mini", contextLength: Int = 2, prompt: Prompt? = nil) {
self.model = model
self.contextLength = contextLength
self.prompt = prompt
Expand Down
27 changes: 0 additions & 27 deletions Chato/Models/Data/ChatGPT.swift
Original file line number Diff line number Diff line change
@@ -1,31 +1,4 @@
import Foundation
import SwiftOpenAI

struct ChatGPTModel {
let name: String
let value: String
let swiftOpenAIModel: SwiftOpenAI.Model
let version: Int

init(_ name: String, _ value: String, _ swiftOpenAIModel: SwiftOpenAI.Model, _ version: Int) {
self.name = name
self.value = value
self.swiftOpenAIModel = swiftOpenAIModel
self.version = version
}
}

let chatGPTModels = [
ChatGPTModel("GPT3.5 Turbo", "gpt-3.5-turbo", .gpt35Turbo, 3),
defaultGPTModel,
ChatGPTModel("GPT4", "gpt-4", .gpt4, 4),
ChatGPTModel("GPT-4o", "gpt-4o", .gpt4o, 4),
ChatGPTModel("o1-preview", "o1-preview", .o1Preview, 4),
ChatGPTModel("o1-mini", "o1-mini", .o1Mini, 4),
ChatGPTModel("GPT-4 Turbo", "gpt-4-turbo", .gpt4turbo, 4)
].sorted { a, b in a.value < b.value }

let defaultGPTModel = ChatGPTModel("GPT-4o mini", "gpt-4o-mini", .gpt4omini, 3)

enum ContextLength: Hashable {
case zero
Expand Down
13 changes: 5 additions & 8 deletions Chato/Service/Deps/APIClientKey.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,12 @@

import Foundation
import os
import SwiftOpenAI
import AIProxy
import SwiftUI

// Define your custom environment key
struct OpenAIServiceKey: EnvironmentKey {
static let defaultValue: OpenAIService = OpenAIServiceFactory.service(apiKey: "your-default-api-key")
static let defaultValue: OpenAIService = AIProxy.openAIDirectService(unprotectedAPIKey: "your-default-api-key")
}

// Add new AIClient environment key
Expand Down Expand Up @@ -40,24 +40,21 @@ class OpenAIServiceProvider: ObservableObject {
let service: OpenAIService

init(apiKey: String, endpint: String? = nil, timeout: TimeInterval = 120) {
let conf = URLSessionConfiguration.default
conf.timeoutIntervalForRequest = timeout

if let endpint {
do {
let res = try parseURL(endpint)
self.service = OpenAIServiceFactory.service(apiKey: apiKey, configuration: conf, overrideBaseURL: res.base, proxyPath: res.path)
self.service = AIProxy.openAIDirectService(unprotectedAPIKey: apiKey, baseURL: res.base)
} catch {
AppLogger.logError(.from(
error: error,
operation: "Parse URL",
component: "OpenAIServiceProvider",
userMessage: nil
))
self.service = OpenAIServiceFactory.service(apiKey: apiKey, configuration: conf)
self.service = AIProxy.openAIDirectService(unprotectedAPIKey: apiKey)
}
} else {
self.service = OpenAIServiceFactory.service(apiKey: apiKey, configuration: conf)
self.service = AIProxy.openAIDirectService(unprotectedAPIKey: apiKey)
}
}
}
Expand Down
125 changes: 57 additions & 68 deletions Chato/Views/MessageList/VM.swift
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import AIProxy
import Combine
import os
import SwiftData
import SwiftOpenAI
import SwiftUI
import os

extension InputAreaView {
static let whiteSpaces: [Character] = ["\n", " ", "\t"]
Expand Down Expand Up @@ -83,51 +83,62 @@ extension InputAreaView {
}

func ask2(text: String, contextLength: Int) {
let isO1 = chat.option.model.contains("o1-")
let timeout: Double = isO1 ? 120.0 : 15.0
AppLogger.network.debug("using timeout: \(timeout), contextLength: \(contextLength)")

var msgs: [ChatCompletionParameters.Message] = [.init(role: .user, content: .text(text))]
var msgs: [OpenAIChatCompletionRequestBody.Message] = [.user(content: .text(text))]

var actualCL = 0
if contextLength > 0 {
let hist = self.modelContext.recentMessgagesEarlyOnTop(chatId: chat.persistentModelID, limit: contextLength)
actualCL = hist.count

for item in hist.sorted().reversed() {
let msg: ChatCompletionParameters.Message = .init(role: item.role == .user ? .user : (item.role == .assistant ? .assistant : .system), content: .text(item.message.isMeaningful ? item.message : item.errorInfo))
let msg: OpenAIChatCompletionRequestBody.Message
switch item.role {
case .user:
msg = .user(content: .text(item.message.isMeaningful ? item.message : item.errorInfo))
case .assistant:
msg = .assistant(content: .text(item.message.isMeaningful ? item.message : item.errorInfo))
default:
msg = .system(content: .text(item.message.isMeaningful ? item.message : item.errorInfo))
}
msgs.insert(msg, at: 0)
}
}

chat.option.prompt?.messages.sorted().reversed().forEach {
let msg: ChatCompletionParameters.Message = .init(role: $0.role == .assistant ? .assistant : ($0.role == .user || isO1 ? .user : .system), content: .text($0.content))
let msg: OpenAIChatCompletionRequestBody.Message
if $0.role == .assistant {
msg = .assistant(content: .text($0.content))
} else if $0.role == .user {
msg = .user(content: .text($0.content))
} else {
msg = .system(content: .text($0.content))
}
msgs.insert(msg, at: 0)
}

let parameters = ChatCompletionParameters(
let streamParameters = OpenAIChatCompletionRequestBody(
model: chat.option.model,
messages: msgs,
model: .custom(chat.option.model),
frequencyPenalty: chat.option.maybeFrequencyPenalty,
presencePenalty: chat.option.maybePresencePenalty,
stream: true,
temperature: chat.option.maybeTemperature
)

let streamParameters = ChatCompletionParameters(
messages: msgs,
model: .custom(chat.option.model),
frequencyPenalty: chat.option.maybeFrequencyPenalty,
presencePenalty: chat.option.maybePresencePenalty,
temperature: chat.option.maybeTemperature,
streamOptions: ChatCompletionParameters.StreamOptions(includeUsage: true)
)

AppLogger.network.debug("using temperature: \(String(describing: chat.option.maybeTemperature)), presencePenalty: \(String(describing: chat.option.maybePresencePenalty)), frequencyPenalty: \(String(describing: chat.option.maybeFrequencyPenalty))")

AppLogger.network.debug("===whole message list begins===")

for (i, m) in msgs.enumerated() {
AppLogger.network.debug("\(i).\(m.role): \(String(describing: m.content))")
let roleStr: String
switch m {
case .user: roleStr = "user"
case .assistant: roleStr = "assistant"
case .system: roleStr = "system"
case .developer: roleStr = "developer"
case .tool: roleStr = "tool"
}
AppLogger.network.debug("\(i).\(roleStr): \(String(describing: m))")
}

AppLogger.network.debug("===whole message list ends===")
Expand Down Expand Up @@ -177,56 +188,34 @@ extension InputAreaView {

Task.detached {
do {
if isO1 {
AppLogger.network.info("not using stream")
Task { @MainActor in
aiMsg.meta?.startedAt = .now
}
let result = try await openAIService.startChat(parameters: parameters)
AppLogger.network.info("using stream")
let stream = try await openAIService.streamingChatCompletionRequest(body: streamParameters, secondsToWait: 60)
for try await chunk in stream {
Task { @MainActor in
let content = result.choices[0].message.content ?? ""
userMsg.onSent()
userMsg.meta?.promptTokens = result.usage.promptTokens
aiMsg.meta?.completionTokens = result.usage.completionTokens
aiMsg.onEOF(text: content)
em.messageEvent.send(.eof)
}
} else {
AppLogger.network.info("using stream")
let stream = try await openAIService.startStreamedChat(parameters: streamParameters)
for try await chunk in stream {
Task { @MainActor in
if aiMsg.meta?.startedAt == nil {
aiMsg.meta?.startedAt = .now
}
if userMsg.status == .sending {
userMsg.onSent()
}
if aiMsg.meta?.startedAt == nil {
aiMsg.meta?.startedAt = .now
}
if userMsg.status == .sending {
userMsg.onSent()
}

if let choice = chunk.choices.first {
let text = choice.delta.content ?? "\nchoice has no content\n"
if let fr = choice.finishReason {
if case .string(let reason) = fr {
AppLogger.network.debug("finished reason: \(reason)")
aiMsg.onEOF(text: "")
em.messageEvent.send(.eof)
//// skip eof; the last chunk's 'usage' isn't nil and it has no 'choices'
// } else {
// // if reason is not stop, something may be wrong
// aiMsg.onError("finished reason: \(fr)", .unknown)
}
} else {
aiMsg.onTyping(text: text)
}
if let choice = chunk.choices.first {
let text = choice.delta.content ?? "\nchoice has no content\n"
if let fr = choice.finishReason {
AppLogger.network.debug("finished reason: \(fr)")
aiMsg.onEOF(text: "")
em.messageEvent.send(.eof)
} else {
aiMsg.onTyping(text: text)
}
} else {
if let usage = chunk.usage {
userMsg.meta?.promptTokens = usage.promptTokens
aiMsg.meta?.completionTokens = usage.completionTokens
aiMsg.onEOF(text: "")
em.messageEvent.send(.eof)
} else {
if let usage = chunk.usage {
userMsg.meta?.promptTokens = usage.promptTokens
aiMsg.meta?.completionTokens = usage.completionTokens
aiMsg.onEOF(text: "")
em.messageEvent.send(.eof)
} else {
AppLogger.network.debug("no text in chunk, chunk: \(String(describing: chunk))")
}
AppLogger.network.debug("no text in chunk, chunk: \(String(describing: chunk))")
}
}
}
Expand Down
4 changes: 2 additions & 2 deletions Chato/Views/Settings/ChatGPTSettingSection.swift
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import Haptico
import SwiftData
import SwiftOpenAI
import AIProxy
import SwiftUI

struct ChatGPTSettingSection: View {
Expand Down Expand Up @@ -76,7 +76,7 @@ struct ChatGPTSettingSection: View {
TestButton {
let openai = pref.gptEnableEndpoint ? OpenAIServiceProvider(apiKey: pref.gptApiKey, endpint: pref.gptEndpoint, timeout: 5) : OpenAIServiceProvider(apiKey: pref.gptApiKey, timeout: 5)
do {
let res = try await openai.service.hello(model: models.randomElement()?.modelId ?? Model.gpt4omini.value)
let res = try await openai.service.hello(model: models.randomElement()?.modelId ?? "gpt-4o-mini")
HapticsService.shared.shake(.success)
return .succeeded(res)
} catch {
Expand Down
Loading