Skip to content

Commit 4ebacf8

Browse files
committed
Add Claude support
1 parent 0e1e9dc commit 4ebacf8

File tree

6 files changed

+110
-20
lines changed

6 files changed

+110
-20
lines changed

Core/Sources/HostApp/AccountSettings/ChatModelManagement/ChatModelEdit.swift

Lines changed: 30 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -86,27 +86,35 @@ struct ChatModelEdit: ReducerProtocol {
8686
)
8787
return .run { send in
8888
do {
89-
let reply =
90-
try await ChatGPTService(
91-
configuration: UserPreferenceChatGPTConfiguration()
92-
.overriding {
93-
$0.model = model
94-
}
95-
).sendAndWait(content: "Hello")
89+
let service = ChatGPTService(
90+
configuration: UserPreferenceChatGPTConfiguration()
91+
.overriding {
92+
$0.model = model
93+
}
94+
)
95+
let reply = try await service
96+
.sendAndWait(content: "Respond with \"Test succeeded\"")
9697
await send(.testSucceeded(reply ?? "No Message"))
98+
let stream = try await service
99+
.send(content: "Respond with \"Stream response is working\"")
100+
var streamReply = ""
101+
for try await chunk in stream {
102+
streamReply += chunk
103+
}
104+
await send(.testSucceeded(streamReply))
97105
} catch {
98106
await send(.testFailed(error.localizedDescription))
99107
}
100108
}
101109

102110
case let .testSucceeded(message):
103111
state.isTesting = false
104-
toast(message, .info)
112+
toast(message.trimmingCharacters(in: .whitespacesAndNewlines), .info)
105113
return .none
106114

107115
case let .testFailed(message):
108116
state.isTesting = false
109-
toast(message, .error)
117+
toast(message.trimmingCharacters(in: .whitespacesAndNewlines), .error)
110118
return .none
111119

112120
case .refreshAvailableModelNames:
@@ -132,6 +140,15 @@ struct ChatModelEdit: ReducerProtocol {
132140
state.suggestedMaxTokens = nil
133141
}
134142
return .none
143+
case .claude:
144+
if let knownModel = ClaudeChatCompletionsService
145+
.KnownModel(rawValue: state.modelName)
146+
{
147+
state.suggestedMaxTokens = knownModel.contextWindow
148+
} else {
149+
state.suggestedMaxTokens = nil
150+
}
151+
return .none
135152
default:
136153
state.suggestedMaxTokens = nil
137154
return .none
@@ -192,13 +209,12 @@ extension ChatModel {
192209
isFullURL: state.isFullURL,
193210
maxTokens: state.maxTokens,
194211
supportsFunctionCalling: {
195-
if case .googleAI = state.format {
196-
return false
197-
}
198-
if case .ollama = state.format {
212+
switch state.format {
213+
case .googleAI, .ollama, .claude:
199214
return false
215+
case .azureOpenAI, .openAI, .openAICompatible:
216+
return state.supportsFunctionCalling
200217
}
201-
return state.supportsFunctionCalling
202218
}(),
203219
modelName: state.modelName.trimmingCharacters(in: .whitespacesAndNewlines),
204220
ollamaInfo: .init(keepAlive: state.ollamaKeepAlive)

Core/Sources/HostApp/AccountSettings/ChatModelManagement/ChatModelEditView.swift

Lines changed: 52 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import AIModel
22
import ComposableArchitecture
3+
import OpenAIService
34
import Preferences
45
import SwiftUI
56

@@ -26,6 +27,8 @@ struct ChatModelEditView: View {
2627
googleAI
2728
case .ollama:
2829
ollama
30+
case .claude:
31+
claude
2932
}
3033
}
3134
}
@@ -96,6 +99,8 @@ struct ChatModelEditView: View {
9699
Text("Google Generative AI").tag(format)
97100
case .ollama:
98101
Text("Ollama").tag(format)
102+
case .claude:
103+
Text("Claude").tag(format)
99104
}
100105
}
101106
},
@@ -348,7 +353,7 @@ struct ChatModelEditView: View {
348353

349354
maxTokensTextField
350355
}
351-
356+
352357
@ViewBuilder
353358
var ollama: some View {
354359
baseURLTextField(prompt: Text("http://127.0.0.1:11434")) {
@@ -363,7 +368,7 @@ struct ChatModelEditView: View {
363368
}
364369

365370
maxTokensTextField
366-
371+
367372
WithViewStore(
368373
store,
369374
removeDuplicates: { $0.ollamaKeepAlive == $1.ollamaKeepAlive }
@@ -380,6 +385,51 @@ struct ChatModelEditView: View {
380385
}
381386
.padding(.vertical)
382387
}
388+
389+
@ViewBuilder
390+
var claude: some View {
391+
baseURLTextField(prompt: Text("https://api.anthropic.com")) {
392+
Text("/v1/messages")
393+
}
394+
395+
apiKeyNamePicker
396+
397+
WithViewStore(
398+
store,
399+
removeDuplicates: { $0.modelName == $1.modelName }
400+
) { viewStore in
401+
TextField("Model Name", text: viewStore.$modelName)
402+
.overlay(alignment: .trailing) {
403+
Picker(
404+
"",
405+
selection: viewStore.$modelName,
406+
content: {
407+
if ClaudeChatCompletionsService
408+
.KnownModel(rawValue: viewStore.state.modelName) == nil
409+
{
410+
Text("Custom Model").tag(viewStore.state.modelName)
411+
}
412+
ForEach(
413+
ClaudeChatCompletionsService.KnownModel.allCases,
414+
id: \.self
415+
) { model in
416+
Text(model.rawValue).tag(model.rawValue)
417+
}
418+
}
419+
)
420+
.frame(width: 20)
421+
}
422+
}
423+
424+
maxTokensTextField
425+
426+
VStack(alignment: .leading, spacing: 8) {
427+
Text(Image(systemName: "exclamationmark.triangle.fill")) + Text(
428+
" For more details, please visit [https://anthropic.com](https://anthropic.com)."
429+
)
430+
}
431+
.padding(.vertical)
432+
}
383433
}
384434

385435
#Preview("OpenAI") {

Core/Sources/HostApp/AccountSettings/ChatModelManagement/ChatModelManagement.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ extension ChatModel: ManageableAIModel {
1212
case .openAICompatible: return "OpenAI Compatible"
1313
case .googleAI: return "Google Generative AI"
1414
case .ollama: return "Ollama"
15+
case .claude: return "Claude"
1516
}
1617
}
1718

Tool/Sources/AIModel/ChatModel.swift

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ public struct ChatModel: Codable, Equatable, Identifiable {
2222
case openAICompatible
2323
case googleAI
2424
case ollama
25+
case claude
2526
}
2627

2728
public struct Info: Codable, Equatable {
@@ -107,6 +108,10 @@ public struct ChatModel: Codable, Equatable, Identifiable {
107108
let baseURL = info.baseURL
108109
if baseURL.isEmpty { return "http://localhost:11434/api/chat" }
109110
return "\(baseURL)/api/chat"
111+
case .claude:
112+
let baseURL = info.baseURL
113+
if baseURL.isEmpty { return "https://api.anthropic.com/v1/messages" }
114+
return "\(baseURL)/v1/messages"
110115
}
111116
}
112117
}

Tool/Sources/OpenAIService/APIs/OpenAIChatCompletionsService.swift

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -238,8 +238,8 @@ actor OpenAIChatCompletionsService: ChatCompletionsStreamAPI, ChatCompletionsAPI
238238
case .openAI:
239239
if !model.info.openAIInfo.organizationID.isEmpty {
240240
request.setValue(
241-
"OpenAI-Organization",
242-
forHTTPHeaderField: model.info.openAIInfo.organizationID
241+
model.info.openAIInfo.organizationID,
242+
forHTTPHeaderField: "OpenAI-Organization"
243243
)
244244
}
245245
request.setValue("Bearer \(apiKey)", forHTTPHeaderField: "Authorization")
@@ -251,6 +251,8 @@ actor OpenAIChatCompletionsService: ChatCompletionsStreamAPI, ChatCompletionsAPI
251251
assertionFailure("Unsupported")
252252
case .ollama:
253253
assertionFailure("Unsupported")
254+
case .claude:
255+
assertionFailure("Unsupported")
254256
}
255257
}
256258

@@ -319,6 +321,8 @@ actor OpenAIChatCompletionsService: ChatCompletionsStreamAPI, ChatCompletionsAPI
319321
assertionFailure("Unsupported")
320322
case .ollama:
321323
assertionFailure("Unsupported")
324+
case .claude:
325+
assertionFailure("Unsupported")
322326
}
323327
}
324328

@@ -376,7 +380,7 @@ extension OpenAIChatCompletionsService.ResponseBody {
376380
),
377381
]
378382
} else {
379-
return []
383+
return nil
380384
}
381385
}()
382386
)

Tool/Sources/OpenAIService/ChatGPTService.swift

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,13 @@ public class ChatGPTService: ChatGPTServiceType {
111111
endpoint: endpoint,
112112
requestBody: requestBody
113113
)
114+
case .claude:
115+
return ClaudeChatCompletionsService(
116+
apiKey: apiKey,
117+
model: model,
118+
endpoint: endpoint,
119+
requestBody: requestBody
120+
)
114121
}
115122
}
116123

@@ -138,6 +145,13 @@ public class ChatGPTService: ChatGPTServiceType {
138145
endpoint: endpoint,
139146
requestBody: requestBody
140147
)
148+
case .claude:
149+
return ClaudeChatCompletionsService(
150+
apiKey: apiKey,
151+
model: model,
152+
endpoint: endpoint,
153+
requestBody: requestBody
154+
)
141155
}
142156
}
143157

@@ -579,7 +593,7 @@ extension ChatGPTService {
579593
let serviceSupportsFunctionCalling = switch model.format {
580594
case .openAI, .openAICompatible, .azureOpenAI:
581595
model.info.supportsFunctionCalling
582-
case .ollama, .googleAI:
596+
case .ollama, .googleAI, .claude:
583597
false
584598
}
585599

0 commit comments

Comments
 (0)