Skip to content

Commit 198820f

Browse files
author
MLeo
committed
add Cloudflare
1 parent 6831e8e commit 198820f

File tree

8 files changed

+201
-23
lines changed

8 files changed

+201
-23
lines changed

Sources/Enums/AIProviderEnum.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ enum AIProviderEnum: String, Codable, CaseIterable, Identifiable {
1212
case openAI = "OpenAI"
1313
case gemini = "Gemini"
1414
case deepSeek = "DeepSeek"
15+
case cloudflare = "Cloudflare"
1516
case custom = "Custom(OpenAI)"
1617

1718
var id: String { self.rawValue }
@@ -22,6 +23,7 @@ enum AIProviderEnum: String, Codable, CaseIterable, Identifiable {
2223
.openAI:AIProviderEnumModel.getOpenAI(),
2324
.gemini:AIProviderEnumModel.getGemini(),
2425
.deepSeek:AIProviderEnumModel.getDeepSeek(),
26+
.cloudflare:AIProviderEnumModel.getCloudflare(),
2527
.custom:AIProviderEnumModel.getCustom()
2628
]
2729

Sources/Enums/Models/AIProviderEnumModel.swift

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,20 @@ class AIProviderEnumModel {
8787
service: service
8888
)
8989
}
90+
static func getCloudflare() -> AIProviderEnumModel {
91+
let title = "Cloudflare(AI Gateway)"
92+
let icon = ""
93+
let supportUrl = "https://developers.cloudflare.com/ai-gateway/usage/chat-completion/"
94+
let APIURL = "https://gateway.ai.cloudflare.com/v1/{account_id}/{gateway_id}"
95+
let service = CloudflareService()
96+
return AIProviderEnumModel(
97+
title: title,
98+
icon: icon,
99+
supportUrl: supportUrl,
100+
APIURL: APIURL,
101+
service: service
102+
)
103+
}
90104
static func getCustom() -> AIProviderEnumModel {
91105
let title = "Custom(OpenAI)"
92106
let icon = ""
Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
//
2+
// GrokService.swift
3+
// iChat
4+
//
5+
// Created by Lion on 2025/4/28.
6+
//
7+
8+
import Foundation
9+
10+
/// Cloudflare客户端实现
11+
class CloudflareService: AIProtocol {
12+
13+
private let chatPath = "/compat/chat/completions"
14+
private let modelPath = "/compat/openai/models"
15+
private let session: URLSession = .shared
16+
17+
func getModels(provider: AIProvider) async throws -> [Model] {
18+
guard let APIURL = URL(string: provider.APIURL + modelPath) else {
19+
throw AIError.WrongAPIURL
20+
}
21+
// 构建请求
22+
var request = URLRequest(url: APIURL)
23+
request.httpMethod = "GET"
24+
request.setValue(
25+
"Bearer \(provider.APIKey)",
26+
forHTTPHeaderField: "Authorization"
27+
)
28+
request.setValue(
29+
"application/json",
30+
forHTTPHeaderField: "Content-Type"
31+
)
32+
let (data, response) = try await session.data(for: request)
33+
// print(String(data: data, encoding: .utf8))
34+
try validateResponse(response)
35+
let modelResponse = try JSONDecoder().decode(
36+
ModelResponse.self,
37+
from: data
38+
)
39+
return modelResponse.data
40+
}
41+
42+
43+
44+
45+
func streamChatResponse(
46+
provider:AIProvider,
47+
model: AIModel,
48+
messages: [ChatMessage],
49+
temperature:Double
50+
) async throws -> AsyncThrowingStream<Delta, Error> {
51+
guard let provider = model.provider else{
52+
throw AIError.MissingProvider
53+
}
54+
guard
55+
let APIURL = URL(string: provider.APIURL + chatPath)
56+
else {
57+
throw AIError.WrongAPIURL
58+
}
59+
// 构建请求
60+
var request = URLRequest(url: APIURL)
61+
request.httpMethod = "POST"
62+
request.setValue(
63+
"Bearer \(provider.APIKey)",
64+
forHTTPHeaderField: "Authorization"
65+
)
66+
request.setValue(
67+
"application/json",
68+
forHTTPHeaderField: "Content-Type"
69+
)
70+
71+
// 构建请求体
72+
var requestBody = StreamRequestBody(
73+
model: model.name,
74+
messages: messages.map({ message in
75+
message.apiRepresentation
76+
}),
77+
temperature: temperature,
78+
stream: true
79+
)
80+
requestBody.extraBody = ExtraBody(google: Google(thinkingConfig: ThinkingConfig()))
81+
request.httpBody = try JSONEncoder().encode(requestBody)
82+
83+
// 获取流式响应
84+
let (bytes, response) = try await session.bytes(
85+
for: request
86+
)
87+
88+
try validateResponse(response)
89+
90+
return AsyncThrowingStream { continuation in
91+
Task {
92+
do {
93+
// 处理 SSE 流
94+
try await processStream(
95+
bytes: bytes,
96+
continuation: continuation
97+
)
98+
continuation.finish()
99+
} catch {
100+
continuation.finish(throwing: error)
101+
}
102+
}
103+
}
104+
}
105+
106+
107+
108+
// 处理 SSE 流
109+
private func processStream(
110+
bytes: URLSession.AsyncBytes,
111+
continuation: AsyncThrowingStream<Delta, Error>.Continuation
112+
) async throws {
113+
var isThinking = false
114+
for try await line in bytes.lines {
115+
print(line)
116+
guard line.hasPrefix("data:") else { continue }
117+
118+
let jsonDataString = line.dropFirst(5).trimmingCharacters(
119+
in: .whitespacesAndNewlines
120+
)
121+
122+
if jsonDataString == "[DONE]" {
123+
return
124+
}
125+
126+
guard let jsonData = jsonDataString.data(using: .utf8) else {
127+
continue
128+
}
129+
130+
do {
131+
let response = try JSONDecoder().decode(
132+
APIResponseMessage.self,
133+
from: jsonData
134+
)
135+
if let content = response.choices?.first?.delta {
136+
let content = transformThinking(delta: content,isThinking:&isThinking)
137+
continuation.yield(content)
138+
}
139+
} catch {
140+
print("JSON 解码错误: \(error) for data: \(jsonDataString)")
141+
continue
142+
}
143+
}
144+
}
145+
146+
private func transformThinking(delta: Delta,isThinking: inout Bool) -> Delta{
147+
guard var content = delta.content else {
148+
return delta
149+
}
150+
if content.contains("<thought>") {
151+
isThinking = true
152+
content.removeAll{ "<thought>".contains($0) }
153+
}else if content.contains("</thought>") {
154+
isThinking = false
155+
content.removeAll { content in
156+
"</thought>".contains(content)
157+
}
158+
}
159+
let delta = Delta(
160+
content: isThinking ? nil : content,
161+
reasoning: isThinking ? content : nil
162+
)
163+
return delta
164+
}
165+
}
166+

Sources/Protocols/Services/GeminiService.swift

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,6 @@ class GeminiService: AIProtocol {
1414
private let modelPath = "/v1beta/openai/models"
1515
private let session: URLSession = .shared
1616

17-
// init(session: URLSession = .shared) {
18-
// self.session = session
19-
// }
20-
2117
func getModels(provider: AIProvider) async throws -> [Model] {
2218
guard let APIURL = URL(string: provider.APIURL + modelPath) else {
2319
throw AIError.WrongAPIURL

Sources/Views/Chat/Message/ChatContentView.swift

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -30,11 +30,6 @@ struct ChatContentView: View {
3030
.cornerRadius(20)
3131
case .assistant:
3232
Markdown(toMarkdown(), lazy: true)
33-
// .padding()
34-
// .fixedSize(horizontal: false, vertical: true)
35-
// .background(messageBackgroundColor)
36-
// .foregroundStyle(messageForegroundColor)
37-
// .cornerRadius(30)
3833
}
3934
}
4035
}

Sources/iChat.entitlements

Lines changed: 1 addition & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,5 @@
11
<?xml version="1.0" encoding="UTF-8"?>
22
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
33
<plist version="1.0">
4-
<dict>
5-
<key>com.apple.security.app-sandbox</key>
6-
<true/>
7-
<key>com.apple.security.files.user-selected.read-write</key>
8-
<true/>
9-
<key>com.apple.security.network.client</key>
10-
<true/>
11-
<key>com.apple.security.network.server</key>
12-
<true/>
13-
</dict>
4+
<dict/>
145
</plist>

iChat.xcodeproj/project.pbxproj

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88

99
/* Begin PBXBuildFile section */
1010
1045A4552DFF16140062042A /* SessionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1045A4542DFF16140062042A /* SessionView.swift */; };
11+
107710F22ED89A6E00EB62D4 /* CloudflareService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 107710F12ED89A6E00EB62D4 /* CloudflareService.swift */; };
1112
10C9F6AD2E05295A00A8719E /* ResponseContentHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 10C9F6AC2E05295A00A8719E /* ResponseContentHelper.swift */; };
1213
B42DB27F2DFA8C1A005EF8BE /* AssistantView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B42DB27E2DFA8C1A005EF8BE /* AssistantView.swift */; };
1314
B42DB2812DFA8F7B005EF8BE /* AssistantEditView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B42DB2802DFA8F7B005EF8BE /* AssistantEditView.swift */; };
@@ -77,6 +78,7 @@
7778

7879
/* Begin PBXFileReference section */
7980
1045A4542DFF16140062042A /* SessionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionView.swift; sourceTree = "<group>"; };
81+
107710F12ED89A6E00EB62D4 /* CloudflareService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CloudflareService.swift; sourceTree = "<group>"; };
8082
10C9F6AC2E05295A00A8719E /* ResponseContentHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ResponseContentHelper.swift; sourceTree = "<group>"; };
8183
B400B5062DEF3DAB002CF2B4 /* README_zh.md */ = {isa = PBXFileReference; explicitFileType = net.daringfireball.markdown; path = README_zh.md; sourceTree = "<group>"; };
8284
B400B5082DEF4199002CF2B4 /* _config.yml */ = {isa = PBXFileReference; lastKnownFileType = text.yaml; path = _config.yml; sourceTree = "<group>"; };
@@ -289,6 +291,7 @@
289291
children = (
290292
B457E88E2DBFA74100598C36 /* GrokService.swift */,
291293
B440D2742DDF72AC0064B3BE /* GeminiService.swift */,
294+
107710F12ED89A6E00EB62D4 /* CloudflareService.swift */,
292295
B49731592DF1C55C004D9DCD /* DeepSeekService.swift */,
293296
B457E8802DBB775F00598C36 /* APIResponseMessage.swift */,
294297
);
@@ -435,7 +438,7 @@
435438
attributes = {
436439
BuildIndependentTargetsInParallel = 1;
437440
LastSwiftUpdateCheck = 1620;
438-
LastUpgradeCheck = 1630;
441+
LastUpgradeCheck = 2610;
439442
TargetAttributes = {
440443
B4806FAB2D9B8047004C952C = {
441444
CreatedOnToolsVersion = 16.2;
@@ -531,6 +534,7 @@
531534
B49731712DF74FAF004D9DCD /* PromptEnumModel.swift in Sources */,
532535
B457E8892DBFA6DA00598C36 /* ChatRoleEnum.swift in Sources */,
533536
B457E88B2DBFA6EA00598C36 /* ChatSession.swift in Sources */,
537+
107710F22ED89A6E00EB62D4 /* CloudflareService.swift in Sources */,
534538
B49731612DF46B9C004D9DCD /* CustomButtonView.swift in Sources */,
535539
B4A22C792DEC5E460091CB68 /* AppearanceEnum.swift in Sources */,
536540
B44725D12DC1FA8A00B9B31B /* InputAreaView.swift in Sources */,
@@ -609,6 +613,7 @@
609613
MTL_FAST_MATH = YES;
610614
ONLY_ACTIVE_ARCH = YES;
611615
SDKROOT = macosx;
616+
STRING_CATALOG_GENERATE_SYMBOLS = YES;
612617
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)";
613618
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
614619
};
@@ -666,6 +671,7 @@
666671
MTL_ENABLE_DEBUG_INFO = NO;
667672
MTL_FAST_MATH = YES;
668673
SDKROOT = macosx;
674+
STRING_CATALOG_GENERATE_SYMBOLS = YES;
669675
SWIFT_COMPILATION_MODE = wholemodule;
670676
};
671677
name = Release;
@@ -680,11 +686,15 @@
680686
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "-";
681687
CODE_SIGN_STYLE = Automatic;
682688
COMBINE_HIDPI_IMAGES = YES;
683-
CURRENT_PROJECT_VERSION = 6;
689+
CURRENT_PROJECT_VERSION = 7;
684690
DEAD_CODE_STRIPPING = YES;
685691
DEVELOPMENT_ASSET_PATHS = "\"Sources/Preview Content\"";
686692
DEVELOPMENT_TEAM = "";
693+
ENABLE_APP_SANDBOX = YES;
694+
ENABLE_INCOMING_NETWORK_CONNECTIONS = YES;
695+
ENABLE_OUTGOING_NETWORK_CONNECTIONS = YES;
687696
ENABLE_PREVIEWS = YES;
697+
ENABLE_USER_SELECTED_FILES = readwrite;
688698
GENERATE_INFOPLIST_FILE = YES;
689699
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities";
690700
INFOPLIST_KEY_NSHumanReadableCopyright = "© 2025 iChochy";
@@ -715,11 +725,15 @@
715725
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "-";
716726
CODE_SIGN_STYLE = Automatic;
717727
COMBINE_HIDPI_IMAGES = YES;
718-
CURRENT_PROJECT_VERSION = 6;
728+
CURRENT_PROJECT_VERSION = 7;
719729
DEAD_CODE_STRIPPING = YES;
720730
DEVELOPMENT_ASSET_PATHS = "\"Sources/Preview Content\"";
721731
DEVELOPMENT_TEAM = "";
732+
ENABLE_APP_SANDBOX = YES;
733+
ENABLE_INCOMING_NETWORK_CONNECTIONS = YES;
734+
ENABLE_OUTGOING_NETWORK_CONNECTIONS = YES;
722735
ENABLE_PREVIEWS = YES;
736+
ENABLE_USER_SELECTED_FILES = readwrite;
723737
GENERATE_INFOPLIST_FILE = YES;
724738
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities";
725739
INFOPLIST_KEY_NSHumanReadableCopyright = "© 2025 iChochy";

iChat.xcodeproj/xcshareddata/xcschemes/iChat.xcscheme

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
<?xml version="1.0" encoding="UTF-8"?>
22
<Scheme
3-
LastUpgradeVersion = "1640"
3+
LastUpgradeVersion = "2610"
44
version = "1.7">
55
<BuildAction
66
parallelizeBuildables = "YES"

0 commit comments

Comments
 (0)