Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ on:
push:
branches: [main]
pull_request:
branches: [main]

concurrency:
group: ci-${{ github.ref }}
Expand Down
59 changes: 59 additions & 0 deletions packages/ai/test/fixtures/model-generation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { type SpawnSyncReturns, spawnSync } from "node:child_process";
import { cpSync, mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { fileURLToPath, pathToFileURL } from "node:url";

const packageRoot = fileURLToPath(new URL("../..", import.meta.url));

export const individualModelIds = [
"deepseek-v4-flash-0731",
"deepseek-v4-pro",
"glm-5.2",
"qwen3.6-flash",
"qwen3.7-max",
"qwen3.7-plus",
"qwen3.8-max",
] as const;

export const individualModelCatalog = {
"alibaba-token-plan": {
models: Object.fromEntries(individualModelIds.map((id) => [id, { id, name: id, tool_call: true }])),
},
};

export function withModelGenerationFixture(
catalog: object,
verify: (fixture: { packageRoot: string; generate: () => SpawnSyncReturns<string> }) => void,
): void {
const fixtureRoot = mkdtempSync(join(tmpdir(), "diffex-generate-models-"));
const isolatedPackageRoot = join(fixtureRoot, "package");
const preloadPath = join(fixtureRoot, "mock-catalog.mjs");
function generate() {
return spawnSync(
process.execPath,
["--import", pathToFileURL(preloadPath).href, "scripts/generate-models.ts", "--strict"],
{ cwd: isolatedPackageRoot, encoding: "utf8", timeout: 10_000 },
);
}
try {
mkdirSync(isolatedPackageRoot);
for (const entry of ["package.json", "scripts", "src"]) {
cpSync(join(packageRoot, entry), join(isolatedPackageRoot, entry), { recursive: true });
}
writeFileSync(
preloadPath,
`const catalog = ${JSON.stringify(catalog)};\n` +
`const emptyFeeds = new Set(["https://openrouter.ai/api/v1/models", "https://ai-gateway.vercel.sh/v1/models", "https://integrate.api.nvidia.com/v1/models"]);\n` +
`globalThis.fetch = async (input) => {\n` +
` const url = String(input);\n` +
` if (url === "https://models.dev/api.json") return new Response(JSON.stringify(catalog), { status: 200 });\n` +
` if (emptyFeeds.has(url)) return new Response(JSON.stringify({ data: [] }), { status: 200 });\n` +
` throw new Error(\`Unexpected fetch: \${url}\`);\n` +
`};\n`,
);
verify({ packageRoot: isolatedPackageRoot, generate });
} finally {
rmSync(fixtureRoot, { force: true, recursive: true });
}
}
144 changes: 38 additions & 106 deletions packages/ai/test/generate-models-strict.test.ts
Original file line number Diff line number Diff line change
@@ -1,39 +1,15 @@
import { spawnSync } from "node:child_process";
import { cpSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { readFileSync } from "node:fs";
import { join } from "node:path";
import { fileURLToPath, pathToFileURL } from "node:url";
import { afterEach, describe, expect, it } from "vitest";
import { fileURLToPath } from "node:url";
import { describe, expect, it } from "vitest";
import { individualModelCatalog, individualModelIds, withModelGenerationFixture } from "./fixtures/model-generation.ts";

const packageRoot = fileURLToPath(new URL("..", import.meta.url));
const temporaryRoots: string[] = [];

afterEach(() => {
for (const root of temporaryRoots.splice(0)) rmSync(root, { force: true, recursive: true });
});

describe("strict model generation", () => {
it("preserves Kimi Coding models from the renamed regional catalog", () => {
const fixtureRoot = mkdtempSync(join(tmpdir(), "diffex-generate-kimi-"));
temporaryRoots.push(fixtureRoot);
const isolatedPackageRoot = join(fixtureRoot, "package");
mkdirSync(isolatedPackageRoot);
for (const entry of ["package.json", "scripts", "src"]) {
cpSync(join(packageRoot, entry), join(isolatedPackageRoot, entry), { recursive: true });
}
const modelIds = [
"deepseek-v4-flash-0731",
"deepseek-v4-pro",
"glm-5.2",
"qwen3.6-flash",
"qwen3.7-max",
"qwen3.7-plus",
"qwen3.8-max",
];
const catalog = {
"alibaba-token-plan": {
models: Object.fromEntries(modelIds.map((id) => [id, { id, name: id, tool_call: true }])),
},
...individualModelCatalog,
"kimi-code-plan-cn": {
models: {
"kimi-for-coding": {
Expand All @@ -46,51 +22,25 @@ describe("strict model generation", () => {
},
},
};
const preloadPath = join(fixtureRoot, "mock-catalog.mjs");
writeFileSync(
preloadPath,
`const catalog = ${JSON.stringify(catalog)};\n` +
`globalThis.fetch = async (input) => new Response(JSON.stringify(\n` +
` String(input) === "https://models.dev/api.json" ? catalog : { data: [] }\n` +
`), { status: 200 });\n`,
);
const result = spawnSync(
process.execPath,
["--import", pathToFileURL(preloadPath).href, "scripts/generate-models.ts", "--strict"],
{ cwd: isolatedPackageRoot, encoding: "utf8", timeout: 10_000 },
);
expect(result.status, `${result.stdout}\n${result.stderr}`).toBe(0);
const models = JSON.parse(readFileSync(join(isolatedPackageRoot, "src/providers/data/kimi-coding.json"), "utf8"));
expect(models["anthropic-messages"]["kimi-for-coding"]).toMatchObject({
provider: "kimi-coding",
baseUrl: "https://api.kimi.com/coding",
contextWindow: 1048576,
maxTokens: 32768,
compat: { forceAdaptiveThinking: true, allowEmptySignature: true },
withModelGenerationFixture(catalog, (fixture) => {
const result = fixture.generate();
expect(result.status, `${result.stdout}\n${result.stderr}`).toBe(0);
const models = JSON.parse(
readFileSync(join(fixture.packageRoot, "src/providers/data/kimi-coding.json"), "utf8"),
);
expect(models["anthropic-messages"]["kimi-for-coding"]).toMatchObject({
provider: "kimi-coding",
baseUrl: "https://api.kimi.com/coding",
contextWindow: 1048576,
maxTokens: 32768,
compat: { forceAdaptiveThinking: true, allowEmptySignature: true },
});
});
});

it("fails before mutating generated data when an Individual model loses tool support", () => {
const fixtureRoot = mkdtempSync(join(tmpdir(), "diffex-generate-models-"));
temporaryRoots.push(fixtureRoot);
const isolatedPackageRoot = join(fixtureRoot, "package");
mkdirSync(isolatedPackageRoot);
for (const entry of ["package.json", "scripts", "src"]) {
cpSync(join(packageRoot, entry), join(isolatedPackageRoot, entry), { recursive: true });
}
const preloadPath = join(fixtureRoot, "mock-models-dev.mjs");
const modelIds = [
"deepseek-v4-flash-0731",
"deepseek-v4-pro",
"glm-5.2",
"qwen3.6-flash",
"qwen3.7-max",
"qwen3.7-plus",
"qwen3.8-max",
"qwen3.8-max-preview",
];
const sourceModels = Object.fromEntries(
modelIds.map((id) => [
[...individualModelIds, "qwen3.8-max-preview"].map((id) => [
id,
{
id,
Expand All @@ -100,43 +50,25 @@ describe("strict model generation", () => {
]),
);
const catalog = { "alibaba-token-plan": { models: sourceModels } };
writeFileSync(
preloadPath,
`const catalog = ${JSON.stringify(catalog)};\n` +
`globalThis.fetch = async (input) => {\n` +
` if (String(input) === "https://models.dev/api.json") {\n` +
` return new Response(JSON.stringify(catalog), { status: 200 });\n` +
` }\n` +
` throw new Error(\`Unexpected fetch: \${String(input)}\`);\n` +
`};\n`,
);

const generatedPaths = [
"src/models.generated.ts",
"src/providers/qwen-token-plan-individual.models.ts",
"src/providers/data/qwen-token-plan-individual.json",
"src/providers/data/.manifest.json",
];
const sourceBefore = generatedPaths.map((path) => readFileSync(join(packageRoot, path), "utf8"));
const isolatedBefore = generatedPaths.map((path) => readFileSync(join(isolatedPackageRoot, path), "utf8"));
withModelGenerationFixture(catalog, (fixture) => {
const generatedPaths = [
"src/models.generated.ts",
"src/providers/qwen-token-plan-individual.models.ts",
"src/providers/data/qwen-token-plan-individual.json",
"src/providers/data/.manifest.json",
];
const sourceBefore = generatedPaths.map((path) => readFileSync(join(packageRoot, path), "utf8"));
const isolatedBefore = generatedPaths.map((path) => readFileSync(join(fixture.packageRoot, path), "utf8"));

const result = spawnSync(
process.execPath,
["--import", pathToFileURL(preloadPath).href, "scripts/generate-models.ts", "--strict"],
{
cwd: isolatedPackageRoot,
encoding: "utf8",
timeout: 10_000,
},
);

expect(result.status).toBe(1);
expect(`${result.stdout}\n${result.stderr}`).toContain(
"qwen-token-plan-individual model IDs do not match (missing: deepseek-v4-flash-0731)",
);
expect(generatedPaths.map((path) => readFileSync(join(isolatedPackageRoot, path), "utf8"))).toEqual(
isolatedBefore,
);
expect(generatedPaths.map((path) => readFileSync(join(packageRoot, path), "utf8"))).toEqual(sourceBefore);
const result = fixture.generate();
expect(result.status).toBe(1);
expect(`${result.stdout}\n${result.stderr}`).toContain(
"qwen-token-plan-individual model IDs do not match (missing: deepseek-v4-flash-0731)",
);
expect(generatedPaths.map((path) => readFileSync(join(fixture.packageRoot, path), "utf8"))).toEqual(
isolatedBefore,
);
expect(generatedPaths.map((path) => readFileSync(join(packageRoot, path), "utf8"))).toEqual(sourceBefore);
});
});
});
Loading
Loading