Files
oh-my-pi/packages/ai/test/github-copilot-model-limits.test.ts
T
maximhar ab37a18225 feat: add GPT-5.4 mini and nano models (#476)
* feat: add GPT-5.4 mini and nano models

* fix: address GPT-5.4 review feedback
2026-03-20 23:15:11 +01:00

176 lines
5.2 KiB
TypeScript

import { afterEach, describe, expect, it, vi } from "bun:test";
import { Effort } from "../src/model-thinking";
import { githubCopilotModelManagerOptions } from "../src/provider-models/openai-compat";
const originalFetch = global.fetch;
afterEach(() => {
global.fetch = originalFetch;
vi.restoreAllMocks();
});
function getHeaderValue(headers: unknown, key: string): string | undefined {
if (!headers) return undefined;
if (headers instanceof Headers) {
return headers.get(key) ?? undefined;
}
if (Array.isArray(headers)) {
for (const item of headers) {
if (!Array.isArray(item) || item.length < 2) continue;
const [name, value] = item;
if (typeof name === "string" && name.toLowerCase() === key.toLowerCase() && typeof value === "string") {
return value;
}
}
return undefined;
}
if (typeof headers === "object") {
for (const [name, value] of Object.entries(headers as Record<string, unknown>)) {
if (name.toLowerCase() === key.toLowerCase() && typeof value === "string") {
return value;
}
}
}
return undefined;
}
async function discoverCopilotModels(
payload: unknown,
apiKey = "copilot-test-key",
expectedBaseUrl = "https://api.individual.githubcopilot.com",
) {
const fetchMock = vi.fn(async (input: string | URL, init?: RequestInit) => {
const url = typeof input === "string" ? input : input.toString();
expect(url).toBe(`${expectedBaseUrl}/models`);
expect(init?.method).toBe("GET");
expect(getHeaderValue(init?.headers, "Authorization")).toBe(`Bearer ${apiKey}`);
return new Response(JSON.stringify(payload), {
status: 200,
headers: { "Content-Type": "application/json" },
});
});
global.fetch = fetchMock as unknown as typeof fetch;
const options = githubCopilotModelManagerOptions({ apiKey });
expect(options.fetchDynamicModels).toBeDefined();
const models = await options.fetchDynamicModels?.();
expect(models).not.toBeNull();
return { models: models ?? [], fetchMock };
}
describe("github copilot model limits mapping", () => {
it("uses proxy endpoint from token for discovery base URL", async () => {
const enterpriseToken = "tid=2;proxy-ep=proxy.enterprise.githubcopilot.com;exp=9999999999";
const { fetchMock } = await discoverCopilotModels(
{ data: [] },
enterpriseToken,
"https://api.enterprise.githubcopilot.com",
);
expect(fetchMock).toHaveBeenCalledTimes(1);
});
it("uses capabilities.limits max_prompt_tokens as context window when context_length is absent", async () => {
const { models, fetchMock } = await discoverCopilotModels({
data: [
{
id: "gemini-2.5-pro",
name: "Gemini 2.5 Pro",
capabilities: {
limits: {
max_context_window_tokens: 1_048_576,
max_prompt_tokens: 128_000,
max_output_tokens: 64_000,
},
},
},
],
});
const model = models.find(candidate => candidate.id === "gemini-2.5-pro");
expect(model).toBeDefined();
expect(model?.contextWindow).toBe(128_000);
expect(model?.maxTokens).toBe(64_000);
expect(fetchMock).toHaveBeenCalledTimes(1);
});
it("prefers explicit context_length/max_completion_tokens when present", async () => {
const { models } = await discoverCopilotModels({
data: [
{
id: "gpt-5.2-codex",
name: "GPT-5.2 Codex",
context_length: 250_000,
max_completion_tokens: 120_000,
capabilities: {
limits: {
max_context_window_tokens: 400_000,
max_prompt_tokens: 272_000,
max_output_tokens: 128_000,
},
},
},
],
});
const model = models.find(candidate => candidate.id === "gpt-5.2-codex");
expect(model).toBeDefined();
expect(model?.api).toBe("openai-responses");
expect(model?.contextWindow).toBe(250_000);
expect(model?.maxTokens).toBe(120_000);
});
it("falls back to max_non_streaming_output_tokens when max_output_tokens is absent", async () => {
const { models } = await discoverCopilotModels({
data: [
{
id: "claude-opus-4.6",
name: "Claude Opus 4.6",
capabilities: {
limits: {
max_context_window_tokens: 200_000,
max_prompt_tokens: 128_000,
max_non_streaming_output_tokens: 16_000,
},
},
},
],
});
const model = models.find(candidate => candidate.id === "claude-opus-4.6");
expect(model).toBeDefined();
expect(model?.contextWindow).toBe(128_000);
expect(model?.maxTokens).toBe(16_000);
});
it("inherits bundled GPT-5.4 mini reasoning metadata during discovery", async () => {
const { models } = await discoverCopilotModels({
data: [
{
id: "gpt-5.4-mini",
name: "GPT-5.4 mini",
context_length: 400_000,
max_completion_tokens: 128_000,
capabilities: {
limits: {
max_context_window_tokens: 400_000,
max_prompt_tokens: 272_000,
max_output_tokens: 128_000,
},
},
},
],
});
const model = models.find(candidate => candidate.id === "gpt-5.4-mini");
expect(model).toBeDefined();
expect(model?.api).toBe("openai-responses");
expect(model?.reasoning).toBe(true);
expect(model?.contextWindow).toBe(400_000);
expect(model?.maxTokens).toBe(128_000);
expect(model?.premiumMultiplier).toBe(0.33);
expect(model?.thinking).toEqual({
mode: "effort",
minLevel: Effort.Low,
maxLevel: Effort.XHigh,
});
});
});