mirror of
https://github.com/farion1231/cc-switch.git
synced 2026-10-26 01:34:51 +08:00
A provider's /models endpoint only returns model IDs, so a model picked from the fetched list left its context window, output limit, reasoning levels, and modalities empty, while the same model in a preset had them. When a model is picked in the Codex, OpenClaw, Hermes, OpenCode, or MiniMax Code form, look up its metadata field by field: 1. the preset model with the same endpoint (hand-verified values); 2. the models.dev provider whose API endpoint matches; 3. the vendor's own models.dev entry, without its price. Endpoints match only when host and path are equal after dropping a trailing version segment or request path, so plans that share a host (z.ai standard vs Coding Plan, OpenCode Zen vs Go) never borrow each other's values. Models are never matched by name across providers: the same model ID often has very different context windows per deployment. Only empty fields are filled, and reasoning or image input is only ever turned on. models.dev is prefetched once the model list is fetched and shared with the usage pricing import; when it arrives late the row is filled again, located by its stable row key. A single toast asks the user to review what was filled.
273 lines
7.2 KiB
TypeScript
273 lines
7.2 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
|
|
import {
|
|
resolveModelMetadata,
|
|
type KnownModelMetadata,
|
|
type PresetModelSource,
|
|
} from "@/lib/modelMetadata";
|
|
import type { ModelsDevResponse } from "@/lib/modelsDev";
|
|
|
|
const modelsDev: ModelsDevResponse = {
|
|
zhipuai: {
|
|
id: "zhipuai",
|
|
api: "https://open.bigmodel.cn/api/paas/v4",
|
|
models: {
|
|
"glm-5": {
|
|
id: "glm-5",
|
|
limit: { context: 204800, output: 131072 },
|
|
reasoning: true,
|
|
reasoning_options: [{ type: "toggle" }],
|
|
modalities: { input: ["text"], output: ["text"] },
|
|
cost: { input: 1, output: 3.2 },
|
|
},
|
|
},
|
|
},
|
|
zai: {
|
|
id: "zai",
|
|
api: "https://api.z.ai/api/paas/v4",
|
|
models: {
|
|
"glm-5": { id: "glm-5", limit: { context: 200000, output: 128000 } },
|
|
},
|
|
},
|
|
"zai-coding-plan": {
|
|
id: "zai-coding-plan",
|
|
api: "https://api.z.ai/api/coding/paas/v4",
|
|
models: {
|
|
"glm-5": {
|
|
id: "glm-5",
|
|
limit: { context: 202752, output: 98304 },
|
|
cost: { input: 0, output: 0 },
|
|
},
|
|
},
|
|
},
|
|
"opencode-go": {
|
|
id: "opencode-go",
|
|
api: "https://opencode.ai/zen/go/v1",
|
|
models: {
|
|
"glm-5.3": {
|
|
id: "glm-5.3",
|
|
limit: { context: 1000000, output: 131072 },
|
|
reasoning_options: [{ type: "effort", values: ["low", "high", "max"] }],
|
|
},
|
|
},
|
|
},
|
|
openrouter: {
|
|
id: "openrouter",
|
|
api: "https://openrouter.ai/api/v1",
|
|
models: {
|
|
"z-ai/glm-5": {
|
|
id: "z-ai/glm-5",
|
|
limit: { context: 202800 },
|
|
canonical_model_id: "zhipuai/glm-5",
|
|
},
|
|
"someone/kimi-k9": {
|
|
id: "someone/kimi-k9",
|
|
limit: { context: 65536 },
|
|
canonical_model_id: "moonshotai/kimi-k9",
|
|
},
|
|
},
|
|
},
|
|
moonshotai: {
|
|
id: "moonshotai",
|
|
api: "https://api.moonshot.ai/v1",
|
|
models: {
|
|
"kimi-k9": { id: "kimi-k9", limit: { context: 262144 } },
|
|
},
|
|
},
|
|
other: {
|
|
id: "other",
|
|
models: {
|
|
"kimi-k9": {
|
|
id: "kimi-k9",
|
|
limit: { context: 100000 },
|
|
canonical_model_id: "elsewhere/kimi-k9",
|
|
},
|
|
},
|
|
},
|
|
elsewhere: {
|
|
id: "elsewhere",
|
|
models: { "kimi-k9": { id: "kimi-k9", limit: { context: 50000 } } },
|
|
},
|
|
};
|
|
|
|
const preset = (
|
|
baseUrl: string,
|
|
models: Record<string, KnownModelMetadata>,
|
|
): PresetModelSource => ({ baseUrl, models: new Map(Object.entries(models)) });
|
|
|
|
describe("resolveModelMetadata", () => {
|
|
it("uses the provider recognized by its API address", () => {
|
|
expect(
|
|
resolveModelMetadata("glm-5.3", {
|
|
baseUrl: "https://opencode.ai/zen/go/v1/",
|
|
modelsDev,
|
|
}),
|
|
).toMatchObject({
|
|
contextWindow: 1000000,
|
|
maxOutputTokens: 131072,
|
|
reasoningEfforts: ["low", "high", "max"],
|
|
});
|
|
});
|
|
|
|
it("prefers the provider whose path matches the address best", () => {
|
|
expect(
|
|
resolveModelMetadata("glm-5", {
|
|
baseUrl: "https://api.z.ai/api/coding/paas/v4",
|
|
modelsDev,
|
|
})?.contextWindow,
|
|
).toBe(202752);
|
|
expect(
|
|
resolveModelMetadata("glm-5", {
|
|
baseUrl: "https://api.z.ai/api/paas/v4",
|
|
modelsDev,
|
|
})?.contextWindow,
|
|
).toBe(200000);
|
|
});
|
|
|
|
it("matches ids case-insensitively and across namespaces", () => {
|
|
expect(
|
|
resolveModelMetadata("Z-AI/GLM-5", {
|
|
baseUrl: "https://openrouter.ai/api/v1",
|
|
modelsDev,
|
|
})?.contextWindow,
|
|
).toBe(202800);
|
|
expect(
|
|
resolveModelMetadata("zai-org/glm-5", {
|
|
baseUrl: "https://open.bigmodel.cn/api/paas/v4",
|
|
modelsDev,
|
|
})?.contextWindow,
|
|
).toBe(204800);
|
|
});
|
|
|
|
it("falls back to the vendor entry without carrying its price", () => {
|
|
const metadata = resolveModelMetadata("glm-5", {
|
|
baseUrl: "https://relay.example.com/v1",
|
|
modelsDev,
|
|
});
|
|
expect(metadata).toMatchObject({
|
|
contextWindow: 204800,
|
|
reasoning: true,
|
|
inputModalities: ["text"],
|
|
sources: ["models-dev"],
|
|
});
|
|
expect(metadata?.cost).toBeUndefined();
|
|
expect(metadata?.reasoningEfforts).toBeUndefined();
|
|
});
|
|
|
|
it("keeps the price when the provider itself is recognized", () => {
|
|
expect(
|
|
resolveModelMetadata("glm-5", {
|
|
baseUrl: "https://open.bigmodel.cn/api/paas/v4",
|
|
modelsDev,
|
|
})?.cost,
|
|
).toEqual({ input: 1, output: 3.2 });
|
|
});
|
|
|
|
it("gives up when the same name points at different vendors", () => {
|
|
expect(
|
|
resolveModelMetadata("kimi-k9", {
|
|
baseUrl: "https://relay.example.com/v1",
|
|
modelsDev,
|
|
}),
|
|
).toBeNull();
|
|
});
|
|
|
|
it("lets the matching preset row win field by field", () => {
|
|
const presets = [
|
|
preset("https://api.z.ai/api/coding/paas/v4", {
|
|
"glm-5": { contextWindow: 500000 },
|
|
}),
|
|
preset("https://relay.example.com/v1", {
|
|
"glm-5": { contextWindow: 1 },
|
|
}),
|
|
];
|
|
expect(
|
|
resolveModelMetadata("glm-5", {
|
|
baseUrl: "https://api.z.ai/api/coding/paas/v4",
|
|
presets,
|
|
modelsDev,
|
|
}),
|
|
).toMatchObject({
|
|
contextWindow: 500000,
|
|
maxOutputTokens: 98304,
|
|
sources: ["preset", "models-dev"],
|
|
});
|
|
});
|
|
|
|
it("does not take another plan's preset on the same host", () => {
|
|
expect(
|
|
resolveModelMetadata("glm-5", {
|
|
baseUrl: "https://api.z.ai/api/paas/v4",
|
|
presets: [
|
|
preset("https://api.z.ai/api/coding/paas/v4", {
|
|
"glm-5": { contextWindow: 500000 },
|
|
}),
|
|
],
|
|
modelsDev,
|
|
}),
|
|
).toMatchObject({ contextWindow: 200000, sources: ["models-dev"] });
|
|
});
|
|
|
|
it("accepts the same endpoint written with or without a version suffix", () => {
|
|
expect(
|
|
resolveModelMetadata("glm-5.3", {
|
|
baseUrl: "https://opencode.ai/zen/go",
|
|
modelsDev,
|
|
})?.contextWindow,
|
|
).toBe(1000000);
|
|
});
|
|
|
|
it("does not treat a parent path as the plans nested under it", () => {
|
|
expect(
|
|
resolveModelMetadata("glm-5.3", {
|
|
baseUrl: "https://opencode.ai/zen",
|
|
presets: [
|
|
preset("https://opencode.ai/zen/go/v1", {
|
|
"glm-5.3": { contextWindow: 1 },
|
|
}),
|
|
],
|
|
modelsDev,
|
|
}),
|
|
).toBeNull();
|
|
});
|
|
|
|
it("ignores a pasted request path after the base address", () => {
|
|
expect(
|
|
resolveModelMetadata("glm-5", {
|
|
baseUrl: "https://api.z.ai/api/coding/paas/v4/chat/completions",
|
|
modelsDev,
|
|
})?.contextWindow,
|
|
).toBe(202752);
|
|
});
|
|
|
|
it("fills fields the provider entry leaves out from the vendor entry", () => {
|
|
const metadata = resolveModelMetadata("glm-5", {
|
|
baseUrl: "https://api.z.ai/api/paas/v4",
|
|
modelsDev,
|
|
});
|
|
expect(metadata).toMatchObject({
|
|
contextWindow: 200000,
|
|
maxOutputTokens: 128000,
|
|
reasoning: true,
|
|
inputModalities: ["text"],
|
|
});
|
|
expect(metadata?.cost).toBeUndefined();
|
|
});
|
|
|
|
it("uses presets alone when models.dev is unavailable", () => {
|
|
expect(
|
|
resolveModelMetadata("glm-5", {
|
|
baseUrl: "https://api.z.ai/api/coding/paas/v4",
|
|
presets: [
|
|
preset("https://api.z.ai/api/coding/paas/v4", {
|
|
"glm-5": { contextWindow: 500000 },
|
|
}),
|
|
],
|
|
}),
|
|
).toEqual({ contextWindow: 500000, sources: ["preset"] });
|
|
expect(
|
|
resolveModelMetadata("glm-5", { baseUrl: "https://api.z.ai/v1" }),
|
|
).toBeNull();
|
|
});
|
|
});
|