Skip to content

Commit 7c2c89b

Browse files
committed
Support github copilot chat direct call
1 parent 5e8d1ec commit 7c2c89b

File tree

9 files changed

+218
-10
lines changed

9 files changed

+218
-10
lines changed

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

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -56,12 +56,13 @@ struct ChatModelEdit {
5656
case googleAI
5757
case ollama
5858
case claude
59+
case gitHubCopilot
5960
case openAICompatible
6061
case deepSeekOpenAICompatible
6162
case openRouterOpenAICompatible
6263
case grokOpenAICompatible
6364
case mistralOpenAICompatible
64-
65+
6566
init(_ format: ChatModel.Format) {
6667
switch format {
6768
case .openAI:
@@ -76,6 +77,8 @@ struct ChatModelEdit {
7677
self = .claude
7778
case .openAICompatible:
7879
self = .openAICompatible
80+
case .gitHubCopilot:
81+
self = .gitHubCopilot
7982
}
8083
}
8184
}
@@ -195,6 +198,13 @@ struct ChatModelEdit {
195198
state.suggestedMaxTokens = nil
196199
}
197200
return .none
201+
case .gitHubCopilot:
202+
if let knownModel = AvailableGitHubCopilotModel(rawValue: state.modelName) {
203+
state.suggestedMaxTokens = knownModel.contextWindow
204+
} else {
205+
state.suggestedMaxTokens = nil
206+
}
207+
return .none
198208
default:
199209
state.suggestedMaxTokens = nil
200210
return .none
@@ -212,6 +222,8 @@ struct ChatModelEdit {
212222
state.format = .ollama
213223
case .claude:
214224
state.format = .claude
225+
case .gitHubCopilot:
226+
state.format = .gitHubCopilot
215227
case .openAICompatible:
216228
state.format = .openAICompatible
217229
case .deepSeekOpenAICompatible:
@@ -272,7 +284,7 @@ extension ChatModel {
272284
switch state.format {
273285
case .googleAI, .ollama, .claude:
274286
return false
275-
case .azureOpenAI, .openAI, .openAICompatible:
287+
case .azureOpenAI, .openAI, .openAICompatible, .gitHubCopilot:
276288
return state.supportsFunctionCalling
277289
}
278290
}(),

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

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@ struct ChatModelEditView: View {
2929
OllamaForm(store: store)
3030
case .claude:
3131
ClaudeForm(store: store)
32+
case .gitHubCopilot:
33+
GitHubCopilotForm(store: store)
3234
}
3335
}
3436
.padding()
@@ -108,6 +110,8 @@ struct ChatModelEditView: View {
108110
Text("Ollama")
109111
case .claude:
110112
Text("Claude")
113+
case .gitHubCopilot:
114+
Text("GitHub Copilot")
111115
case .deepSeekOpenAICompatible:
112116
Text("DeepSeek (OpenAI Compatible)")
113117
case .openRouterOpenAICompatible:
@@ -464,6 +468,61 @@ struct ChatModelEditView: View {
464468
}
465469
}
466470
}
471+
472+
struct GitHubCopilotForm: View {
473+
@Perception.Bindable var store: StoreOf<ChatModelEdit>
474+
@State var isEditingCustomHeader = false
475+
476+
var body: some View {
477+
WithPerceptionTracking {
478+
TextField("Model Name", text: $store.modelName)
479+
.overlay(alignment: .trailing) {
480+
Picker(
481+
"",
482+
selection: $store.modelName,
483+
content: {
484+
if AvailableGitHubCopilotModel(rawValue: store.modelName) == nil {
485+
Text("Custom Model").tag(store.modelName)
486+
}
487+
ForEach(AvailableGitHubCopilotModel.allCases, id: \.self) { model in
488+
Text(model.rawValue).tag(model.rawValue)
489+
}
490+
}
491+
)
492+
.frame(width: 20)
493+
}
494+
495+
MaxTokensTextField(store: store)
496+
SupportsFunctionCallingToggle(store: store)
497+
498+
Toggle(isOn: $store.enforceMessageOrder) {
499+
Text("Enforce message order to be user/assistant alternated")
500+
}
501+
502+
Toggle(isOn: $store.openAICompatibleSupportsMultipartMessageContent) {
503+
Text("Support multi-part message content")
504+
}
505+
506+
Button("Custom Headers") {
507+
isEditingCustomHeader.toggle()
508+
}
509+
510+
VStack(alignment: .leading, spacing: 8) {
511+
Text(Image(systemName: "exclamationmark.triangle.fill")) + Text(
512+
" Please login in the GitHub Copilot settings to use the model."
513+
)
514+
515+
Text(Image(systemName: "exclamationmark.triangle.fill")) + Text(
516+
" This will call the APIs directly, which may not be allowed by GitHub. But it's used in other popular apps like Zed."
517+
)
518+
}
519+
.dynamicHeightTextInFormWorkaround()
520+
.padding(.vertical)
521+
}.sheet(isPresented: $isEditingCustomHeader) {
522+
CustomHeaderSettingsView(headers: $store.customHeaders)
523+
}
524+
}
525+
}
467526
}
468527

469528
#Preview("OpenAI") {

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ extension ChatModel: ManageableAIModel {
1313
case .googleAI: return "Google Generative AI"
1414
case .ollama: return "Ollama"
1515
case .claude: return "Claude"
16+
case .gitHubCopilot: return "GitHub Copilot"
1617
}
1718
}
1819

Tool/Sources/AIModel/ChatModel.swift

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ public struct ChatModel: Codable, Equatable, Identifiable {
2323
case googleAI
2424
case ollama
2525
case claude
26+
case gitHubCopilot
2627
}
2728

2829
public struct Info: Codable, Equatable {
@@ -178,6 +179,8 @@ public struct ChatModel: Codable, Equatable, Identifiable {
178179
let baseURL = info.baseURL
179180
if baseURL.isEmpty { return "https://api.anthropic.com/v1/messages" }
180181
return "\(baseURL)/v1/messages"
182+
case .gitHubCopilot:
183+
return "https://api.githubcopilot.com/chat/completions"
181184
}
182185
}
183186
}

Tool/Sources/GitHubCopilotService/GitHubCopilotExtension.swift

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -157,10 +157,10 @@ extension GitHubCopilotExtension {
157157
public let individual: Bool
158158
public let endpoints: Endpoints
159159
public let chat_enabled: Bool
160-
public let sku: String
160+
// public let sku: String
161161
// public let copilotignore_enabled: Bool
162162
// public let limited_user_quotas: String?
163-
public let tracking_id: String
163+
// public let tracking_id: String
164164
// public let xcode: Bool
165165
// public let limited_user_reset_date: String?
166166
// public let telemetry: String
@@ -183,7 +183,7 @@ extension GitHubCopilotExtension {
183183
public let api: String
184184
public let proxy: String
185185
public let telemetry: String
186-
public let origin_tracker: String
186+
// public let origin-tracker: String
187187
}
188188
}
189189

@@ -242,10 +242,18 @@ extension GitHubCopilotExtension {
242242
request.setValue("*/*", forHTTPHeaderField: "accept")
243243
request.setValue("gzip,deflate,br", forHTTPHeaderField: "accept-encoding")
244244

245-
let (data, _) = try await URLSession.shared.data(for: request)
246-
let newToken = try JSONDecoder().decode(Token.self, from: data)
247-
await MainActor.run { cachedToken = newToken }
248-
return newToken
245+
do {
246+
let (data, _) = try await URLSession.shared.data(for: request)
247+
if let jsonString = String(data: data, encoding: .utf8) {
248+
print(jsonString)
249+
}
250+
let newToken = try JSONDecoder().decode(Token.self, from: data)
251+
await MainActor.run { cachedToken = newToken }
252+
return newToken
253+
} catch {
254+
Logger.service.error(error.localizedDescription)
255+
throw error
256+
}
249257
}
250258
}
251259

Tool/Sources/OpenAIService/APIs/ChatCompletionsAPIBuilder.swift

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,11 @@ struct DefaultChatCompletionsAPIBuilder: ChatCompletionsAPIBuilder {
6262
endpoint: endpoint,
6363
requestBody: requestBody
6464
)
65+
case .gitHubCopilot:
66+
return GitHubCopilotChatCompletionsService(
67+
model: model,
68+
requestBody: requestBody
69+
)
6570
}
6671
}
6772

@@ -107,6 +112,11 @@ struct DefaultChatCompletionsAPIBuilder: ChatCompletionsAPIBuilder {
107112
endpoint: endpoint,
108113
requestBody: requestBody
109114
)
115+
case .gitHubCopilot:
116+
return GitHubCopilotChatCompletionsService(
117+
model: model,
118+
requestBody: requestBody
119+
)
110120
}
111121
}
112122
}
@@ -121,3 +131,4 @@ extension DependencyValues {
121131
set { self[ChatCompletionsAPIBuilderDependencyKey.self] = newValue }
122132
}
123133
}
134+
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
import AIModel
2+
import AsyncAlgorithms
3+
import ChatBasic
4+
import Foundation
5+
import GitHubCopilotService
6+
import Logger
7+
import Preferences
8+
9+
public enum AvailableGitHubCopilotModel: String, CaseIterable {
10+
case claude35sonnet = "claude-3.5-sonnet"
11+
case o1Mini = "o1-mini"
12+
case o1 = "o1"
13+
case gpt4Turbo = "gpt-4-turbo"
14+
case gpt4oMini = "gpt-4o-mini"
15+
case gpt4o = "gpt-4o"
16+
case gpt4 = "gpt-4"
17+
case gpt35Turbo = "gpt-3.5-turbo"
18+
19+
public var contextWindow: Int {
20+
switch self {
21+
case .claude35sonnet:
22+
return 200_000
23+
case .o1Mini:
24+
return 128_000
25+
case .o1:
26+
return 128_000
27+
case .gpt4Turbo:
28+
return 128_000
29+
case .gpt4oMini:
30+
return 128_000
31+
case .gpt4o:
32+
return 128_000
33+
case .gpt4:
34+
return 32_768
35+
case .gpt35Turbo:
36+
return 16_384
37+
}
38+
}
39+
}
40+
41+
/// Looks like it's used in many other popular repositories so maybe it's safe.
42+
actor GitHubCopilotChatCompletionsService: ChatCompletionsStreamAPI, ChatCompletionsAPI {
43+
44+
let chatModel: ChatModel
45+
let requestBody: ChatCompletionsRequestBody
46+
47+
init(
48+
model: ChatModel,
49+
requestBody: ChatCompletionsRequestBody
50+
) {
51+
var model = model
52+
model.format = .openAICompatible
53+
chatModel = model
54+
self.requestBody = requestBody
55+
}
56+
57+
func callAsFunction() async throws
58+
-> AsyncThrowingStream<ChatCompletionsStreamDataChunk, any Error>
59+
{
60+
let service = try await buildService()
61+
return try await service()
62+
}
63+
64+
func callAsFunction() async throws -> ChatCompletionResponseBody {
65+
let service = try await buildService()
66+
return try await service()
67+
}
68+
69+
private func buildService() async throws -> OpenAIChatCompletionsService {
70+
let token = try await GitHubCopilotExtension.fetchToken()
71+
72+
guard let endpoint = URL(string: token.endpoints.api + "/chat/completions") else {
73+
throw ChatGPTServiceError.endpointIncorrect
74+
}
75+
76+
return OpenAIChatCompletionsService(
77+
apiKey: token.token,
78+
model: chatModel,
79+
endpoint: endpoint,
80+
requestBody: requestBody
81+
) { request in
82+
83+
// POST /chat/completions HTTP/2
84+
// :authority: api.individual.githubcopilot.com
85+
// authorization: Bearer *
86+
// x-request-id: *
87+
// openai-organization: github-copilot
88+
// vscode-sessionid: *
89+
// vscode-machineid: *
90+
// editor-version: vscode/1.89.1
91+
// editor-plugin-version: Copilot for Xcode/0.35.5
92+
// copilot-language-server-version: 1.236.0
93+
// x-github-api-version: 2023-07-07
94+
// openai-intent: conversation-panel
95+
// content-type: application/json
96+
// user-agent: GithubCopilot/1.236.0
97+
// content-length: 9061
98+
// accept: */*
99+
// accept-encoding: gzip,deflate,br
100+
101+
request.setValue(
102+
"Copilot for Xcode/\(Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "unknown")",
103+
forHTTPHeaderField: "Editor-Version"
104+
)
105+
request.setValue("Bearer \(token.token)", forHTTPHeaderField: "Authorization")
106+
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
107+
request.setValue("vscode-chat", forHTTPHeaderField: "Copilot-Integration-Id")
108+
request.setValue("2023-07-07", forHTTPHeaderField: "X-Github-Api-Version")
109+
}
110+
}
111+
}
112+

Tool/Sources/OpenAIService/APIs/OpenAIChatCompletionsService.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -464,6 +464,8 @@ actor OpenAIChatCompletionsService: ChatCompletionsStreamAPI, ChatCompletionsAPI
464464
request.setValue("Bearer \(apiKey)", forHTTPHeaderField: "Authorization")
465465
case .azureOpenAI:
466466
request.setValue(apiKey, forHTTPHeaderField: "api-key")
467+
case .gitHubCopilot:
468+
break
467469
case .googleAI:
468470
assertionFailure("Unsupported")
469471
case .ollama:

Tool/Sources/OpenAIService/ChatGPTService.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -540,7 +540,7 @@ extension ChatGPTService {
540540
stream: Bool
541541
) -> ChatCompletionsRequestBody {
542542
let serviceSupportsFunctionCalling = switch model.format {
543-
case .openAI, .openAICompatible, .azureOpenAI:
543+
case .openAI, .openAICompatible, .azureOpenAI, .gitHubCopilot:
544544
model.info.supportsFunctionCalling
545545
case .ollama, .googleAI, .claude:
546546
false

0 commit comments

Comments
 (0)