Skip to content

Commit 5b02f1b

Browse files
committed
feat: add GitHub Copilot Provider that support agent mode #7010
- Implemented the Copilot component with authentication and model selection features. - Added tests for the Copilot component covering authentication states, error handling, and model management. - Updated localization files to include Copilot-related strings in multiple languages. - Enhanced the model validation functions to support Copilot models. - Modified the useSelectedModel hook to handle Copilot model selection. - Updated the index file to export the Copilot component.
1 parent ca6f261 commit 5b02f1b

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

41 files changed

+3411
-0
lines changed
Lines changed: 230 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,230 @@
1+
import { describe, it, expect } from "vitest"
2+
import {
3+
providerSettingsSchema,
4+
providerNamesSchema,
5+
providerNames,
6+
providerSettingsSchemaDiscriminated,
7+
MODEL_ID_KEYS,
8+
getModelId,
9+
type ProviderSettings,
10+
} from "../provider-settings.js"
11+
12+
describe("Provider Settings Schema - Copilot Provider", () => {
13+
describe("providerNames", () => {
14+
it("should include copilot in provider names", () => {
15+
expect(providerNames).toContain("copilot")
16+
})
17+
18+
it("should validate copilot as a valid provider name", () => {
19+
const result = providerNamesSchema.safeParse("copilot")
20+
expect(result.success).toBe(true)
21+
expect(result.data).toBe("copilot")
22+
})
23+
})
24+
25+
describe("discriminated union schema", () => {
26+
it("should validate copilot provider configuration", () => {
27+
const config = {
28+
apiProvider: "copilot" as const,
29+
copilotModelId: "claude-4",
30+
}
31+
32+
const result = providerSettingsSchemaDiscriminated.safeParse(config)
33+
expect(result.success).toBe(true)
34+
if (result.success && result.data.apiProvider === "copilot") {
35+
expect(result.data.apiProvider).toBe("copilot")
36+
expect(result.data.copilotModelId).toBe("claude-4")
37+
}
38+
})
39+
40+
it("should allow copilot configuration without model ID", () => {
41+
const config = {
42+
apiProvider: "copilot" as const,
43+
}
44+
45+
const result = providerSettingsSchemaDiscriminated.safeParse(config)
46+
expect(result.success).toBe(true)
47+
if (result.success && result.data.apiProvider === "copilot") {
48+
expect(result.data.apiProvider).toBe("copilot")
49+
expect(result.data.copilotModelId).toBeUndefined()
50+
}
51+
})
52+
53+
it("should reject invalid copilot model ID type", () => {
54+
const config = {
55+
apiProvider: "copilot",
56+
copilotModelId: 123,
57+
}
58+
59+
const result = providerSettingsSchemaDiscriminated.safeParse(config)
60+
expect(result.success).toBe(false)
61+
})
62+
})
63+
64+
describe("general provider settings schema", () => {
65+
it("should include copilotModelId in the schema", () => {
66+
const config: Partial<ProviderSettings> = {
67+
copilotModelId: "gpt-4",
68+
}
69+
70+
const result = providerSettingsSchema.safeParse(config)
71+
expect(result.success).toBe(true)
72+
if (result.success) {
73+
expect(result.data.copilotModelId).toBe("gpt-4")
74+
}
75+
})
76+
77+
it("should allow undefined copilotModelId", () => {
78+
const config: Partial<ProviderSettings> = {
79+
apiProvider: "anthropic",
80+
}
81+
82+
const result = providerSettingsSchema.safeParse(config)
83+
expect(result.success).toBe(true)
84+
if (result.success) {
85+
expect(result.data.copilotModelId).toBeUndefined()
86+
}
87+
})
88+
})
89+
90+
describe("MODEL_ID_KEYS", () => {
91+
it("should include copilotModelId in model ID keys", () => {
92+
expect(MODEL_ID_KEYS).toContain("copilotModelId")
93+
})
94+
})
95+
96+
describe("getModelId function", () => {
97+
it("should return copilotModelId for copilot provider", () => {
98+
const settings: ProviderSettings = {
99+
apiProvider: "copilot",
100+
copilotModelId: "claude-4",
101+
}
102+
103+
const modelId = getModelId(settings)
104+
expect(modelId).toBe("claude-4")
105+
})
106+
107+
it("should return undefined for copilot provider without model ID", () => {
108+
const settings: ProviderSettings = {
109+
apiProvider: "copilot",
110+
}
111+
112+
const modelId = getModelId(settings)
113+
expect(modelId).toBeUndefined()
114+
})
115+
116+
it("should fallback to apiModelId for copilot if copilotModelId not set", () => {
117+
const settings: ProviderSettings = {
118+
apiProvider: "copilot",
119+
apiModelId: "fallback-model",
120+
}
121+
122+
const modelId = getModelId(settings)
123+
expect(modelId).toBe("fallback-model")
124+
})
125+
})
126+
127+
describe("schema validation edge cases", () => {
128+
it("should handle empty string copilotModelId", () => {
129+
const config = {
130+
apiProvider: "copilot",
131+
copilotModelId: "",
132+
}
133+
134+
const result = providerSettingsSchemaDiscriminated.safeParse(config)
135+
expect(result.success).toBe(true)
136+
if (result.success && result.data.apiProvider === "copilot") {
137+
expect(result.data.copilotModelId).toBe("")
138+
}
139+
})
140+
141+
it("should handle very long copilotModelId", () => {
142+
const longModelId = "a".repeat(1000)
143+
const config = {
144+
apiProvider: "copilot",
145+
copilotModelId: longModelId,
146+
}
147+
148+
const result = providerSettingsSchemaDiscriminated.safeParse(config)
149+
expect(result.success).toBe(true)
150+
if (result.success && result.data.apiProvider === "copilot") {
151+
expect(result.data.copilotModelId).toBe(longModelId)
152+
}
153+
})
154+
155+
it("should handle special characters in copilotModelId", () => {
156+
const specialModelId = "claude-4.1-beta@2024/01/01"
157+
const config = {
158+
apiProvider: "copilot",
159+
copilotModelId: specialModelId,
160+
}
161+
162+
const result = providerSettingsSchemaDiscriminated.safeParse(config)
163+
expect(result.success).toBe(true)
164+
if (result.success && result.data.apiProvider === "copilot") {
165+
expect(result.data.copilotModelId).toBe(specialModelId)
166+
}
167+
})
168+
})
169+
170+
describe("type safety", () => {
171+
it("should maintain type safety for ProviderSettings", () => {
172+
const settings: ProviderSettings = {
173+
apiProvider: "copilot",
174+
copilotModelId: "claude-4",
175+
}
176+
177+
// This should compile without errors
178+
expect(settings.apiProvider).toBe("copilot")
179+
expect(settings.copilotModelId).toBe("claude-4")
180+
})
181+
182+
it("should allow copilot in discriminated union", () => {
183+
const config = {
184+
apiProvider: "copilot" as const,
185+
copilotModelId: "gpt-4",
186+
}
187+
188+
const result = providerSettingsSchemaDiscriminated.safeParse(config)
189+
expect(result.success).toBe(true)
190+
if (result.success) {
191+
// TypeScript should infer the correct type
192+
expect(result.data.apiProvider).toBe("copilot")
193+
}
194+
})
195+
})
196+
197+
describe("compatibility with other providers", () => {
198+
it("should not interfere with other provider configurations", () => {
199+
const anthropicConfig = {
200+
apiProvider: "anthropic",
201+
anthropicApiKey: "test-key",
202+
}
203+
204+
const result = providerSettingsSchemaDiscriminated.safeParse(anthropicConfig)
205+
expect(result.success).toBe(true)
206+
if (result.success) {
207+
expect(result.data.apiProvider).toBe("anthropic")
208+
// copilotModelId shouldn't exist on other providers
209+
expect("copilotModelId" in result.data).toBe(false)
210+
}
211+
})
212+
213+
it("should handle mixed provider settings in general schema", () => {
214+
const mixedConfig = {
215+
apiProvider: "anthropic",
216+
anthropicApiKey: "test-key",
217+
copilotModelId: "claude-4", // This should be allowed but ignored
218+
openRouterModelId: "other-model",
219+
}
220+
221+
const result = providerSettingsSchema.safeParse(mixedConfig)
222+
expect(result.success).toBe(true)
223+
if (result.success) {
224+
expect(result.data.apiProvider).toBe("anthropic")
225+
expect(result.data.copilotModelId).toBe("claude-4")
226+
expect(result.data.openRouterModelId).toBe("other-model")
227+
}
228+
})
229+
})
230+
})

packages/types/src/provider-settings.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ export const providerNames = [
4747
"zai",
4848
"fireworks",
4949
"io-intelligence",
50+
"copilot",
5051
] as const
5152

5253
export const providerNamesSchema = z.enum(providerNames)
@@ -288,6 +289,10 @@ const ioIntelligenceSchema = apiModelIdProviderModelSchema.extend({
288289
ioIntelligenceApiKey: z.string().optional(),
289290
})
290291

292+
const copilotSchema = baseProviderSettingsSchema.extend({
293+
copilotModelId: z.string().optional(),
294+
})
295+
291296
const defaultSchema = z.object({
292297
apiProvider: z.undefined(),
293298
})
@@ -324,6 +329,7 @@ export const providerSettingsSchemaDiscriminated = z.discriminatedUnion("apiProv
324329
zaiSchema.merge(z.object({ apiProvider: z.literal("zai") })),
325330
fireworksSchema.merge(z.object({ apiProvider: z.literal("fireworks") })),
326331
ioIntelligenceSchema.merge(z.object({ apiProvider: z.literal("io-intelligence") })),
332+
copilotSchema.merge(z.object({ apiProvider: z.literal("copilot") })),
327333
defaultSchema,
328334
])
329335

@@ -360,6 +366,7 @@ export const providerSettingsSchema = z.object({
360366
...zaiSchema.shape,
361367
...fireworksSchema.shape,
362368
...ioIntelligenceSchema.shape,
369+
...copilotSchema.shape,
363370
...codebaseIndexProviderSchema.shape,
364371
})
365372

@@ -386,6 +393,7 @@ export const MODEL_ID_KEYS: Partial<keyof ProviderSettings>[] = [
386393
"litellmModelId",
387394
"huggingFaceModelId",
388395
"ioIntelligenceModelId",
396+
"copilotModelId",
389397
]
390398

391399
export const getModelId = (settings: ProviderSettings): string | undefined => {

src/api/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ import {
3636
DoubaoHandler,
3737
ZAiHandler,
3838
FireworksHandler,
39+
CopilotHandler,
3940
} from "./providers"
4041

4142
export interface SingleCompletionHandler {
@@ -140,6 +141,8 @@ export function buildApiHandler(configuration: ProviderSettings): ApiHandler {
140141
return new FireworksHandler(options)
141142
case "io-intelligence":
142143
return new IOIntelligenceHandler(options)
144+
case "copilot":
145+
return new CopilotHandler(options)
143146
default:
144147
apiProvider satisfies "gemini-cli" | undefined
145148
return new AnthropicHandler(options)

0 commit comments

Comments
 (0)