Skip to content

feat: add GitHub Copilot Provider that support agent mode #7010 #7072

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 2 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
230 changes: 230 additions & 0 deletions packages/types/src/__tests__/provider-settings.copilot.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,230 @@
import { describe, it, expect } from "vitest"
import {
providerSettingsSchema,
providerNamesSchema,
providerNames,
providerSettingsSchemaDiscriminated,
MODEL_ID_KEYS,
getModelId,
type ProviderSettings,
} from "../provider-settings.js"

describe("Provider Settings Schema - Copilot Provider", () => {
describe("providerNames", () => {
it("should include copilot in provider names", () => {
expect(providerNames).toContain("copilot")
})

it("should validate copilot as a valid provider name", () => {
const result = providerNamesSchema.safeParse("copilot")
expect(result.success).toBe(true)
expect(result.data).toBe("copilot")
})
})

describe("discriminated union schema", () => {
it("should validate copilot provider configuration", () => {
const config = {
apiProvider: "copilot" as const,
copilotModelId: "claude-4",
}

const result = providerSettingsSchemaDiscriminated.safeParse(config)
expect(result.success).toBe(true)
if (result.success && result.data.apiProvider === "copilot") {
expect(result.data.apiProvider).toBe("copilot")
expect(result.data.copilotModelId).toBe("claude-4")
}
})

it("should allow copilot configuration without model ID", () => {
const config = {
apiProvider: "copilot" as const,
}

const result = providerSettingsSchemaDiscriminated.safeParse(config)
expect(result.success).toBe(true)
if (result.success && result.data.apiProvider === "copilot") {
expect(result.data.apiProvider).toBe("copilot")
expect(result.data.copilotModelId).toBeUndefined()
}
})

it("should reject invalid copilot model ID type", () => {
const config = {
apiProvider: "copilot",
copilotModelId: 123,
}

const result = providerSettingsSchemaDiscriminated.safeParse(config)
expect(result.success).toBe(false)
})
})

describe("general provider settings schema", () => {
it("should include copilotModelId in the schema", () => {
const config: Partial<ProviderSettings> = {
copilotModelId: "gpt-4",
}

const result = providerSettingsSchema.safeParse(config)
expect(result.success).toBe(true)
if (result.success) {
expect(result.data.copilotModelId).toBe("gpt-4")
}
})

it("should allow undefined copilotModelId", () => {
const config: Partial<ProviderSettings> = {
apiProvider: "anthropic",
}

const result = providerSettingsSchema.safeParse(config)
expect(result.success).toBe(true)
if (result.success) {
expect(result.data.copilotModelId).toBeUndefined()
}
})
})

describe("MODEL_ID_KEYS", () => {
it("should include copilotModelId in model ID keys", () => {
expect(MODEL_ID_KEYS).toContain("copilotModelId")
})
})

describe("getModelId function", () => {
it("should return copilotModelId for copilot provider", () => {
const settings: ProviderSettings = {
apiProvider: "copilot",
copilotModelId: "claude-4",
}

const modelId = getModelId(settings)
expect(modelId).toBe("claude-4")
})

it("should return undefined for copilot provider without model ID", () => {
const settings: ProviderSettings = {
apiProvider: "copilot",
}

const modelId = getModelId(settings)
expect(modelId).toBeUndefined()
})

it("should fallback to apiModelId for copilot if copilotModelId not set", () => {
const settings: ProviderSettings = {
apiProvider: "copilot",
apiModelId: "fallback-model",
}

const modelId = getModelId(settings)
expect(modelId).toBe("fallback-model")
})
})

describe("schema validation edge cases", () => {
it("should handle empty string copilotModelId", () => {
const config = {
apiProvider: "copilot",
copilotModelId: "",
}

const result = providerSettingsSchemaDiscriminated.safeParse(config)
expect(result.success).toBe(true)
if (result.success && result.data.apiProvider === "copilot") {
expect(result.data.copilotModelId).toBe("")
}
})

it("should handle very long copilotModelId", () => {
const longModelId = "a".repeat(1000)
const config = {
apiProvider: "copilot",
copilotModelId: longModelId,
}

const result = providerSettingsSchemaDiscriminated.safeParse(config)
expect(result.success).toBe(true)
if (result.success && result.data.apiProvider === "copilot") {
expect(result.data.copilotModelId).toBe(longModelId)
}
})

it("should handle special characters in copilotModelId", () => {
const specialModelId = "claude-4.1-beta@2024/01/01"
const config = {
apiProvider: "copilot",
copilotModelId: specialModelId,
}

const result = providerSettingsSchemaDiscriminated.safeParse(config)
expect(result.success).toBe(true)
if (result.success && result.data.apiProvider === "copilot") {
expect(result.data.copilotModelId).toBe(specialModelId)
}
})
})

describe("type safety", () => {
it("should maintain type safety for ProviderSettings", () => {
const settings: ProviderSettings = {
apiProvider: "copilot",
copilotModelId: "claude-4",
}

// This should compile without errors
expect(settings.apiProvider).toBe("copilot")
expect(settings.copilotModelId).toBe("claude-4")
})

it("should allow copilot in discriminated union", () => {
const config = {
apiProvider: "copilot" as const,
copilotModelId: "gpt-4",
}

const result = providerSettingsSchemaDiscriminated.safeParse(config)
expect(result.success).toBe(true)
if (result.success) {
// TypeScript should infer the correct type
expect(result.data.apiProvider).toBe("copilot")
}
})
})

describe("compatibility with other providers", () => {
it("should not interfere with other provider configurations", () => {
const anthropicConfig = {
apiProvider: "anthropic",
anthropicApiKey: "test-key",
}

const result = providerSettingsSchemaDiscriminated.safeParse(anthropicConfig)
expect(result.success).toBe(true)
if (result.success) {
expect(result.data.apiProvider).toBe("anthropic")
// copilotModelId shouldn't exist on other providers
expect("copilotModelId" in result.data).toBe(false)
}
})

it("should handle mixed provider settings in general schema", () => {
const mixedConfig = {
apiProvider: "anthropic",
anthropicApiKey: "test-key",
copilotModelId: "claude-4", // This should be allowed but ignored
openRouterModelId: "other-model",
}

const result = providerSettingsSchema.safeParse(mixedConfig)
expect(result.success).toBe(true)
if (result.success) {
expect(result.data.apiProvider).toBe("anthropic")
expect(result.data.copilotModelId).toBe("claude-4")
expect(result.data.openRouterModelId).toBe("other-model")
}
})
})
})
8 changes: 8 additions & 0 deletions packages/types/src/provider-settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ export const providerNames = [
"zai",
"fireworks",
"io-intelligence",
"copilot",
] as const

export const providerNamesSchema = z.enum(providerNames)
Expand Down Expand Up @@ -288,6 +289,10 @@ const ioIntelligenceSchema = apiModelIdProviderModelSchema.extend({
ioIntelligenceApiKey: z.string().optional(),
})

const copilotSchema = baseProviderSettingsSchema.extend({
copilotModelId: z.string().optional(),
})

const defaultSchema = z.object({
apiProvider: z.undefined(),
})
Expand Down Expand Up @@ -324,6 +329,7 @@ export const providerSettingsSchemaDiscriminated = z.discriminatedUnion("apiProv
zaiSchema.merge(z.object({ apiProvider: z.literal("zai") })),
fireworksSchema.merge(z.object({ apiProvider: z.literal("fireworks") })),
ioIntelligenceSchema.merge(z.object({ apiProvider: z.literal("io-intelligence") })),
copilotSchema.merge(z.object({ apiProvider: z.literal("copilot") })),
defaultSchema,
])

Expand Down Expand Up @@ -360,6 +366,7 @@ export const providerSettingsSchema = z.object({
...zaiSchema.shape,
...fireworksSchema.shape,
...ioIntelligenceSchema.shape,
...copilotSchema.shape,
...codebaseIndexProviderSchema.shape,
})

Expand All @@ -386,6 +393,7 @@ export const MODEL_ID_KEYS: Partial<keyof ProviderSettings>[] = [
"litellmModelId",
"huggingFaceModelId",
"ioIntelligenceModelId",
"copilotModelId",
]

export const getModelId = (settings: ProviderSettings): string | undefined => {
Expand Down
3 changes: 3 additions & 0 deletions src/api/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ import {
DoubaoHandler,
ZAiHandler,
FireworksHandler,
CopilotHandler,
} from "./providers"

export interface SingleCompletionHandler {
Expand Down Expand Up @@ -140,6 +141,8 @@ export function buildApiHandler(configuration: ProviderSettings): ApiHandler {
return new FireworksHandler(options)
case "io-intelligence":
return new IOIntelligenceHandler(options)
case "copilot":
return new CopilotHandler(options)
default:
apiProvider satisfies "gemini-cli" | undefined
return new AnthropicHandler(options)
Expand Down
Loading
Loading