fix(ai): omit service tier for GitHub Copilot
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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> = [];
|
||||
|
||||
Reference in New Issue
Block a user