diff --git a/packages/pi-ai/src/utils/oauth/github-copilot.test.ts b/packages/pi-ai/src/utils/oauth/github-copilot.test.ts new file mode 100644 index 000000000..fabe2c09f --- /dev/null +++ b/packages/pi-ai/src/utils/oauth/github-copilot.test.ts @@ -0,0 +1,71 @@ +import assert from "node:assert/strict"; +import test from "node:test"; + +import type { Api, Model } from "../../types.js"; +import type { OAuthCredentials } from "./index.js"; +import { githubCopilotOAuthProvider } from "./github-copilot.js"; + +function makeModel(provider: string, id: string): Model { + return { + id, + name: id, + api: "openai-completions", + provider, + baseUrl: `${provider}:`, + reasoning: false, + input: ["text"], + cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, + contextWindow: 128000, + maxTokens: 16384, + }; +} + +function makeCredentials(overrides: Partial }> = {}) { + return { + type: "oauth" as const, + access: "copilot-token", + refresh: "refresh-token", + expires: Date.now() + 60_000, + ...overrides, + }; +} + +test("githubCopilotOAuthProvider.modifyModels filters unavailable copilot models (#3849)", () => { + const models = [ + makeModel("github-copilot", "gpt-5"), + makeModel("github-copilot", "claude-sonnet-4"), + makeModel("openai", "gpt-4.1"), + ]; + + assert.ok(githubCopilotOAuthProvider.modifyModels, "github copilot provider should expose modifyModels"); + const modified = githubCopilotOAuthProvider.modifyModels(models, makeCredentials({ + modelLimits: { + "gpt-5": { contextWindow: 256000, maxTokens: 32000 }, + }, + })); + + assert.deepEqual( + modified.map((model) => `${model.provider}/${model.id}`), + ["github-copilot/gpt-5", "openai/gpt-4.1"], + ); + + const copilotModel = modified.find((model) => model.provider === "github-copilot" && model.id === "gpt-5"); + assert.ok(copilotModel, "available copilot model should remain"); + assert.equal(copilotModel.contextWindow, 256000); + assert.equal(copilotModel.maxTokens, 32000); + assert.match(copilotModel.baseUrl, /githubcopilot\.com/); +}); + +test("githubCopilotOAuthProvider.modifyModels keeps all copilot models when limits are unavailable", () => { + const models = [ + makeModel("github-copilot", "gpt-5"), + makeModel("github-copilot", "claude-sonnet-4"), + ]; + + assert.ok(githubCopilotOAuthProvider.modifyModels, "github copilot provider should expose modifyModels"); + const modified = githubCopilotOAuthProvider.modifyModels(models, makeCredentials()); + + assert.equal(modified.length, 2, "lack of limits should not hide every copilot model"); + assert.ok(modified.every((model) => model.provider === "github-copilot")); + assert.ok(modified.every((model) => model.baseUrl.includes("githubcopilot.com"))); +}); diff --git a/packages/pi-ai/src/utils/oauth/github-copilot.ts b/packages/pi-ai/src/utils/oauth/github-copilot.ts index eae8e9a5f..6e01295c4 100644 --- a/packages/pi-ai/src/utils/oauth/github-copilot.ts +++ b/packages/pi-ai/src/utils/oauth/github-copilot.ts @@ -441,8 +441,11 @@ export const githubCopilotOAuthProvider: OAuthProviderInterface = { const domain = creds.enterpriseUrl ? (normalizeDomain(creds.enterpriseUrl) ?? undefined) : undefined; const baseUrl = getGitHubCopilotBaseUrl(creds.access, domain); const limits = creds.modelLimits; - return models.map((m) => { + const availableModelIds = limits ? new Set(Object.keys(limits)) : null; + const shouldFilterByAvailability = !!availableModelIds && availableModelIds.size > 0; + return models.flatMap((m) => { if (m.provider !== "github-copilot") return m; + if (shouldFilterByAvailability && !availableModelIds.has(m.id)) return []; const modelLimits = limits?.[m.id]; return { ...m,