fix(ai): omit service tier for GitHub Copilot

This commit is contained in:
Audrey Tang
2026-07-11 14:37:37 +08:00
parent c893e7ab73
commit 36be7e698d
3 changed files with 43 additions and 4 deletions
+4
View File
@@ -2,6 +2,10 @@
## [Unreleased]
### Fixed
- Fixed GitHub Copilot OpenAI-compatible requests being rejected when the session's native OpenAI service tier was set to `priority`.
## [16.4.3] - 2026-07-11
### Fixed
+9 -4
View File
@@ -141,13 +141,17 @@ function isOpenAIServiceTierApi(api: Api | undefined): boolean {
return api === "openai-completions" || api === "openai-responses" || api === "openai-codex-responses";
}
function hasDedicatedServiceTierControl(provider: Provider | undefined): boolean {
return provider === "fireworks";
function excludesInferredOpenAIServiceTier(provider: Provider | undefined): boolean {
// Fireworks has its own priority-only control. GitHub Copilot proxies OpenAI
// models but rejects OpenAI's `service_tier` request field.
return provider === "fireworks" || provider === "github-copilot";
}
function isOpenAIServiceTierModel(model: ServiceTierModel): boolean {
return (
!hasDedicatedServiceTierControl(model.provider) && isOpenAIServiceTierApi(model.api) && isOpenAIModelId(model.id)
!excludesInferredOpenAIServiceTier(model.provider) &&
isOpenAIServiceTierApi(model.api) &&
isOpenAIModelId(model.id)
);
}
@@ -159,7 +163,8 @@ function isOpenAIServiceTierModel(model: ServiceTierModel): boolean {
* `openai/`); Claude on Bedrock/Vertex (api `anthropic-messages`) is the
* anthropic family even though its provider is `amazon-bedrock`/`google-vertex`.
* Custom OpenAI-compatible relays that serve OpenAI model ids are OpenAI family
* too unless that provider owns a separate tier control such as Fireworks.
* too unless the provider owns a separate tier control (Fireworks) or rejects
* OpenAI's service-tier field (GitHub Copilot).
*/
export function serviceTierFamily(model: ServiceTierModel): ServiceTierFamily | undefined {
const provider = model.provider;
@@ -30,6 +30,13 @@ function getRequestHeader(
return new Headers(init?.headers).get(headerName);
}
async function getRequestBody(input: string | URL | Request, init?: RequestInit): Promise<Record<string, unknown>> {
if (input instanceof Request) {
return (await input.clone().json()) as Record<string, unknown>;
}
return JSON.parse(String(init?.body)) as Record<string, unknown>;
}
function createUnauthorizedResponse(): Response {
return new Response(JSON.stringify({ error: { message: "Unauthorized" } }), {
status: 401,
@@ -79,6 +86,29 @@ describe("GitHub Copilot OpenAI transport base URL", () => {
expect(requestedUrls[0]).toBe("https://api.githubcopilot.com/responses");
});
it("omits OpenAI priority service tier while native OpenAI keeps it", async () => {
const requestedBodies: Record<string, unknown>[] = [];
const fetchMock = vi.fn(async (input: string | URL | Request, init?: RequestInit) => {
requestedBodies.push(await getRequestBody(input, init));
return createUnauthorizedResponse();
});
const requestOptions = {
apiKey: testToken,
fetch: fetchMock as unknown as typeof fetch,
serviceTier: "priority" as const,
};
const copilotModel = getBundledModel("github-copilot", "gpt-5.4") as Model<"openai-responses">;
await streamOpenAIResponses(copilotModel, testContext, requestOptions).result();
const openAIModel = getBundledModel("openai", "gpt-5-mini") as Model<"openai-responses">;
await streamOpenAIResponses(openAIModel, testContext, requestOptions).result();
expect(requestedBodies).toHaveLength(2);
expect(requestedBodies[0]?.service_tier).toBeUndefined();
expect(requestedBodies[1]?.service_tier).toBe("priority");
});
it("routes structured enterprise credentials to the enterprise chat completions host", async () => {
const requestedUrls: string[] = [];
const requestedAuthHeaders: Array<string | null> = [];