fix(providers): required copilot vision confirmation

Keep non-personal Copilot endpoints text-only when discovery omits supports.vision, while preserving explicit vision support.

Fixes #4779
This commit is contained in:
roboomp
2026-07-10 12:08:43 +00:00
parent b0f22caf83
commit 4bae9a42ab
2 changed files with 45 additions and 6 deletions
@@ -16,7 +16,12 @@ import { getBundledModels } from "../models";
import type { Api, FetchImpl, Model, ModelSpec, OpenAICompat, Provider, ThinkingConfig } from "../types";
import { discoveryFetch, isAnthropicOAuthToken, isRecord, toBoolean, toNumber, toPositiveNumber } from "../utils";
import { coreWeaveProjectHeaders } from "../wire/coreweave";
import { COPILOT_API_HEADERS, getGitHubCopilotBaseUrl, parseGitHubCopilotApiKey } from "../wire/github-copilot";
import {
COPILOT_API_HEADERS,
getGitHubCopilotBaseUrl,
isPersonalGitHubCopilotBaseUrl,
parseGitHubCopilotApiKey,
} from "../wire/github-copilot";
import { createBundledReferenceMap, createReferenceResolver, toModelSpec } from "./bundled-references";
const MODELS_DEV_URL = "https://models.dev/api.json";
@@ -3692,11 +3697,11 @@ export function githubCopilotModelManagerOptions(config?: GithubCopilotModelMana
const api = inferCopilotApi(defaults.id);
const supportsVision = extractCopilotSupportsVision(entry);
const input: ModelSpec<Api>["input"] =
supportsVision === undefined
? (reference?.input ?? defaults.input)
: supportsVision
? ["text", "image"]
: ["text"];
supportsVision === true
? ["text", "image"]
: supportsVision === false || !isPersonalGitHubCopilotBaseUrl(baseUrl)
? ["text"]
: (reference?.input ?? defaults.input);
// With COPILOT_API_HEADERS the served window is the long-context
// ceiling; the default tier ends at token_prices.default.context_max
// prompt tokens. Cap the base entry to the default tier — the long
@@ -605,6 +605,40 @@ describe("github copilot vision endpoint policy", () => {
}
});
it("maps omitted upstream vision to text-only on non-personal Copilot endpoints", async () => {
for (const endpoint of [
{
apiKey: businessApiKey,
baseUrl: "https://api.business.githubcopilot.com",
token: "ghu_business_token",
},
{
apiKey: enterpriseApiKey,
baseUrl: "https://copilot-api.ghe.example.com",
token: "ghu_enterprise_token",
},
]) {
const { models } = await discoverCopilotModels(
{
data: [
tieredCopilotEntry({
id: "claude-sonnet-4.6",
name: "Claude Sonnet 4.6",
window: 200_000,
maxOutput: 32_000,
}),
],
},
endpoint.apiKey,
endpoint.baseUrl,
endpoint.token,
);
const model = models.find(candidate => candidate.id === "claude-sonnet-4.6");
expect(model?.baseUrl).toBe(endpoint.baseUrl);
expect(model?.input).toEqual(["text"]);
}
});
it("keeps vision on the canonical personal Copilot endpoint", async () => {
const { models } = await discoverCopilotModels({
data: [