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: support local Ollama service #601

Closed
wants to merge 26 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
82db8e1
refactor: replace const key with enum StoredKey
tisfeng Jun 8, 2024
27b588e
perf: remove unused code
tisfeng Jun 8, 2024
00318e7
refactor: improve Defaults.Key
tisfeng Jun 17, 2024
3b7e1dd
Merge branch 'dev' into refactor-stored-key
tisfeng Jun 24, 2024
c8bb781
refactor: improve TextEditorCell, use ServiceConfigurationPickerCell …
tisfeng Jun 25, 2024
6bf590b
perf: disable user to edit built-in supported models
tisfeng Jun 26, 2024
af08306
refactor: improve StreamConfigurationView, remove viewModel
tisfeng Jun 28, 2024
863cb5c
fix: setup subscribers when init, post update notification if model c…
tisfeng Jun 28, 2024
0af9a9f
perf: set defaultModels for OpenAI, Gemini and Built-in service
tisfeng Jun 30, 2024
18725e2
refactor: rename enum OpenAIModel and GeminiModel, update gpt3_5_turbo
tisfeng Jun 30, 2024
addfda3
fix: due to service memory leaks, multiple notifications are posted
tisfeng Jul 1, 2024
0a0e388
fix: if main window dealloc, we need to setup subscribers again
tisfeng Jul 2, 2024
c70e01c
fix: improve Gemini error message for empty model
tisfeng Jul 3, 2024
fcc1072
fix: replace validation viewModel @StateObject with @ObservedObject
tisfeng Jul 3, 2024
4f9f835
refactor: improve Gemini translate()
tisfeng Jul 3, 2024
998103d
fix: Gemini and Built-in service cannot validate
tisfeng Jul 3, 2024
48f58d4
fix: show different api key placeholders
tisfeng Jul 4, 2024
58eb633
fix: remove unused ObservableObject
tisfeng Jul 4, 2024
cabdad6
style: format code
tisfeng Jul 4, 2024
18f9592
chore: update SwiftFormat to 0.54
tisfeng Jul 4, 2024
9c9364a
style: replace override public with public override
tisfeng Jul 4, 2024
36e7c60
Merge branch 'dev' into refactor-defaults
tisfeng Jul 4, 2024
b2485c3
fix: validModels is empty even if defaultModels is set
tisfeng Jul 6, 2024
4a14841
feat: support local Ollama service
tisfeng Jul 6, 2024
dd1b4cb
feat: get Ollama local models by api/tags automatically
tisfeng Jul 7, 2024
771f5a9
Merge branch 'dev' into support-ollama
tisfeng Jul 8, 2024
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
25 changes: 21 additions & 4 deletions Easydict.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
03262C1C29EEE91700EFECA0 /* EZEnumTypes.m in Sources */ = {isa = PBXBuildFile; fileRef = 03262C1B29EEE91700EFECA0 /* EZEnumTypes.m */; };
03262C2529EFE97B00EFECA0 /* NSViewController+EZWindow.m in Sources */ = {isa = PBXBuildFile; fileRef = 03262C2429EFE97B00EFECA0 /* NSViewController+EZWindow.m */; };
03280B812C23FE4A00E75A24 /* StreamConfigurationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03280B802C23FE4A00E75A24 /* StreamConfigurationView.swift */; };
03280B812C23FE4A00E75A24 /* StreamConfigurationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03280B802C23FE4A00E75A24 /* StreamConfigurationView.swift */; };
0329CD6F29EE924500963F78 /* EZRightClickDetector.m in Sources */ = {isa = PBXBuildFile; fileRef = 0329CD6E29EE924500963F78 /* EZRightClickDetector.m */; };
033363A0293A05D200FED9C8 /* EZSelectLanguageButton.m in Sources */ = {isa = PBXBuildFile; fileRef = 0333639F293A05D200FED9C8 /* EZSelectLanguageButton.m */; };
033363A6293C4AFA00FED9C8 /* PrintBeautifulLog.m in Sources */ = {isa = PBXBuildFile; fileRef = 033363A5293C4AFA00FED9C8 /* PrintBeautifulLog.m */; };
Expand Down Expand Up @@ -74,9 +75,10 @@
03779F0F2BB256A7008D3C42 /* Prompt.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03779F0C2BB256A7008D3C42 /* Prompt.swift */; };
03779F132BB256B5008D3C42 /* APIKey.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03779F102BB256B5008D3C42 /* APIKey.swift */; };
03779F142BB256B5008D3C42 /* EncryptedSecretKeys.plist in Resources */ = {isa = PBXBuildFile; fileRef = 03779F112BB256B5008D3C42 /* EncryptedSecretKeys.plist */; };
03779F172BB256C5008D3C42 /* URL+IsValid.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03779F152BB256C5008D3C42 /* URL+IsValid.swift */; };
03779F172BB256C5008D3C42 /* URL+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03779F152BB256C5008D3C42 /* URL+Extension.swift */; };
03779F1A2BB25797008D3C42 /* OpenAI in Frameworks */ = {isa = PBXBuildFile; productRef = 03779F192BB25797008D3C42 /* OpenAI */; };
037852B9295D49F900D0E2CF /* EZTableRowView.m in Sources */ = {isa = PBXBuildFile; fileRef = 037852B8295D49F900D0E2CF /* EZTableRowView.m */; };
03792EA22C3831040074A145 /* OllamaService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03792EA12C3831040074A145 /* OllamaService.swift */; };
038030952B4106800009230C /* CocoaLumberjack in Frameworks */ = {isa = PBXBuildFile; productRef = 038030942B4106800009230C /* CocoaLumberjack */; };
038030972B4106800009230C /* CocoaLumberjackSwift in Frameworks */ = {isa = PBXBuildFile; productRef = 038030962B4106800009230C /* CocoaLumberjackSwift */; };
03832F542B5F6BE200D0DC64 /* AdvancedTab.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03832F532B5F6BE200D0DC64 /* AdvancedTab.swift */; };
Expand Down Expand Up @@ -201,6 +203,7 @@
03CAB9552ADBF0FF00DA94A3 /* EZSystemUtility.m in Sources */ = {isa = PBXBuildFile; fileRef = 03CAB9542ADBF0FF00DA94A3 /* EZSystemUtility.m */; };
03CF27FE2B3DA7D500E19B57 /* Realm in Frameworks */ = {isa = PBXBuildFile; productRef = 03CF27FD2B3DA7D500E19B57 /* Realm */; };
03CF28002B3DA7D500E19B57 /* RealmSwift in Frameworks */ = {isa = PBXBuildFile; productRef = 03CF27FF2B3DA7D500E19B57 /* RealmSwift */; };
03CF5E642C39B2310058F9DB /* OllamaModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03CF5E632C39B2310058F9DB /* OllamaModel.swift */; };
03CF88632B137F650030C199 /* Array+Convenience.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03CF88622B137F650030C199 /* Array+Convenience.swift */; };
03D0434E292886D200E7559E /* EZMiniQueryWindow.m in Sources */ = {isa = PBXBuildFile; fileRef = 03D0434D292886D200E7559E /* EZMiniQueryWindow.m */; };
03D043522928935300E7559E /* EZMainQueryWindow.m in Sources */ = {isa = PBXBuildFile; fileRef = 03D043512928935300E7559E /* EZMainQueryWindow.m */; };
Expand Down Expand Up @@ -449,9 +452,10 @@
03779F0C2BB256A7008D3C42 /* Prompt.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Prompt.swift; sourceTree = "<group>"; };
03779F102BB256B5008D3C42 /* APIKey.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = APIKey.swift; sourceTree = "<group>"; };
03779F112BB256B5008D3C42 /* EncryptedSecretKeys.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = EncryptedSecretKeys.plist; sourceTree = "<group>"; };
03779F152BB256C5008D3C42 /* URL+IsValid.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "URL+IsValid.swift"; sourceTree = "<group>"; };
03779F152BB256C5008D3C42 /* URL+Extension.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "URL+Extension.swift"; sourceTree = "<group>"; };
037852B7295D49F900D0E2CF /* EZTableRowView.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = EZTableRowView.h; sourceTree = "<group>"; };
037852B8295D49F900D0E2CF /* EZTableRowView.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = EZTableRowView.m; sourceTree = "<group>"; };
03792EA12C3831040074A145 /* OllamaService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OllamaService.swift; sourceTree = "<group>"; };
03832F532B5F6BE200D0DC64 /* AdvancedTab.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdvancedTab.swift; sourceTree = "<group>"; };
0387FB792BFBA990000A7A82 /* LLMStreamService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LLMStreamService.swift; sourceTree = "<group>"; };
03882F8229D95044005B5A52 /* CTScreen.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = CTScreen.h; sourceTree = "<group>"; };
Expand Down Expand Up @@ -668,6 +672,7 @@
03CAB9532ADBF0FF00DA94A3 /* EZSystemUtility.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = EZSystemUtility.h; sourceTree = "<group>"; };
03CAB9542ADBF0FF00DA94A3 /* EZSystemUtility.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = EZSystemUtility.m; sourceTree = "<group>"; };
03CC6C092B21B0DC0049ED29 /* Info-debug.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = "Info-debug.plist"; sourceTree = "<group>"; };
03CF5E632C39B2310058F9DB /* OllamaModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OllamaModel.swift; sourceTree = "<group>"; };
03CF88622B137F650030C199 /* Array+Convenience.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Array+Convenience.swift"; sourceTree = "<group>"; };
03D0434C292886D200E7559E /* EZMiniQueryWindow.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = EZMiniQueryWindow.h; sourceTree = "<group>"; };
03D0434D292886D200E7559E /* EZMiniQueryWindow.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = EZMiniQueryWindow.m; sourceTree = "<group>"; };
Expand Down Expand Up @@ -1199,6 +1204,7 @@
036D627F2BCAB5C6002C95C7 /* BuiltInAI */,
0A9AFBA92B7F8D6A0064C9A8 /* CustomOpenAI */,
C415C0AB2B450C4500A9D231 /* Gemini */,
03792EA02C3830E60074A145 /* Ollama */,
C4DD01E72B12B3B00025EE8E /* Tencent */,
2746AEBF2AF95040005FE0A1 /* Caiyun */,
62E2BF462B4082BA00E42D38 /* Ali */,
Expand Down Expand Up @@ -1230,11 +1236,20 @@
03779F162BB256C5008D3C42 /* URL */ = {
isa = PBXGroup;
children = (
03779F152BB256C5008D3C42 /* URL+IsValid.swift */,
03779F152BB256C5008D3C42 /* URL+Extension.swift */,
);
path = URL;
sourceTree = "<group>";
};
03792EA02C3830E60074A145 /* Ollama */ = {
isa = PBXGroup;
children = (
03792EA12C3831040074A145 /* OllamaService.swift */,
03CF5E632C39B2310058F9DB /* OllamaModel.swift */,
);
path = Ollama;
sourceTree = "<group>";
};
03882F8129D95044005B5A52 /* CoolToast */ = {
isa = PBXGroup;
children = (
Expand Down Expand Up @@ -2771,6 +2786,7 @@
03008B2B2940D3230062B821 /* EZDeepLTranslate.m in Sources */,
03991166292A8A4400E1B06D /* EZTitleBarMoveView.m in Sources */,
27FE98092B3DD536000AD654 /* SettingView.swift in Sources */,
03CF5E642C39B2310058F9DB /* OllamaModel.swift in Sources */,
035E37E72A0953120061DFAF /* EZToast.m in Sources */,
03542A492937B5CF00C34C33 /* EZGoogleTranslate.m in Sources */,
03D0435A2928C4C800E7559E /* EZWindowManager.m in Sources */,
Expand Down Expand Up @@ -2913,7 +2929,7 @@
03B0231729231FA6001C7E63 /* Snip.m in Sources */,
03BFFC6E295FE59C004E033E /* EZQueryResult+EZYoudaoDictModel.m in Sources */,
03779F0F2BB256A7008D3C42 /* Prompt.swift in Sources */,
03779F172BB256C5008D3C42 /* URL+IsValid.swift in Sources */,
03779F172BB256C5008D3C42 /* URL+Extension.swift in Sources */,
6ADED1552BAE8809004A15BE /* NSBundle+Localization.m in Sources */,
03B0232829231FA6001C7E63 /* NSTextView+Height.m in Sources */,
03B0232129231FA6001C7E63 /* NSPasteboard+MM.m in Sources */,
Expand Down Expand Up @@ -2983,6 +2999,7 @@
03542A5E2938F05B00C34C33 /* EZLanguageModel.m in Sources */,
EA9943E82B534D8900EE7B97 /* LanguageDetectOptimizeExtensions.swift in Sources */,
03F639952AA6CFBB009B9914 /* EZBingConfig.m in Sources */,
03792EA22C3831040074A145 /* OllamaService.swift in Sources */,
0AC8A83F2B689E68006DA5CC /* ServiceSecretConfigreValidatable.swift in Sources */,
03D2A3E329F4C6F50035CED4 /* EZNetworkManager.m in Sources */,
9643D9562B73B3CD000FBEA6 /* Shortcut+Menu.swift in Sources */,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{
"images" : [
{
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "Ollama.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
18 changes: 17 additions & 1 deletion Easydict/App/Localizable.xcstrings
Original file line number Diff line number Diff line change
Expand Up @@ -244,7 +244,7 @@
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Apple Translation"
"value" : "Apple Translate"
}
},
"zh-Hans" : {
Expand Down Expand Up @@ -1749,6 +1749,22 @@
}
}
},
"ollama_translate" : {
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Ollama Translate"
}
},
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "Ollama 翻译"
}
}
}
},
"open_in_apple_dictionary" : {
"localizations" : {
"en" : {
Expand Down
10 changes: 8 additions & 2 deletions Easydict/Swift/Feature/Configuration/DefaultsStoredKey.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,17 @@

import Foundation

func storedKey(_ key: StoredKey, serviceType: ServiceType) -> String {
func storedKey(_ key: StoredKey, serviceType: ServiceType, id: String? = nil) -> String {
// This key should be compatible with existing OpenAI config keys
// EZOpenAIServiceUsageStatusKey
// EZOpenAIDictionaryKey
"EZ" + serviceType.rawValue + key.rawValue.capitalizeFirstLetter() + "Key"
// EZOpenAIDictionary_ID_Key

var identifier = ""
if let id, !id.isEmpty {
identifier = "_\(id)_"
}
return "EZ" + serviceType.rawValue + key.rawValue.capitalizeFirstLetter() + identifier + "Key"
}

extension UserDefaults {
Expand Down
14 changes: 8 additions & 6 deletions Easydict/Swift/Feature/DefaultAPIKeys/APIKey.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,34 +9,36 @@
import Defaults
import Foundation

// MARK: - APIKey

extension BuiltInAIService {
var defaultAPIKey: String {
var builtInAIAPIKey: String {
APIKey.builtInAIAPIKey.stringValue
}

var defaultEndpoint: String {
var builtInAIEndpoint: String {
APIKey.builtInAIEndpoint.stringValue
}
}

extension CaiyunService {
var defaultToken: String {
var caiyunToken: String {
APIKey.caiyunToken.stringValue
}
}

extension TencentService {
var defaultSecretId: String {
var tencentSecretId: String {
APIKey.tencentSecretId.stringValue
}

var defaultSecretKey: String {
var tencentSecretKey: String {
APIKey.tencentSecretKey.stringValue
}
}

extension EZNiuTransTranslate {
@objc var defaultAPIKey: String {
@objc var niutransAPIKey: String {
APIKey.niutransAPIKey.stringValue
}
}
Expand Down
4 changes: 2 additions & 2 deletions Easydict/Swift/Service/BuiltInAI/BuiltInAIService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -66,11 +66,11 @@ class BuiltInAIService: BaseOpenAIService {
}

override var apiKey: String {
defaultAPIKey
builtInAIAPIKey
}

override var endpoint: String {
defaultEndpoint
builtInAIEndpoint
}

override var observeKeys: [Defaults.Key<String>] {
Expand Down
4 changes: 2 additions & 2 deletions Easydict/Swift/Service/Caiyun/CaiyunService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ public final class CaiyunService: QueryService {
}

public override func hasPrivateAPIKey() -> Bool {
token != defaultToken
token != caiyunToken
}

public override func autoConvertTraditionalChinese() -> Bool {
Expand Down Expand Up @@ -118,7 +118,7 @@ public final class CaiyunService: QueryService {
if !token.isEmpty {
return token
} else {
return defaultToken
return caiyunToken
}
}
}
Expand Down
46 changes: 46 additions & 0 deletions Easydict/Swift/Service/Ollama/OllamaModel.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
//
// OllamaModel.swift
// Easydict
//
// Created by tisfeng on 2024/7/7.
// Copyright © 2024 izual. All rights reserved.
//

import Foundation

// MARK: - OllamaModels

// Ollama docs https://github.com/ollama/ollama/blob/main/docs/api.md#list-local-models
struct OllamaModels: Codable {
let models: [OllamaModel]
}

// MARK: - OllamaModel

struct OllamaModel: Codable {
enum CodingKeys: String, CodingKey {
case name, model
case modifiedAt = "modified_at"
case size, digest, details
}

let name, model, modifiedAt: String
let size: Int
let digest: String
let details: OllamaModelDetails
}

// MARK: - OllamaModelDetails

struct OllamaModelDetails: Codable {
enum CodingKeys: String, CodingKey {
case parentModel = "parent_model"
case format, family, families
case parameterSize = "parameter_size"
case quantizationLevel = "quantization_level"
}

let parentModel, format, family: String
let families: [String]?
let parameterSize, quantizationLevel: String
}
84 changes: 84 additions & 0 deletions Easydict/Swift/Service/Ollama/OllamaService.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
//
// OllamaService.swift
// Easydict
//
// Created by tisfeng on 2024/7/5.
// Copyright © 2024 izual. All rights reserved.
//

import Alamofire
import Defaults
import Foundation

// MARK: - OllamaService

@objc(EZOllamaService)
class OllamaService: BaseOpenAIService {
// MARK: Lifecycle

override init() {
super.init()

Task {
let models = try await localModels()
self.ollamaModels = models.models.map(\.name)
logInfo("ollama models: \(ollamaModels)")
}
}

// MARK: Public

public override func name() -> String {
NSLocalizedString("ollama_translate", comment: "")
}

public override func serviceType() -> ServiceType {
.ollama
}

// MARK: Internal

override var defaultEndpoint: String {
"http://localhost:11434/v1/chat/completions"
}

override var defaultModels: [String] {
ollamaModels
}

override var observeKeys: [Defaults.Key<String>] {
[supportedModelsKey]
}

override var isSentenceEnabledByDefault: Bool {
false
}

override var isDictionaryEnabledByDefault: Bool {
false
}

override func configurationListItems() -> Any {
StreamConfigurationView(
service: self,
showAPIKeySection: false
)
}

// MARK: Private

private var ollamaModels = [""]

/// Get Ollama modles https://github.com/ollama/ollama/blob/main/docs/api.md#list-local-models
private func localModels() async throws -> OllamaModels {
// endpoint is http://localhost:11434/v1/chat/completions, we need url http://localhost:11434/api/tags
let endpointURL = URL(string: endpoint)
guard let endpointURL, let trueBaseURL = endpointURL.rootURL else {
throw EZError(type: .param, description: "`\(serviceType().rawValue)` endpoint is invalid")
}

let modelsURL = trueBaseURL.appendingPathComponent("api/tags")
let dataTask = AF.request(modelsURL).serializingDecodable(OllamaModels.self)
return try await dataTask.value
}
}
Loading