From 95d06cbb4503d9b448af38080c25597eaa04fba6 Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Sun, 9 Mar 2025 23:44:12 +0800 Subject: [PATCH 01/11] Bump Codeium language server to 1.40.2 --- .../LanguageServer/CodeiumInstallationManager.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tool/Sources/CodeiumService/LanguageServer/CodeiumInstallationManager.swift b/Tool/Sources/CodeiumService/LanguageServer/CodeiumInstallationManager.swift index 13e430e6..58ae4725 100644 --- a/Tool/Sources/CodeiumService/LanguageServer/CodeiumInstallationManager.swift +++ b/Tool/Sources/CodeiumService/LanguageServer/CodeiumInstallationManager.swift @@ -3,7 +3,7 @@ import Terminal public struct CodeiumInstallationManager { private static var isInstalling = false - static let latestSupportedVersion = "1.28.3" + static let latestSupportedVersion = "1.40.2" static let minimumSupportedVersion = "1.20.0" public init() {} From 67bfe32e782bc15ea89c0ff9a7b2d27367d371e6 Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Mon, 10 Mar 2025 00:23:10 +0800 Subject: [PATCH 02/11] Add join JSON --- Core/Tests/ServiceTests/Environment.swift | 4 + TestPlan.xctestplan | 93 +++++++++++--------- Tool/Package.swift | 8 +- Tool/Sources/JoinJSON/JoinJSON.swift | 29 ++++++ Tool/Tests/JoinJSONTests/JoinJSONTests.swift | 74 ++++++++++++++++ 5 files changed, 164 insertions(+), 44 deletions(-) create mode 100644 Tool/Sources/JoinJSON/JoinJSON.swift create mode 100644 Tool/Tests/JoinJSONTests/JoinJSONTests.swift diff --git a/Core/Tests/ServiceTests/Environment.swift b/Core/Tests/ServiceTests/Environment.swift index ee78f322..13a66210 100644 --- a/Core/Tests/ServiceTests/Environment.swift +++ b/Core/Tests/ServiceTests/Environment.swift @@ -14,6 +14,10 @@ func completion(text: String, range: CursorRange, uuid: String = "") -> CodeSugg } class MockSuggestionService: GitHubCopilotSuggestionServiceType { + func cancelOngoingTask(workDoneToken: String) async { + fatalError() + } + func notifyChangeTextDocument(fileURL: URL, content: String, version: Int) async throws { fatalError() } diff --git a/TestPlan.xctestplan b/TestPlan.xctestplan index 8806c7f2..56634aa7 100644 --- a/TestPlan.xctestplan +++ b/TestPlan.xctestplan @@ -24,135 +24,135 @@ "testTargets" : [ { "target" : { - "containerPath" : "container:Core", - "identifier" : "ServiceTests", - "name" : "ServiceTests" + "containerPath" : "container:Tool", + "identifier" : "OpenAIServiceTests", + "name" : "OpenAIServiceTests" } }, { "target" : { "containerPath" : "container:Core", - "identifier" : "SuggestionWidgetTests", - "name" : "SuggestionWidgetTests" + "identifier" : "KeyBindingManagerTests", + "name" : "KeyBindingManagerTests" } }, { "target" : { - "containerPath" : "container:Core", - "identifier" : "PromptToCodeServiceTests", - "name" : "PromptToCodeServiceTests" + "containerPath" : "container:Tool", + "identifier" : "SuggestionInjectorTests", + "name" : "SuggestionInjectorTests" } }, { "target" : { "containerPath" : "container:Tool", - "identifier" : "LangChainTests", - "name" : "LangChainTests" + "identifier" : "SharedUIComponentsTests", + "name" : "SharedUIComponentsTests" } }, { "target" : { "containerPath" : "container:Tool", - "identifier" : "OpenAIServiceTests", - "name" : "OpenAIServiceTests" + "identifier" : "SuggestionBasicTests", + "name" : "SuggestionBasicTests" } }, { "target" : { "containerPath" : "container:Core", - "identifier" : "ChatServiceTests", - "name" : "ChatServiceTests" + "identifier" : "PromptToCodeServiceTests", + "name" : "PromptToCodeServiceTests" } }, { "target" : { "containerPath" : "container:Tool", - "identifier" : "TokenEncoderTests", - "name" : "TokenEncoderTests" + "identifier" : "JoinJSONTests", + "name" : "JoinJSONTests" } }, { "target" : { "containerPath" : "container:Tool", - "identifier" : "SharedUIComponentsTests", - "name" : "SharedUIComponentsTests" + "identifier" : "FocusedCodeFinderTests", + "name" : "FocusedCodeFinderTests" } }, { "target" : { "containerPath" : "container:Tool", - "identifier" : "ASTParserTests", - "name" : "ASTParserTests" + "identifier" : "ActiveDocumentChatContextCollectorTests", + "name" : "ActiveDocumentChatContextCollectorTests" } }, { "target" : { - "containerPath" : "container:Core", - "identifier" : "ServiceUpdateMigrationTests", - "name" : "ServiceUpdateMigrationTests" + "containerPath" : "container:Tool", + "identifier" : "XcodeInspectorTests", + "name" : "XcodeInspectorTests" } }, { "target" : { "containerPath" : "container:Tool", - "identifier" : "KeychainTests", - "name" : "KeychainTests" + "identifier" : "GitHubCopilotServiceTests", + "name" : "GitHubCopilotServiceTests" } }, { "target" : { - "containerPath" : "container:Tool", - "identifier" : "ActiveDocumentChatContextCollectorTests", - "name" : "ActiveDocumentChatContextCollectorTests" + "containerPath" : "container:Core", + "identifier" : "SuggestionWidgetTests", + "name" : "SuggestionWidgetTests" } }, { "target" : { "containerPath" : "container:Tool", - "identifier" : "GitHubCopilotServiceTests", - "name" : "GitHubCopilotServiceTests" + "identifier" : "TokenEncoderTests", + "name" : "TokenEncoderTests" } }, { "target" : { - "containerPath" : "container:Tool", - "identifier" : "FocusedCodeFinderTests", - "name" : "FocusedCodeFinderTests" + "containerPath" : "container:Core", + "identifier" : "ChatServiceTests", + "name" : "ChatServiceTests" } }, { "target" : { "containerPath" : "container:Tool", - "identifier" : "XcodeInspectorTests", - "name" : "XcodeInspectorTests" + "identifier" : "LangChainTests", + "name" : "LangChainTests" } }, { "target" : { "containerPath" : "container:Tool", - "identifier" : "SuggestionProviderTests", - "name" : "SuggestionProviderTests" + "identifier" : "KeychainTests", + "name" : "KeychainTests" } }, { "target" : { "containerPath" : "container:Core", - "identifier" : "KeyBindingManagerTests", - "name" : "KeyBindingManagerTests" + "identifier" : "ServiceTests", + "name" : "ServiceTests" } }, { "target" : { "containerPath" : "container:Tool", - "identifier" : "SuggestionBasicTests", - "name" : "SuggestionBasicTests" + "identifier" : "ASTParserTests", + "name" : "ASTParserTests" } }, { "target" : { "containerPath" : "container:Tool", - "identifier" : "SuggestionInjectorTests", - "name" : "SuggestionInjectorTests" + "identifier" : "SuggestionProviderTests", + "name" : "SuggestionProviderTests" } }, { @@ -161,6 +161,13 @@ "identifier" : "CodeDiffTests", "name" : "CodeDiffTests" } + }, + { + "target" : { + "containerPath" : "container:Core", + "identifier" : "ServiceUpdateMigrationTests", + "name" : "ServiceUpdateMigrationTests" + } } ], "version" : 1 diff --git a/Tool/Package.swift b/Tool/Package.swift index 7d356d08..550491a7 100644 --- a/Tool/Package.swift +++ b/Tool/Package.swift @@ -81,7 +81,10 @@ let package = Package( url: "https://github.com/intitni/generative-ai-swift", branch: "support-setting-base-url" ), - .package(url: "https://github.com/intitni/CopilotForXcodeKit", branch: "feature/custom-chat-tab"), + .package( + url: "https://github.com/intitni/CopilotForXcodeKit", + branch: "feature/custom-chat-tab" + ), // TreeSitter .package(url: "https://github.com/intitni/SwiftTreeSitter.git", branch: "main"), @@ -104,6 +107,9 @@ let package = Package( .target(name: "ObjectiveCExceptionHandling"), + .target(name: "JoinJSON"), + .testTarget(name: "JoinJSONTests", dependencies: ["JoinJSON"]), + .target(name: "CodeDiff", dependencies: ["SuggestionBasic"]), .testTarget(name: "CodeDiffTests", dependencies: ["CodeDiff"]), diff --git a/Tool/Sources/JoinJSON/JoinJSON.swift b/Tool/Sources/JoinJSON/JoinJSON.swift new file mode 100644 index 00000000..26181e88 --- /dev/null +++ b/Tool/Sources/JoinJSON/JoinJSON.swift @@ -0,0 +1,29 @@ +import Foundation + +public struct JoinJSON { + public init() {} + + public func join(_ a: String, with b: String) -> Data { + return join(a.data(using: .utf8) ?? Data(), with: b.data(using: .utf8) ?? Data()) + } + + public func join(_ a: Data, with b: String) -> Data { + return join(a, with: b.data(using: .utf8) ?? Data()) + } + + public func join(_ a: Data, with b: Data) -> Data { + guard let firstDict = try? JSONSerialization.jsonObject(with: a) as? [String: Any], + let secondDict = try? JSONSerialization.jsonObject(with: b) as? [String: Any] + else { + return a + } + + var merged = firstDict + for (key, value) in secondDict { + merged[key] = value + } + + return (try? JSONSerialization.data(withJSONObject: merged)) ?? a + } +} + diff --git a/Tool/Tests/JoinJSONTests/JoinJSONTests.swift b/Tool/Tests/JoinJSONTests/JoinJSONTests.swift new file mode 100644 index 00000000..05cbf3e6 --- /dev/null +++ b/Tool/Tests/JoinJSONTests/JoinJSONTests.swift @@ -0,0 +1,74 @@ +import Foundation + +import XCTest +@testable import JoinJSON + +final class JoinJSONTests: XCTestCase { + var sut: JoinJSON! + + override func setUp() { + super.setUp() + sut = JoinJSON() + } + + override func tearDown() { + sut = nil + super.tearDown() + } + + func test_join_two_valid_json_strings() throws { + let json1 = """ + {"name": "John"} + """ + let json2 = """ + {"age": 30} + """ + + let result = sut.join(json1, with: json2) + let dict = try JSONSerialization.jsonObject(with: result) as? [String: Any] + + XCTAssertEqual(dict?["name"] as? String, "John") + XCTAssertEqual(dict?["age"] as? Int, 30) + } + + func test_join_with_invalid_json_returns_first_data() { + let json1 = """ + {"name": "John"} + """ + let invalidJSON = "invalid json" + + let result = sut.join(json1, with: invalidJSON) + XCTAssertEqual(result, json1.data(using: .utf8)) + } + + func test_join_with_overlapping_keys_prefers_second_value() throws { + let json1 = """ + {"name": "John", "age": 25} + """ + let json2 = """ + {"age": 30} + """ + + let result = sut.join(json1, with: json2) + let dict = try JSONSerialization.jsonObject(with: result) as? [String: Any] + + XCTAssertEqual(dict?["name"] as? String, "John") + XCTAssertEqual(dict?["age"] as? Int, 30) + } + + func test_join_with_data_input() throws { + let data1 = """ + {"name": "John"} + """.data(using: .utf8)! + + let data2 = """ + {"age": 30} + """.data(using: .utf8)! + + let result = sut.join(data1, with: data2) + let dict = try JSONSerialization.jsonObject(with: result) as? [String: Any] + + XCTAssertEqual(dict?["name"] as? String, "John") + XCTAssertEqual(dict?["age"] as? Int, 30) + } +} From de4da8f5fbc555a1bfe65a8c156f054701cf4ea4 Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Mon, 10 Mar 2025 00:26:03 +0800 Subject: [PATCH 03/11] Add custom body info to chat model --- Tool/Sources/AIModel/ChatModel.swift | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/Tool/Sources/AIModel/ChatModel.swift b/Tool/Sources/AIModel/ChatModel.swift index 7ae666e3..145d0298 100644 --- a/Tool/Sources/AIModel/ChatModel.swift +++ b/Tool/Sources/AIModel/ChatModel.swift @@ -94,6 +94,14 @@ public struct ChatModel: Codable, Equatable, Identifiable { self.headers = headers } } + + public struct CustomBodyInfo: Codable, Equatable { + public var jsonBody: String + + public init(jsonBody: String = "") { + self.jsonBody = jsonBody + } + } @FallbackDecoding public var apiKeyName: String @@ -122,6 +130,8 @@ public struct ChatModel: Codable, Equatable, Identifiable { public var openAICompatibleInfo: OpenAICompatibleInfo @FallbackDecoding public var customHeaderInfo: CustomHeaderInfo + @FallbackDecoding + public var customBodyInfo: CustomBodyInfo public init( apiKeyName: String = "", @@ -136,7 +146,8 @@ public struct ChatModel: Codable, Equatable, Identifiable { ollamaInfo: OllamaInfo = OllamaInfo(), googleGenerativeAIInfo: GoogleGenerativeAIInfo = GoogleGenerativeAIInfo(), openAICompatibleInfo: OpenAICompatibleInfo = OpenAICompatibleInfo(), - customHeaderInfo: CustomHeaderInfo = CustomHeaderInfo() + customHeaderInfo: CustomHeaderInfo = CustomHeaderInfo(), + customBodyInfo: CustomBodyInfo = CustomBodyInfo() ) { self.apiKeyName = apiKeyName self.baseURL = baseURL @@ -151,6 +162,7 @@ public struct ChatModel: Codable, Equatable, Identifiable { self.googleGenerativeAIInfo = googleGenerativeAIInfo self.openAICompatibleInfo = openAICompatibleInfo self.customHeaderInfo = customHeaderInfo + self.customBodyInfo = customBodyInfo } } @@ -217,6 +229,10 @@ public struct EmptyChatModelCustomHeaderInfo: FallbackValueProvider { public static var defaultValue: ChatModel.Info.CustomHeaderInfo { .init() } } +public struct EmptyChatModelCustomBodyInfo: FallbackValueProvider { + public static var defaultValue: ChatModel.Info.CustomBodyInfo { .init() } +} + public struct EmptyTrue: FallbackValueProvider { public static var defaultValue: Bool { true } } From 059a2da27920bac3d31922b62394817db9393d5f Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Mon, 10 Mar 2025 01:40:58 +0800 Subject: [PATCH 04/11] Support custom body --- .../APIKeyManagementView.swift | 6 +- .../ChatModelManagement/ChatModelEdit.swift | 7 +- .../ChatModelEditView.swift | 118 ++++++++++++++---- Tool/Package.swift | 1 + .../APIs/OpenAIChatCompletionsService.swift | 18 +++ 5 files changed, 122 insertions(+), 28 deletions(-) diff --git a/Core/Sources/HostApp/AccountSettings/APIKeyManagement/APIKeyManagementView.swift b/Core/Sources/HostApp/AccountSettings/APIKeyManagement/APIKeyManagementView.swift index 8c06d2dc..bc6c910e 100644 --- a/Core/Sources/HostApp/AccountSettings/APIKeyManagement/APIKeyManagementView.swift +++ b/Core/Sources/HostApp/AccountSettings/APIKeyManagement/APIKeyManagementView.swift @@ -82,8 +82,10 @@ struct APIKeyManagementView: View { state: \.apiKeySubmission, action: \.apiKeySubmission )) { store in - APIKeySubmissionView(store: store) - .frame(minWidth: 400) + WithPerceptionTracking { + APIKeySubmissionView(store: store) + .frame(minWidth: 400) + } } } } diff --git a/Core/Sources/HostApp/AccountSettings/ChatModelManagement/ChatModelEdit.swift b/Core/Sources/HostApp/AccountSettings/ChatModelManagement/ChatModelEdit.swift index 0f5e2d1f..7b4869fc 100644 --- a/Core/Sources/HostApp/AccountSettings/ChatModelManagement/ChatModelEdit.swift +++ b/Core/Sources/HostApp/AccountSettings/ChatModelManagement/ChatModelEdit.swift @@ -34,6 +34,7 @@ struct ChatModelEdit { var customHeaders: [ChatModel.Info.CustomHeaderInfo.HeaderField] = [] var openAICompatibleSupportsMultipartMessageContent = true var requiresBeginWithUserMessage = false + var customBody: String = "" } enum Action: Equatable, BindableAction { @@ -302,7 +303,8 @@ extension ChatModel { .openAICompatibleSupportsMultipartMessageContent, requiresBeginWithUserMessage: state.requiresBeginWithUserMessage ), - customHeaderInfo: .init(headers: state.customHeaders) + customHeaderInfo: .init(headers: state.customHeaders), + customBodyInfo: .init(jsonBody: state.customBody) ) ) } @@ -328,7 +330,8 @@ extension ChatModel { customHeaders: info.customHeaderInfo.headers, openAICompatibleSupportsMultipartMessageContent: info.openAICompatibleInfo .supportsMultipartMessageContent, - requiresBeginWithUserMessage: info.openAICompatibleInfo.requiresBeginWithUserMessage + requiresBeginWithUserMessage: info.openAICompatibleInfo.requiresBeginWithUserMessage, + customBody: info.customBodyInfo.jsonBody ) } } diff --git a/Core/Sources/HostApp/AccountSettings/ChatModelManagement/ChatModelEditView.swift b/Core/Sources/HostApp/AccountSettings/ChatModelManagement/ChatModelEditView.swift index 4d3941be..f28fc9a1 100644 --- a/Core/Sources/HostApp/AccountSettings/ChatModelManagement/ChatModelEditView.swift +++ b/Core/Sources/HostApp/AccountSettings/ChatModelManagement/ChatModelEditView.swift @@ -49,6 +49,25 @@ struct ChatModelEditView: View { .controlSize(.small) } } + + CustomBodyEdit(store: store) + .disabled({ + switch store.format { + case .openAI, .openAICompatible: + return false + default: + return true + } + }()) + CustomHeaderEdit(store: store) + .disabled({ + switch store.format { + case .openAI, .openAICompatible, .ollama, .gitHubCopilot: + return false + default: + return true + } + }()) Spacer() @@ -230,6 +249,79 @@ struct ChatModelEditView: View { } } + struct CustomBodyEdit: View { + @Perception.Bindable var store: StoreOf + @State private var isEditing = false + @Dependency(\.namespacedToast) var toast + + var body: some View { + Button("Custom Body") { + isEditing = true + } + .sheet(isPresented: $isEditing) { + WithPerceptionTracking { + VStack { + TextEditor(text: $store.customBody) + .font(Font.system(.body, design: .monospaced)) + .padding(4) + .frame(minHeight: 120) + .multilineTextAlignment(.leading) + .overlay( + RoundedRectangle(cornerRadius: 4) + .stroke(Color(nsColor: .separatorColor), lineWidth: 1) + ) + .handleToast(namespace: "CustomBodyEdit") + + Text( + "The custom body will be added to the request body. Please use it to add parameters that are not yet available in the form. It should be a valid JSON object." + ) + .foregroundColor(.secondary) + .font(.callout) + .padding(.bottom) + + Button(action: { + if store.customBody.trimmingCharacters(in: .whitespacesAndNewlines) + .isEmpty + { + isEditing = false + return + } + guard let _ = try? JSONSerialization + .jsonObject(with: store.customBody.data(using: .utf8) ?? Data()) + else { + toast("Invalid JSON object", .error, "CustomBodyEdit") + return + } + isEditing = false + }) { + Text("Done") + } + .keyboardShortcut(.defaultAction) + } + .padding() + .frame(width: 600, height: 500) + .background(Color(nsColor: .windowBackgroundColor)) + } + } + } + } + + struct CustomHeaderEdit: View { + @Perception.Bindable var store: StoreOf + @State private var isEditing = false + + var body: some View { + Button("Custom Headers") { + isEditing = true + } + .sheet(isPresented: $isEditing) { + WithPerceptionTracking { + CustomHeaderSettingsView(headers: $store.customHeaders) + } + } + } + } + struct OpenAIForm: View { @Perception.Bindable var store: StoreOf var body: some View { @@ -300,7 +392,6 @@ struct ChatModelEditView: View { struct OpenAICompatibleForm: View { @Perception.Bindable var store: StoreOf - @State var isEditingCustomHeader = false var body: some View { WithPerceptionTracking { @@ -340,16 +431,10 @@ struct ChatModelEditView: View { Toggle(isOn: $store.openAICompatibleSupportsMultipartMessageContent) { Text("Support multi-part message content") } - + Toggle(isOn: $store.requiresBeginWithUserMessage) { Text("Requires the first message to be from the user") } - - Button("Custom Headers") { - isEditingCustomHeader.toggle() - } - }.sheet(isPresented: $isEditingCustomHeader) { - CustomHeaderSettingsView(headers: $store.customHeaders) } } } @@ -394,7 +479,6 @@ struct ChatModelEditView: View { struct OllamaForm: View { @Perception.Bindable var store: StoreOf - @State var isEditingCustomHeader = false var body: some View { WithPerceptionTracking { @@ -411,20 +495,13 @@ struct ChatModelEditView: View { TextField(text: $store.ollamaKeepAlive, prompt: Text("Default Value")) { Text("Keep Alive") } - - Button("Custom Headers") { - isEditingCustomHeader.toggle() - } - + VStack(alignment: .leading, spacing: 8) { Text(Image(systemName: "exclamationmark.triangle.fill")) + Text( " For more details, please visit [https://ollama.com](https://ollama.com)." ) } .padding(.vertical) - - }.sheet(isPresented: $isEditingCustomHeader) { - CustomHeaderSettingsView(headers: $store.customHeaders) } } } @@ -475,7 +552,6 @@ struct ChatModelEditView: View { struct GitHubCopilotForm: View { @Perception.Bindable var store: StoreOf - @State var isEditingCustomHeader = false var body: some View { WithPerceptionTracking { @@ -507,10 +583,6 @@ struct ChatModelEditView: View { Text("Support multi-part message content") } - Button("Custom Headers") { - isEditingCustomHeader.toggle() - } - VStack(alignment: .leading, spacing: 8) { Text(Image(systemName: "exclamationmark.triangle.fill")) + Text( " Please login in the GitHub Copilot settings to use the model." @@ -522,8 +594,6 @@ struct ChatModelEditView: View { } .dynamicHeightTextInFormWorkaround() .padding(.vertical) - }.sheet(isPresented: $isEditingCustomHeader) { - CustomHeaderSettingsView(headers: $store.customHeaders) } } } diff --git a/Tool/Package.swift b/Tool/Package.swift index 550491a7..a2020a9e 100644 --- a/Tool/Package.swift +++ b/Tool/Package.swift @@ -460,6 +460,7 @@ let package = Package( "BuiltinExtension", "ChatBasic", "GitHubCopilotService", + "JoinJSON", .product(name: "JSONRPC", package: "JSONRPC"), .product(name: "AsyncAlgorithms", package: "swift-async-algorithms"), .product(name: "GoogleGenerativeAI", package: "generative-ai-swift"), diff --git a/Tool/Sources/OpenAIService/APIs/OpenAIChatCompletionsService.swift b/Tool/Sources/OpenAIService/APIs/OpenAIChatCompletionsService.swift index 0c34f40a..0418ca8e 100644 --- a/Tool/Sources/OpenAIService/APIs/OpenAIChatCompletionsService.swift +++ b/Tool/Sources/OpenAIService/APIs/OpenAIChatCompletionsService.swift @@ -2,6 +2,7 @@ import AIModel import AsyncAlgorithms import ChatBasic import Foundation +import JoinJSON import Logger import Preferences @@ -345,6 +346,7 @@ actor OpenAIChatCompletionsService: ChatCompletionsStreamAPI, ChatCompletionsAPI request.httpBody = try encoder.encode(requestBody) request.setValue("application/json", forHTTPHeaderField: "Content-Type") + Self.setupCustomBody(&request, model: model) Self.setupAppInformation(&request) Self.setupAPIKey(&request, model: model, apiKey: apiKey) await Self.setupExtraHeaderFields(&request, model: model, apiKey: apiKey) @@ -495,6 +497,22 @@ actor OpenAIChatCompletionsService: ChatCompletionsStreamAPI, ChatCompletionsAPI } } } + + static func setupCustomBody(_ request: inout URLRequest, model: ChatModel) { + switch model.format { + case .openAI, .openAICompatible: + break + default: + return + } + + let join = JoinJSON() + let jsonBody = model.info.customBodyInfo.jsonBody + .trimmingCharacters(in: .whitespacesAndNewlines) + guard let data = request.httpBody, !jsonBody.isEmpty else { return } + let newBody = join.join(data, with: jsonBody) + request.httpBody = newBody + } } extension OpenAIChatCompletionsService.ResponseBody { From d5f83fac03834ef7098a23b3e2cbcc85d5a707d9 Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Mon, 10 Mar 2025 16:47:37 +0800 Subject: [PATCH 05/11] Update claude --- .../ChatModelEditView.swift | 4 ++-- .../APIs/ClaudeChatCompletionsService.swift | 18 +++++++++++++++++- Version.xcconfig | 4 ++-- 3 files changed, 21 insertions(+), 5 deletions(-) diff --git a/Core/Sources/HostApp/AccountSettings/ChatModelManagement/ChatModelEditView.swift b/Core/Sources/HostApp/AccountSettings/ChatModelManagement/ChatModelEditView.swift index f28fc9a1..752d6c6c 100644 --- a/Core/Sources/HostApp/AccountSettings/ChatModelManagement/ChatModelEditView.swift +++ b/Core/Sources/HostApp/AccountSettings/ChatModelManagement/ChatModelEditView.swift @@ -53,7 +53,7 @@ struct ChatModelEditView: View { CustomBodyEdit(store: store) .disabled({ switch store.format { - case .openAI, .openAICompatible: + case .openAI, .openAICompatible, .claude: return false default: return true @@ -62,7 +62,7 @@ struct ChatModelEditView: View { CustomHeaderEdit(store: store) .disabled({ switch store.format { - case .openAI, .openAICompatible, .ollama, .gitHubCopilot: + case .openAI, .openAICompatible, .ollama, .gitHubCopilot, .claude: return false default: return true diff --git a/Tool/Sources/OpenAIService/APIs/ClaudeChatCompletionsService.swift b/Tool/Sources/OpenAIService/APIs/ClaudeChatCompletionsService.swift index c57935b0..12434f1e 100644 --- a/Tool/Sources/OpenAIService/APIs/ClaudeChatCompletionsService.swift +++ b/Tool/Sources/OpenAIService/APIs/ClaudeChatCompletionsService.swift @@ -3,6 +3,7 @@ import AsyncAlgorithms import ChatBasic import CodableWrappers import Foundation +import JoinJSON import Logger import Preferences @@ -10,6 +11,7 @@ import Preferences public actor ClaudeChatCompletionsService: ChatCompletionsStreamAPI, ChatCompletionsAPI { /// https://docs.anthropic.com/en/docs/about-claude/models public enum KnownModel: String, CaseIterable { + case claude37Sonnet = "claude-3-7-sonnet-latest" case claude35Sonnet = "claude-3-5-sonnet-latest" case claude35Haiku = "claude-3-5-haiku-latest" case claude3Opus = "claude-3-opus-latest" @@ -23,6 +25,7 @@ public actor ClaudeChatCompletionsService: ChatCompletionsStreamAPI, ChatComplet case .claude3Opus: return 200_000 case .claude3Sonnet: return 200_000 case .claude3Haiku: return 200_000 + case .claude37Sonnet: return 200_000 } } } @@ -232,6 +235,8 @@ public actor ClaudeChatCompletionsService: ChatCompletionsStreamAPI, ChatComplet if !apiKey.isEmpty { request.setValue(apiKey, forHTTPHeaderField: "x-api-key") } + Self.setupCustomBody(&request, model: model) + await Self.setupExtraHeaderFields(&request, model: model, apiKey: apiKey) let (result, response) = try await URLSession.shared.bytes(for: request) guard let response = response as? HTTPURLResponse else { @@ -295,6 +300,8 @@ public actor ClaudeChatCompletionsService: ChatCompletionsStreamAPI, ChatComplet if !apiKey.isEmpty { request.setValue(apiKey, forHTTPHeaderField: "x-api-key") } + Self.setupCustomBody(&request, model: model) + await Self.setupExtraHeaderFields(&request, model: model, apiKey: apiKey) let (result, response) = try await URLSession.shared.data(for: request) guard let response = response as? HTTPURLResponse else { @@ -315,6 +322,15 @@ public actor ClaudeChatCompletionsService: ChatCompletionsStreamAPI, ChatComplet throw error } } + + static func setupCustomBody(_ request: inout URLRequest, model: ChatModel) { + let join = JoinJSON() + let jsonBody = model.info.customBodyInfo.jsonBody + .trimmingCharacters(in: .whitespacesAndNewlines) + guard let data = request.httpBody, !jsonBody.isEmpty else { return } + let newBody = join.join(data, with: jsonBody) + request.httpBody = newBody + } } extension ClaudeChatCompletionsService.ResponseBody { @@ -453,7 +469,7 @@ extension ClaudeChatCompletionsService.RequestBody { data: image.data.base64EncodedString() ))) } - + return content } diff --git a/Version.xcconfig b/Version.xcconfig index 3ee2110f..dd1998c3 100644 --- a/Version.xcconfig +++ b/Version.xcconfig @@ -1,4 +1,4 @@ -APP_VERSION = 0.35.6 -APP_BUILD = 451 +APP_VERSION = 0.35.7 +APP_BUILD = 452 RELEASE_CHANNEL = RELEASE_NUMBER = 1 From ef0d9e24221549ec3a00561936df62dd8ee9c6b1 Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Tue, 11 Mar 2025 18:52:25 +0800 Subject: [PATCH 06/11] Fix selection range parsing when the text cursor is at the very end --- Tool/Sources/XcodeInspector/SourceEditor.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/Tool/Sources/XcodeInspector/SourceEditor.swift b/Tool/Sources/XcodeInspector/SourceEditor.swift index 09c7da04..c1495402 100644 --- a/Tool/Sources/XcodeInspector/SourceEditor.swift +++ b/Tool/Sources/XcodeInspector/SourceEditor.swift @@ -262,6 +262,7 @@ public extension SourceEditor { countE += line.utf16.count } if cursorRange.end == .outOfScope { + if range.lowerBound == range.upperBound { return .outOfScope } cursorRange.end = .init( line: lines.endIndex - 1, character: lines.last?.utf16.count ?? 0 From e63c5193b603ec0c66ea315c4494d6339ac1980a Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Tue, 11 Mar 2025 21:38:30 +0800 Subject: [PATCH 07/11] Adjust log --- Core/Sources/KeyBindingManager/TabToAcceptSuggestion.swift | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Core/Sources/KeyBindingManager/TabToAcceptSuggestion.swift b/Core/Sources/KeyBindingManager/TabToAcceptSuggestion.swift index 4965efbf..451183cc 100644 --- a/Core/Sources/KeyBindingManager/TabToAcceptSuggestion.swift +++ b/Core/Sources/KeyBindingManager/TabToAcceptSuggestion.swift @@ -177,7 +177,9 @@ final class TabToAcceptSuggestion { } guard let filespace = workspacePool.fetchFilespaceIfExisted(fileURL: fileURL) else { - Logger.service.info("TabToAcceptSuggestion: No file found") + Logger.service.info( + "TabToAcceptSuggestion: No file found for file \(fileURL.lastPathComponent)" + ) return .unchanged } guard let presentingSuggestion = filespace.presentingSuggestion From 07abf10e198220140a74237d142121e0ffd6fb30 Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Wed, 12 Mar 2025 15:11:25 +0800 Subject: [PATCH 08/11] Bump build number --- Version.xcconfig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Version.xcconfig b/Version.xcconfig index dd1998c3..c736061b 100644 --- a/Version.xcconfig +++ b/Version.xcconfig @@ -1,4 +1,4 @@ APP_VERSION = 0.35.7 -APP_BUILD = 452 +APP_BUILD = 454 RELEASE_CHANNEL = RELEASE_NUMBER = 1 From c65b0bbea565805cc0612fe99d0b8b1df9d64698 Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Sat, 5 Apr 2025 16:13:25 +0800 Subject: [PATCH 09/11] Bump Sparkle version --- Core/Package.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Core/Package.swift b/Core/Package.swift index 3934bbbc..89c3bf38 100644 --- a/Core/Package.swift +++ b/Core/Package.swift @@ -40,7 +40,7 @@ let package = Package( .package(path: "../ChatPlugins"), .package(url: "https://github.com/apple/swift-async-algorithms", from: "1.0.0"), .package(url: "https://github.com/gonzalezreal/swift-markdown-ui", from: "2.4.1"), - .package(url: "https://github.com/sparkle-project/Sparkle", from: "2.0.0"), + .package(url: "https://github.com/sparkle-project/Sparkle", from: "2.6.4"), .package(url: "https://github.com/pointfreeco/swift-parsing", from: "0.12.1"), .package(url: "https://github.com/pointfreeco/swift-dependencies", from: "1.0.0"), .package( From bf9e0b1c7106c429f1d67747c775a702250c7941 Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Mon, 7 Apr 2025 17:42:42 +0800 Subject: [PATCH 10/11] Bump Codeium version --- .../LanguageServer/CodeiumInstallationManager.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tool/Sources/CodeiumService/LanguageServer/CodeiumInstallationManager.swift b/Tool/Sources/CodeiumService/LanguageServer/CodeiumInstallationManager.swift index 58ae4725..f832d093 100644 --- a/Tool/Sources/CodeiumService/LanguageServer/CodeiumInstallationManager.swift +++ b/Tool/Sources/CodeiumService/LanguageServer/CodeiumInstallationManager.swift @@ -3,7 +3,7 @@ import Terminal public struct CodeiumInstallationManager { private static var isInstalling = false - static let latestSupportedVersion = "1.40.2" + static let latestSupportedVersion = "1.42.7" static let minimumSupportedVersion = "1.20.0" public init() {} From 18f8542c49e400b48caabb758aa5093884ea4de9 Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Mon, 7 Apr 2025 17:42:52 +0800 Subject: [PATCH 11/11] Update build number --- Version.xcconfig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Version.xcconfig b/Version.xcconfig index c736061b..f6d1a07e 100644 --- a/Version.xcconfig +++ b/Version.xcconfig @@ -1,4 +1,4 @@ APP_VERSION = 0.35.7 -APP_BUILD = 454 +APP_BUILD = 455 RELEASE_CHANNEL = RELEASE_NUMBER = 1