Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
ca4fef5
feat(api): add GitHub Copilot provider with sign-in and live model ca…
KarthickMahalingam Oct 8, 2026
59fcf6e
chore: retrigger CI
KarthickMahalingam Oct 8, 2026
83bf0d9
test(api): expand GitHub Copilot provider coverage
KarthickMahalingam Oct 8, 2026
4d06cfe
test(webview): cover remaining Copilot message handler paths
KarthickMahalingam Oct 8, 2026
ab0e7ea
fix(api): address review feedback on GitHub Copilot provider
KarthickMahalingam Oct 9, 2026
fe26574
fix(webview): drop stale GitHub Copilot sign-in results
KarthickMahalingam Oct 9, 2026
77a8abc
fix(webview): scope stale Copilot sign-in guard to each view
KarthickMahalingam Oct 9, 2026
e8fdf08
fix(copilot): address review feedback on refresh races and image support
KarthickMahalingam Oct 11, 2026
3b619c9
fix(copilot): address remaining review feedback on account and model …
KarthickMahalingam Oct 11, 2026
84ed4e8
fix(webview): style the VS Code LM model note as advisory text
KarthickMahalingam Oct 11, 2026
e6607a1
test(webview): update GitHub Copilot settings baselines for the advis…
KarthickMahalingam Oct 11, 2026
a47bf1d
docs(copilot): document the context window when the host reports no l…
KarthickMahalingam Oct 12, 2026
ecaa7ff
fix(copilot): address pre-merge checks
KarthickMahalingam Oct 12, 2026
342183e
fix(cli): read the GitHub Copilot model from its saved selector
KarthickMahalingam Oct 12, 2026
ea009ef
fix(copilot): send a consent justification with single-shot completions
KarthickMahalingam Oct 12, 2026
de8d086
test(copilot): fix vscode-lm spec for merge check
KarthickMahalingam Oct 12, 2026
c6c9749
test(copilot): fix extension spec for merge check
KarthickMahalingam Oct 12, 2026
4780ed9
fix(copilot): dispose completion resources and test Copilot Chat acti…
KarthickMahalingam Oct 12, 2026
5c9854f
fix(copilot): dispose task API handlers and honor completion abort/ti…
KarthickMahalingam Oct 12, 2026
31692bd
style: format Task.spec.ts
KarthickMahalingam Oct 12, 2026
3631d60
test(copilot): yield from the delayed stream to satisfy require-yield
KarthickMahalingam Oct 12, 2026
757082f
fix(task): dispose API handlers without depending on the api module e…
KarthickMahalingam Oct 12, 2026
6803dc6
fix(task): release replaced API handlers when in-flight work settles
KarthickMahalingam Oct 12, 2026
fac9b6a
fix(vscode-lm): cancel streaming requests from the task's abort signal
KarthickMahalingam Oct 12, 2026
ad49325
fix(vscode-lm): attach the abort listener inside the request's cleanu…
KarthickMahalingam Oct 12, 2026
8df7417
feat(copilot): report host tool support, and release pre-send request…
KarthickMahalingam Oct 12, 2026
fd2f770
fix(copilot): cancel token counting on abort, and show tool support o…
KarthickMahalingam Oct 13, 2026
5c5246b
docs(readme): summarize the GitHub Copilot provider in What's New; dr…
KarthickMahalingam Oct 14, 2026
72b091c
fix(copilot): drop stale passive model refreshes and release temporar…
KarthickMahalingam Oct 14, 2026
fd427d6
chore(copilot): trim explanatory comments to stay within the mutation…
KarthickMahalingam Oct 14, 2026
39c2103
fix(copilot): order manual model refreshes against host broadcasts
KarthickMahalingam Oct 14, 2026
b1a6459
fix(copilot): keep a failed broadcast or account lookup from losing v…
KarthickMahalingam Oct 15, 2026
95ae69e
Merge branch 'main' into feat/github-copilot-provider
KarthickMahalingam Oct 15, 2026
e682752
chore: trigger CI
KarthickMahalingam Oct 15, 2026
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
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ Zoo Code builds on the foundation created by Roo Code and continues to expand it

## What's New in v3.88.0

- 🐙 **GitHub Copilot provider:** Sign in to GitHub and use your Copilot models directly in Zoo Code.
- ✨ **Provider and model support:** Use Grok Build and improved custom-model support. This release also fixes Bedrock model limits and sends the Codex verbosity setting.
- 🧭 **Task reliability:** This release fixes cancellation handling, tool-turn continuation, and task-message saving.
- ⚡ **Faster editing and chat:** File writes no longer use artificial delays by default. Chat messages carry less file data, and code actions reach the last active chat.
Expand Down
42 changes: 42 additions & 0 deletions apps/cli/src/lib/utils/__tests__/context-window.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,48 @@ describe("getContextWindow", () => {
expect(getContextWindow(routerModels, config)).toBe(123_456)
})

describe("GitHub Copilot", () => {
const provider = providerIdentifiers.githubCopilot
const routerModels = { [provider]: { "selected-model": { contextWindow: 123_456 } } }

it("takes the model from the saved selector, where Copilot keeps it", () => {
const config: ProviderSettings = {
apiProvider: provider,
vsCodeLmModelSelector: { vendor: "copilot", id: "selected-model" },
}

expect(getContextWindow(routerModels, config)).toBe(123_456)
})

it("ignores apiModelId, which Copilot profiles do not use", () => {
const config: ProviderSettings = {
apiProvider: provider,
apiModelId: "other-model",
vsCodeLmModelSelector: { vendor: "copilot", id: "selected-model" },
}

expect(getContextWindow(routerModels, config)).toBe(123_456)
})

it.each([
["no selector", undefined],
["a selector that names a family but no id", { vendor: "copilot", family: "gpt-5.5" }],
])("returns the default for %s", (_label, vsCodeLmModelSelector) => {
const config: ProviderSettings = { apiProvider: provider, vsCodeLmModelSelector }

expect(getContextWindow(routerModels, config)).toBe(DEFAULT_CONTEXT_WINDOW)
})

it("returns the default when the selected model is not in the list", () => {
const config: ProviderSettings = {
apiProvider: provider,
vsCodeLmModelSelector: { vendor: "copilot", id: "unlisted-model" },
}

expect(getContextWindow(routerModels, config)).toBe(DEFAULT_CONTEXT_WINDOW)
})
})

it("uses apiModelId for providers without a specialized model field", () => {
const config: ProviderSettings = {
apiProvider: providerIdentifiers.anthropic,
Expand Down
2 changes: 2 additions & 0 deletions apps/cli/src/lib/utils/context-window.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,8 @@ function getModelIdForProvider(config: ProviderSettings): string | undefined {
return config.nanoGptModelId
case providerIdentifiers.zooGateway:
return config.zooGatewayModelId
case providerIdentifiers.githubCopilot:
return config.vsCodeLmModelSelector?.id
case providerIdentifiers.anthropic:
case providerIdentifiers.bedrock:
case providerIdentifiers.baseten:
Expand Down
4 changes: 4 additions & 0 deletions packages/types/src/__tests__/provider-default-model.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,10 @@ describe("getProviderDefaultModelId", () => {
expect(getProviderDefaultModelId(providerIdentifiers.vscodeLm)).toBe(vscodeLlmDefaultModelId)
})

it("gives GitHub Copilot the same default as the VS Code LM provider", () => {
expect(getProviderDefaultModelId(providerIdentifiers.githubCopilot)).toBe(vscodeLlmDefaultModelId)
})

it("uses the canonical OpenAI Native default model", () => {
expect(getProviderDefaultModelId(providerIdentifiers.openaiNative)).toBe(openAiNativeDefaultModelId)
})
Expand Down
3 changes: 2 additions & 1 deletion packages/types/src/__tests__/provider-identifiers.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ const expectedProviderIdentifiers = [
"ollama",
"lmstudio",
"vscode-lm",
"github-copilot",
"openai",
"fake-ai",
"anthropic",
Expand Down Expand Up @@ -114,7 +115,7 @@ describe("provider identifiers", () => {
providerIdentifiers.kimiCode,
])
expect(localProviders).toEqual([providerIdentifiers.ollama, providerIdentifiers.lmstudio])
expect(internalProviders).toEqual([providerIdentifiers.vscodeLm])
expect(internalProviders).toEqual([providerIdentifiers.vscodeLm, providerIdentifiers.githubCopilot])
expect(customProviders).toEqual([providerIdentifiers.openai])
expect(fauxProviders).toEqual([providerIdentifiers.fakeAi])
})
Expand Down
42 changes: 41 additions & 1 deletion packages/types/src/__tests__/provider-model-id.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
import { retiredProviderIdentifiers } from "../provider-identifiers.js"
import { getModelId, modelIdKeys, providerIdentifiers, type ProviderSettings } from "../index.js"
import {
getModelId,
modelIdKeys,
providerIdentifiers,
providerSettingsSchema,
type ProviderSettings,
} from "../index.js"

const expectedModelIdKeys = [
"apiModelId",
Expand Down Expand Up @@ -54,6 +60,40 @@ describe("getModelId", () => {
expect(getModelId(settings)).toBe("vscode-model")
})

it("preserves the exact Copilot model ID through the provider settings schema", () => {
const settings: ProviderSettings = {
apiProvider: providerIdentifiers.githubCopilot,
vsCodeLmModelSelector: { vendor: "copilot", family: "gpt-4o", id: "copilot-model", version: "1" },
}
expect(getModelId(settings)).toBe("copilot-model")
})

it("persists only the selector's identity, never live model capabilities", () => {
const saved = providerSettingsSchema.parse({
apiProvider: providerIdentifiers.githubCopilot,
vsCodeLmModelSelector: { vendor: "copilot", id: "dynamic-model", family: "dynamic-model", version: "1" },
})
expect(saved.vsCodeLmModelSelector).toEqual({
vendor: "copilot",
id: "dynamic-model",
family: "dynamic-model",
version: "1",
})
})

it("drops a stale capability snapshot from profiles saved by earlier builds", () => {
const saved = providerSettingsSchema.parse({
apiProvider: providerIdentifiers.githubCopilot,
vsCodeLmModelSelector: {
vendor: "copilot",
id: "old-model",
modelInfo: { contextWindow: 1, supportsImages: false, supportsPromptCache: false },
},
})
expect(getModelId(saved)).toBe("old-model")
expect(saved.vsCodeLmModelSelector).not.toHaveProperty("modelInfo")
})

it("uses openAiModelId for OpenAI Compatible", () => {
const settings: ProviderSettings = {
apiProvider: providerIdentifiers.openai,
Expand Down
3 changes: 3 additions & 0 deletions packages/types/src/model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,9 @@ export const modelInfoSchema = z.object({
maxThinkingTokens: z.number().nullish(),
contextWindow: z.number(),
supportsImages: z.boolean().optional(),
// Whether the model accepts tool (function) calls, as its host reports it. Only hosts that
// report this set it; unset means "not reported", not "unsupported".
supportsToolCalling: z.boolean().optional(),
supportsPromptCache: z.boolean(),
// Optional default prompt cache retention policy for providers that support it.
// When set to "24h", extended prompt caching will be requested; when omitted
Expand Down
1 change: 1 addition & 0 deletions packages/types/src/provider-identifiers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ export const providerIdentifiers = {
ollama: "ollama",
lmstudio: "lmstudio",
vscodeLm: "vscode-lm",
githubCopilot: "github-copilot",
openai: "openai",
fakeAi: "fake-ai",
anthropic: "anthropic",
Expand Down
7 changes: 6 additions & 1 deletion packages/types/src/provider-settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@
* model list.
*/

export const internalProviders = [providerIdentifiers.vscodeLm] as const
export const internalProviders = [providerIdentifiers.vscodeLm, providerIdentifiers.githubCopilot] as const

export type InternalProvider = (typeof internalProviders)[number]

Expand Down Expand Up @@ -516,6 +516,11 @@
label: "VS Code LM API",
models: Object.keys(vscodeLlmModels),
},
[providerIdentifiers.githubCopilot]: {
id: providerIdentifiers.githubCopilot,
label: "GitHub Copilot",

Check warning on line 521 in packages/types/src/provider-settings.ts

View workflow job for this annotation

GitHub Actions / mutation-diff

Mutation test advisory

packages/types/src/provider-settings.ts:521: Survived StringLiteral mutant (replacement: ""). See the job summary for the complete list and resolution guidance.
models: Object.keys(vscodeLlmModels),
},
[providerIdentifiers.xai]: { id: providerIdentifiers.xai, label: "xAI (Grok)", models: Object.keys(xaiModels) },
[providerIdentifiers.zai]: {
id: providerIdentifiers.zai,
Expand Down
3 changes: 2 additions & 1 deletion packages/types/src/provider-settings/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { bedrockProviderDefinition } from "./bedrock.js"
import { vertexProviderDefinition } from "./vertex.js"
import { openAiProviderDefinition } from "./openai.js"
import { ollamaProviderDefinition } from "./ollama.js"
import { vsCodeLmProviderDefinition } from "./vscode-lm.js"
import { githubCopilotProviderDefinition, vsCodeLmProviderDefinition } from "./vscode-lm.js"
import { lmStudioProviderDefinition } from "./lm-studio.js"
import { geminiProviderDefinition } from "./gemini.js"
import { geminiCliProviderDefinition } from "./gemini-cli.js"
Expand Down Expand Up @@ -56,6 +56,7 @@ export const providerDefinitionList = [
openAiProviderDefinition,
ollamaProviderDefinition,
vsCodeLmProviderDefinition,
githubCopilotProviderDefinition,
lmStudioProviderDefinition,
geminiProviderDefinition,
geminiCliProviderDefinition,
Expand Down
30 changes: 19 additions & 11 deletions packages/types/src/provider-settings/vscode-lm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,26 @@ import { z } from "zod"
import { providerIdentifiers } from "../provider-identifiers.js"
import { baseProviderSettingsShape, createProviderDefinition } from "./common.js"

const schema = {
...baseProviderSettingsShape,
vsCodeLmModelSelector: z
.object({
vendor: z.string().optional(),
family: z.string().optional(),
version: z.string().optional(),
id: z.string().optional(),
})
.optional(),
}

export const vsCodeLmProviderDefinition = createProviderDefinition({
apiProvider: providerIdentifiers.vscodeLm,
getModelId: (settings) => settings.vsCodeLmModelSelector?.id,
schema: {
...baseProviderSettingsShape,
vsCodeLmModelSelector: z
.object({
vendor: z.string().optional(),
family: z.string().optional(),
version: z.string().optional(),
id: z.string().optional(),
})
.optional(),
},
schema,
})

export const githubCopilotProviderDefinition = createProviderDefinition({
apiProvider: providerIdentifiers.githubCopilot,
getModelId: (settings) => settings.vsCodeLmModelSelector?.id,
schema,
})
1 change: 1 addition & 0 deletions packages/types/src/providers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,7 @@ export function getProviderDefaultModelId(
case providerIdentifiers.lmstudio:
return NO_DEFAULT_MODEL_ID
case providerIdentifiers.vscodeLm:
case providerIdentifiers.githubCopilot:
return vscodeLlmDefaultModelId
case providerIdentifiers.sambanova:
return sambaNovaDefaultModelId
Expand Down
37 changes: 36 additions & 1 deletion packages/types/src/providers/vscode-llm.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,50 @@
import { z } from "zod"

import type { ModelInfo } from "../model.js"
import { openAiModelInfoSaneDefaults } from "./openai.js"

export const vsCodeLmModelsMessageTypes = ["requestVsCodeLmModels", "vsCodeLmModels"] as const
export const vsCodeLmModelsMessageTypes = [
"requestVsCodeLmModels",
"vsCodeLmModels",
"githubCopilotSignIn",
"githubCopilotReconnect",
"githubCopilotManageAccount",
"githubCopilotSignInResult",
"githubCopilotModels",
] as const

export const vsCodeLmModelsMessageTypeSchema = z.enum(vsCodeLmModelsMessageTypes)

export const VsCodeLmModelsMessageType = vsCodeLmModelsMessageTypeSchema.enum

export type VsCodeLmModelsMessageType = z.infer<typeof vsCodeLmModelsMessageTypeSchema>

const githubCopilotVendor = "copilot"

Check warning on line 22 in packages/types/src/providers/vscode-llm.ts

View workflow job for this annotation

GitHub Actions / mutation-diff

Mutation test advisory

packages/types/src/providers/vscode-llm.ts:22: Survived StringLiteral mutant (replacement: ""). See the job summary for the complete list and resolution guidance.

/** Identifiers for the language models and sign-in flow GitHub Copilot contributes to VS Code. */
export const githubCopilotLanguageModel = {
vendor: githubCopilotVendor,
/** Selector matching every model the Copilot extension contributes. */
selector: { vendor: githubCopilotVendor },
authProviderId: "github",
/** In Copilot Chat's own preference order, so Zoo recognizes the session Copilot actually uses. */
authScopeSets: [["user:email"], ["read:user"]],
chatExtensionId: "GitHub.copilot-chat",
/** Public Command Palette command; VS Code offers no API to sign out, so Zoo defers to this UI. */
manageAccountsCommandId: "workbench.action.manageAccounts",
} as const

/**
* Baseline for a model whose capabilities are not yet known. Unlike `openAiModelInfoSaneDefaults` it
* leaves vision unset, so a capability nobody reported is not mistaken for one that is unsupported.
*/
export const vscodeLlmBaselineModelInfo: ModelInfo = {

Check warning on line 41 in packages/types/src/providers/vscode-llm.ts

View workflow job for this annotation

GitHub Actions / mutation-diff

Mutation test advisory

packages/types/src/providers/vscode-llm.ts:41: Survived ObjectLiteral mutant (replacement: {}). See the job summary for the complete list and resolution guidance.
contextWindow: openAiModelInfoSaneDefaults.contextWindow,
supportsPromptCache: false,

Check warning on line 43 in packages/types/src/providers/vscode-llm.ts

View workflow job for this annotation

GitHub Actions / mutation-diff

Mutation test advisory

packages/types/src/providers/vscode-llm.ts:43: Survived BooleanLiteral mutant (replacement: true). See the job summary for the complete list and resolution guidance.
inputPrice: 0,
outputPrice: 0,
}

export type VscodeLlmModelId = keyof typeof vscodeLlmModels

export const vscodeLlmDefaultModelId: VscodeLlmModelId = "claude-sonnet-4.5"
Expand Down
18 changes: 16 additions & 2 deletions packages/types/src/vscode-extension-host.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import type { CloudUserInfo, CloudOrganizationMembership, OrganizationAllowList,
import type { SerializedCustomToolDefinition } from "./custom-tool.js"
import type { GitCommit } from "./git.js"
import type { McpServer } from "./mcp.js"
import { RouterModelsMessageType, type ModelRecord, type RouterModels } from "./model.js"
import { RouterModelsMessageType, type ModelInfo, type ModelRecord, type RouterModels } from "./model.js"
import { LmStudioModelsMessageType } from "./providers/lm-studio.js"
import { OllamaModelsMessageType } from "./providers/ollama.js"
import { OpenAiModelsMessageType } from "./providers/openai.js"
Expand Down Expand Up @@ -48,6 +48,8 @@ export interface ExtensionMessage {
| typeof OllamaModelsMessageType.ollamaModels
| typeof LmStudioModelsMessageType.lmStudioModels
| typeof VsCodeLmModelsMessageType.vsCodeLmModels
| typeof VsCodeLmModelsMessageType.githubCopilotModels
| typeof VsCodeLmModelsMessageType.githubCopilotSignInResult
| "vsCodeLmApiAvailable"
| "updatePrompt"
| "systemPrompt"
Expand Down Expand Up @@ -146,7 +148,16 @@ export interface ExtensionMessage {
openAiModels?: string[]
ollamaModels?: ModelRecord
lmStudioModels?: ModelRecord
vsCodeLmModels?: { vendor?: string; family?: string; version?: string; id?: string }[]
vsCodeLmModels?: {
vendor?: string
family?: string
version?: string
id?: string
name?: string
maxInputTokens?: number
modelInfo?: ModelInfo
}[]
githubCopilotAccount?: string | null
mcpServers?: McpServer[]
commits?: GitCommit[]
listApiConfig?: ProviderSettingsEntry[]
Expand Down Expand Up @@ -499,6 +510,9 @@ export interface WebviewMessage {
| typeof LmStudioModelsMessageType.requestLmStudioModels
| "requestRooModels"
| typeof VsCodeLmModelsMessageType.requestVsCodeLmModels
| typeof VsCodeLmModelsMessageType.githubCopilotSignIn
| typeof VsCodeLmModelsMessageType.githubCopilotReconnect
| typeof VsCodeLmModelsMessageType.githubCopilotManageAccount
| "openImage"
| "saveImage"
| "openFile"
Expand Down
29 changes: 29 additions & 0 deletions src/__tests__/extension.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -477,6 +477,35 @@ describe("extension.ts", () => {
},
)

test("registers the Copilot model broadcast against every provider instance and the output channel", async () => {
vi.resetModules()
const disposable = { dispose: vi.fn() }
const registerCopilotModelBroadcast = vi.fn().mockReturnValue(disposable)
vi.doMock("../services/language-models/copilotModelBroadcast", () => ({ registerCopilotModelBroadcast }))
try {
const { ClineProvider } = await import("../core/webview/ClineProvider")
const vscode = await import("vscode")
const { activate } = await import("../extension")

await activate(mockContext)

expect(registerCopilotModelBroadcast).toHaveBeenCalledTimes(1)
const { getTargets, log } = registerCopilotModelBroadcast.mock.calls[0][0]

const instances = [{ id: "first" }, { id: "second" }]
vi.mocked(ClineProvider.getAllInstances).mockReturnValue(instances as never)
expect(getTargets()).toBe(instances)

const outputChannel = vi.mocked(vscode.window.createOutputChannel).mock.results[0].value
log("copilot broadcast message")
expect(outputChannel.appendLine).toHaveBeenCalledWith("copilot broadcast message")

expect(mockContext.subscriptions).toContain(disposable)
} finally {
vi.doUnmock("../services/language-models/copilotModelBroadcast")
}
})

test("does not call dotenv.config when optional .env does not exist", async () => {
vi.resetModules()
vi.clearAllMocks()
Expand Down
Loading
Loading