源码
packages/ai/test/model-discovery.test.ts
import { expect, it } from "vitest";
import {
defaultThinkingLevel,
fetchProviderModels,
presetModels,
reasoningParameters,
thinkingChoices,
} from "../src/index.ts";
it.each([
{ api: "anthropic-messages" as const, key: "fake-api-key", path: "/v1/models", header: "x-api-key" },
{ api: "anthropic-messages" as const, key: "sk-ant-oat-fake", path: "/v1/models", header: "authorization" },
{ api: "openai-completions" as const, key: "fake-api-key", path: "/models", header: "authorization" },
])(
"discovers custom $api models using their catalog protocol and $header",
async ({ api, key, path, header }) => {
const result = await fetchProviderModels({ api, baseUrl: "https://custom.example", apiKey: key }, (async (
url,
options,
) => {
expect(String(url)).toBe(`https://custom.example${path}`);
const headers = new Headers(options?.headers);
expect(headers.get(header)).toBe(header === "authorization" ? `Bearer ${key}` : key);
expect(headers.get("anthropic-version")).toBe(api === "anthropic-messages" ? "2023-06-01" : null);
expect(headers.get(header === "authorization" ? "x-api-key" : "authorization")).toBeNull();
return Response.json({ data: [{ id: "custom-model" }] });
}) as typeof fetch);
expect(result).toMatchObject({ source: "remote", models: [{ id: "custom-model" }] });
},
);
it("keeps preset discovery on its OpenAI catalog when inference uses Anthropic", async () => {
await fetchProviderModels({ preset: "deepseek", api: "anthropic-messages", apiKey: "fake" }, (async (
url,
options,
) => {
expect(String(url)).toBe("https://api.deepseek.com/models");
expect(new Headers(options?.headers).get("authorization")).toBe("Bearer fake");
return Response.json({ data: [{ id: "deepseek-flash" }] });
}) as typeof fetch);
});
it("rejects failed custom Anthropic discovery without exposing credentials", async () => {
await expect(
fetchProviderModels(
{ api: "anthropic-messages", baseUrl: "https://custom.example", apiKey: "fake-secret" },
(async () => new Response("fake-secret", { status: 401 })) as typeof fetch,
),
).rejects.toThrow("无法获取模型列表");
});
it("reads the account catalog without cache, uses slugs, and keeps new models distinct from unverified presets", async () => {
const result = await fetchProviderModels({ preset: "openai-chatgpt", apiKey: "fake-token" }, (async (
url,
options,
) => {
expect(String(url)).toBe("https://api.openai.com/v1/models");
expect(options?.cache).toBe("no-store");
return Response.json({
data: [{ id: "stale-api-catalog" }],
models: [
{ id: "internal-id", slug: "gpt-6-astra", visibility: "list", context_window: 300000 },
{ slug: "gpt-6-luna", visibility: "hide" },
{
slug: "new-account-model",
visibility: "list",
supported_reasoning_levels: ["none", "medium", "xhigh"],
},
],
});
}) as typeof fetch);
expect(result.models.map((model) => model.id)).toEqual([
"gpt-6.1-terra",
"gpt-6.1-sol",
"gpt-6-astra",
"new-account-model",
]);
expect(result.models[2]).toMatchObject({ availability: "listed", contextWindow: 500000 });
expect(result.models[1]).toMatchObject({ availability: "unverified", defaultThinkingLevel: "medium" });
expect(result.warning).toContain("账号可用性待验证");
expect(thinkingChoices(result.models[3] as Parameters<typeof thinkingChoices>[0])).toEqual([
"off",
"medium",
"xhigh",
]);
expect(result.models[3].thinkingLevelMap?.max).toBeNull();
});
it("orders GPT presets Terra, Sol, Luna, Astra and provides Terra's default capabilities", () => {
const models = presetModels("openai-chatgpt");
expect(models.map((model) => model.id)).toEqual([
"gpt-6.1-terra",
"gpt-6.1-sol",
"gpt-6-luna",
"gpt-6-astra",
]);
expect(models[0]).toMatchObject({
name: "GPT-6.1 Terra",
input: ["text", "image"],
contextWindow: 500_000,
maxTokens: 128_000,
defaultThinkingLevel: "medium",
});
expect(thinkingChoices({ ...models[0], reasoning: true })).toEqual([
"off",
"low",
"medium",
"high",
"xhigh",
"max",
]);
});
it.each(["openai-chatgpt", "openai-api"] as const)(
"keeps canonical names and 500K defaults for %s while excluding superseded models",
async (preset) => {
const models = presetModels(preset);
expect(models.map((model) => model.name)).toEqual([
"GPT-6.1 Terra",
"GPT-6.1 Sol",
"GPT-6 Luna",
"GPT-6 Astra",
]);
const entries = [
...models.map((model, index) => ({
id: model.id,
slug: model.id,
visibility: "list",
display_name: model.id.toUpperCase(),
context_window: index % 2 ? 272000 : 1050000,
max_output_tokens: 128000,
input_modalities: ["text", "image"],
})),
...["gpt-6-sol", "gpt-5.6-sol", "gpt-5.6-terra", "gpt-5.6-luna"].map((id) => ({
id,
slug: id,
visibility: "list",
})),
];
const result = await fetchProviderModels({ preset, apiKey: "fake" }, async () =>
Response.json({ models: entries, data: entries }),
);
expect(result.models.map((model) => model.id)).toEqual(models.map((model) => model.id));
expect(result.models.map((model) => model.name)).toEqual(models.map((model) => model.name));
expect(result.models.map((model) => model.contextWindow)).toEqual([500000, 500000, 500000, 500000]);
expect(result.models.every((model) => model.metadataSource === "catalog")).toBe(true);
expect(models.every((model) => model.contextWindow === 500000)).toBe(true);
},
);
it("discovers newly returned GPT models with their complete efforts and account defaults", async () => {
const result = await fetchProviderModels({ preset: "openai-chatgpt", apiKey: "fake" }, (async () =>
Response.json({
models: presetModels("openai-chatgpt")
.toReversed()
.map((model) => ({
slug: model.id,
visibility: "list",
supported_reasoning_levels: (model.id === "gpt-6-luna" || model.id === "gpt-6.1-terra"
? ["none", "low", "medium", "high", "xhigh", "max"]
: ["low", "medium", "high", "xhigh", "max"]
).map((effort) => ({ effort })),
default_reasoning_level: "medium",
})),
})) as typeof fetch);
expect(result.warning).toBeUndefined();
expect(result.models.map((model) => model.id)).toEqual(
presetModels("openai-chatgpt").map((model) => model.id),
);
for (const model of result.models) {
const reasoningModel = { ...model, reasoning: true };
expect(thinkingChoices(reasoningModel)).toEqual(
model.id === "gpt-6-luna" || model.id === "gpt-6.1-terra"
? ["off", "low", "medium", "high", "xhigh", "max"]
: ["low", "medium", "high", "xhigh", "max"],
);
expect(defaultThinkingLevel(reasoningModel)).toBe("medium");
expect(reasoningParameters(reasoningModel)).toEqual({ reasoning_effort: "medium" });
expect(reasoningParameters(reasoningModel, "xhigh")).toEqual({ reasoning_effort: "xhigh" });
expect(reasoningParameters(reasoningModel, "max")).toEqual({ reasoning_effort: "max" });
}
});
it("never substitutes presets for failed or empty ChatGPT account discovery", async () => {
for (const response of [new Response(null, { status: 401 }), Response.json({ models: [] })])
await expect(
fetchProviderModels(
{ preset: "openai-chatgpt", apiKey: "fake" },
(async () => response) as typeof fetch,
),
).rejects.toThrow("无法获取模型");
});
it("discovers official API models with a bearer key, filters non-agent models and supplements missing presets", async () => {
const result = await fetchProviderModels(
{ preset: "openai-api", api: "openai-responses", apiKey: "fake-api-key" },
async (url, options) => {
expect(String(url)).toBe("https://api.openai.com/v1/models");
expect(new Headers(options?.headers).get("authorization")).toBe("Bearer fake-api-key");
expect(options?.cache).toBe("no-store");
return Response.json({
data: [
{ id: "gpt-6-astra" },
{ id: "text-embedding-3-small" },
{ id: "gpt-6.1-sol" },
{ id: "gpt-image-1" },
],
});
},
);
expect(result.source).toBe("remote");
expect(result.models.map((model) => model.id)).toEqual(
presetModels("openai-chatgpt").map((model) => model.id),
);
expect(result.models.map((model) => model.availability)).toEqual([
"unverified",
"listed",
"unverified",
"listed",
]);
expect(result.models[1]).toMatchObject({ reasoning: true, input: ["text", "image"], maxTokens: 128000 });
expect(result.warning).toContain("账号可用性待验证");
});
it("uses all four API presets when discovery fails, is empty or lists no matching models", async () => {
for (const response of [
new Response("fake-secret", { status: 401 }),
Response.json({ data: [] }),
Response.json({ data: [{ id: "text-embedding-3-small" }] }),
Response.json({
data: [
...Array.from({ length: 500 }, (_, index) => ({ id: `embedding-${index}` })),
{ id: "gpt-6.1-sol" },
],
}),
]) {
const result = await fetchProviderModels(
{ preset: "openai-api", apiKey: "fake-secret" },
async () => response,
);
expect(result.models).toEqual(presetModels("openai-chatgpt"));
expect(result.warning).toBeDefined();
expect(result.warning).not.toContain("fake-secret");
}
});
it("keeps all four API models listed without warnings when discovery returns the complete catalog", async () => {
const result = await fetchProviderModels({ preset: "openai-api", apiKey: "fake" }, async () =>
Response.json({
data: presetModels("openai-api")
.toReversed()
.map(({ id }) => ({ id })),
}),
);
expect(result.warning).toBeUndefined();
expect(result.models).toEqual(
presetModels("openai-chatgpt").map((model) => ({ ...model, availability: "listed" })),
);
});
it("Chinese catalogs offer only actual controls while keeping mapped levels usable", () => {
const choices = (preset: string, id: string) => {
const model = presetModels(preset).find((model) => model.id === id);
if (!model) throw new Error("Missing preset model");
return thinkingChoices({ ...model, reasoning: model.reasoning ?? false });
};
expect(choices("deepseek", "deepseek-flash")).toEqual(["off", "low", "high", "max"]);
expect(choices("deepseek", "deepseek-v4-pro")).toEqual(["off", "high", "max"]);
expect(choices("zhipu-api", "glm-5.3")).toEqual(["low", "high", "max"]);
expect(choices("zhipu-api", "glm-4.7")).toEqual(["off", "high"]);
expect(choices("minimax-api", "MiniMax-M2.7")).toEqual(["high"]);
expect(choices("mimo-api", "mimo-v2.6-pro")).toEqual(["off", "high"]);
const booleanModel = {
reasoning: true,
compat: { thinkingFormat: "zai" as const, supportsReasoningEffort: false },
};
expect(thinkingChoices(booleanModel)).toEqual(["off", "high"]);
expect(reasoningParameters(booleanModel, "medium")).toEqual(reasoningParameters(booleanModel, "max"));
const mapped = {
reasoning: true,
compat: { supportsReasoningEffort: true },
thinkingLevelMap: { medium: "high", xhigh: "high", max: "high" },
};
expect(thinkingChoices(mapped)).toEqual(["off", "minimal", "low", "high"]);
expect(reasoningParameters(mapped, "xhigh")).toEqual({ reasoning_effort: "high" });
});