mirror of
https://github.com/badlogic/pi-mono.git
synced 2026-11-05 23:37:25 +00:00
fix: preserve legacy model catalog route on publish
Mixed-API catalogs require per-model OpenRouter dispatch from Pi 0.85.0. Publish them under the 0.85.0 index entry so older clients keep their compatible route (#9099).
This commit is contained in:
parent
441f5767c4
commit
73b3905f4f
2 changed files with 128 additions and 37 deletions
|
|
@ -25,7 +25,8 @@ import {
|
|||
// The storage layout, index format, and version ordering are defined in
|
||||
// model-catalog-protocol.ts, which pi.dev shares to serve these artifacts.
|
||||
// Bump this only when generated model metadata requires behavior unavailable in older pi clients.
|
||||
const MINIMUM_PI_VERSION = "0.80.7";
|
||||
// Pi 0.85.0 added per-model OpenRouter API dispatch. Older clients keep their existing index entry (#9099).
|
||||
const MINIMUM_PI_VERSION = "0.85.0";
|
||||
const JSON_CONTENT_TYPE = "application/json; charset=utf-8";
|
||||
const IMMUTABLE_CACHE_CONTROL = "public, max-age=31536000, immutable";
|
||||
const INDEX_CACHE_CONTROL = "no-store";
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
import assert from "node:assert/strict";
|
||||
import { spawnSync } from "node:child_process";
|
||||
import { mkdtempSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
||||
import { chmodSync, mkdtempSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { delimiter, join } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import test from "node:test";
|
||||
|
||||
|
|
@ -12,30 +12,15 @@ function writeJson(path, value) {
|
|||
writeFileSync(path, `${JSON.stringify(value)}\n`);
|
||||
}
|
||||
|
||||
test("publishes explicit per-type and total model counts", (t) => {
|
||||
const input = mkdtempSync(join(tmpdir(), "pi-model-catalog-counts-"));
|
||||
t.after(() => rmSync(input, { recursive: true, force: true }));
|
||||
function writeCatalog(input, allModels) {
|
||||
const providersDir = join(input, "providers");
|
||||
mkdirSync(providersDir);
|
||||
|
||||
const chatModels = Array.from({ length: 500 }, (_, index) => ({
|
||||
type: "chat",
|
||||
id: `chat-${index}`,
|
||||
provider: "anthropic",
|
||||
}));
|
||||
const allModels = {
|
||||
anthropic: chatModels,
|
||||
openai: [],
|
||||
openrouter: [
|
||||
{ type: "image", id: "shared", provider: "openrouter" },
|
||||
{ type: "classifier", id: "shared", provider: "openrouter" },
|
||||
],
|
||||
};
|
||||
const models = {
|
||||
anthropic: Object.fromEntries(chatModels.map((model) => [model.id, model])),
|
||||
openai: {},
|
||||
openrouter: {},
|
||||
};
|
||||
const models = Object.fromEntries(
|
||||
Object.entries(allModels).map(([providerId, providerModels]) => [
|
||||
providerId,
|
||||
Object.fromEntries(providerModels.filter((model) => model.type === "chat").map((model) => [model.id, model])),
|
||||
]),
|
||||
);
|
||||
const providerIds = Object.keys(allModels).sort();
|
||||
writeJson(join(input, "models.json"), models);
|
||||
writeJson(join(input, "models.all.json"), allModels);
|
||||
|
|
@ -44,19 +29,39 @@ test("publishes explicit per-type and total model counts", (t) => {
|
|||
writeJson(join(providersDir, `${providerId}.json`), models[providerId]);
|
||||
writeJson(join(providersDir, `${providerId}.all.json`), allModels[providerId]);
|
||||
}
|
||||
}
|
||||
|
||||
const result = spawnSync(
|
||||
process.execPath,
|
||||
[
|
||||
"scripts/publish-model-catalog.mjs",
|
||||
"--input",
|
||||
input,
|
||||
"--source-commit",
|
||||
"test-commit",
|
||||
"--dry-run",
|
||||
],
|
||||
{ cwd: root, encoding: "utf8" },
|
||||
);
|
||||
function createCatalog(prefix, openrouterModels = []) {
|
||||
const input = mkdtempSync(join(tmpdir(), prefix));
|
||||
const chatModels = Array.from({ length: 500 }, (_, index) => ({
|
||||
type: "chat",
|
||||
id: `chat-${index}`,
|
||||
provider: "anthropic",
|
||||
}));
|
||||
writeCatalog(input, {
|
||||
anthropic: chatModels,
|
||||
openai: [],
|
||||
openrouter: openrouterModels,
|
||||
});
|
||||
return input;
|
||||
}
|
||||
|
||||
function runPublisher(args, options = {}) {
|
||||
return spawnSync(process.execPath, ["scripts/publish-model-catalog.mjs", ...args], {
|
||||
cwd: root,
|
||||
encoding: "utf8",
|
||||
...options,
|
||||
});
|
||||
}
|
||||
|
||||
test("publishes explicit per-type and total model counts", (t) => {
|
||||
const input = createCatalog("pi-model-catalog-counts-", [
|
||||
{ type: "image", id: "shared", provider: "openrouter" },
|
||||
{ type: "classifier", id: "shared", provider: "openrouter" },
|
||||
]);
|
||||
t.after(() => rmSync(input, { recursive: true, force: true }));
|
||||
|
||||
const result = runPublisher(["--input", input, "--source-commit", "test-commit", "--dry-run"]);
|
||||
assert.equal(result.status, 0, `${result.stdout}\n${result.stderr}`);
|
||||
const publication = JSON.parse(readFileSync(join(input, "publication.json"), "utf8"));
|
||||
assert.equal(publication.modelCount, 500);
|
||||
|
|
@ -65,3 +70,88 @@ test("publishes explicit per-type and total model counts", (t) => {
|
|||
assert.equal(publication.classifierModelCount, 1);
|
||||
assert.equal(publication.totalModelCount, 502);
|
||||
});
|
||||
|
||||
// Regression test for #9099: a mixed-API publication must not replace the catalog served to older clients.
|
||||
test("publishes a mixed-API catalog without replacing the legacy client route", (t) => {
|
||||
const input = createCatalog("pi-model-catalog-input-");
|
||||
const temporaryDir = mkdtempSync(join(tmpdir(), "pi-model-catalog-publish-"));
|
||||
t.after(() => {
|
||||
rmSync(input, { recursive: true, force: true });
|
||||
rmSync(temporaryDir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
const bucketDir = join(temporaryDir, "bucket");
|
||||
const indexPath = join(bucketDir, "models/v1/index.json");
|
||||
mkdirSync(join(bucketDir, "models/v1"), { recursive: true });
|
||||
const legacyEntry = {
|
||||
minimumPiVersion: "0.80.7",
|
||||
revision: `sha256-${"a".repeat(64)}`,
|
||||
sourceCommit: "legacy-commit",
|
||||
publishedAt: "2026-09-01T00:00:00.000Z",
|
||||
modelCount: 499,
|
||||
};
|
||||
writeJson(indexPath, {
|
||||
schemaVersion: 1,
|
||||
defaultRevision: `sha256-${"b".repeat(64)}`,
|
||||
catalogs: [
|
||||
legacyEntry,
|
||||
{ minimumPiVersion: "0.85.0", revision: `sha256-${"b".repeat(64)}` },
|
||||
],
|
||||
});
|
||||
|
||||
const binDir = join(temporaryDir, "bin");
|
||||
mkdirSync(binDir);
|
||||
const awsPath = join(binDir, "aws");
|
||||
writeFileSync(
|
||||
awsPath,
|
||||
`#!/usr/bin/env node
|
||||
const { copyFileSync, existsSync, mkdirSync } = require("node:fs");
|
||||
const { dirname, join } = require("node:path");
|
||||
const [service, command, source, destination] = process.argv.slice(2);
|
||||
if (service !== "s3" || command !== "cp") process.exit(2);
|
||||
const objectPath = (url) => join(process.env.FAKE_R2_DIR, new URL(url).pathname.slice(1));
|
||||
if (source.startsWith("s3://")) {
|
||||
const path = objectPath(source);
|
||||
if (!existsSync(path)) {
|
||||
console.error("NoSuchKey");
|
||||
process.exit(1);
|
||||
}
|
||||
copyFileSync(path, destination);
|
||||
} else {
|
||||
const path = objectPath(destination);
|
||||
mkdirSync(dirname(path), { recursive: true });
|
||||
copyFileSync(source, path);
|
||||
}
|
||||
`,
|
||||
);
|
||||
chmodSync(awsPath, 0o755);
|
||||
|
||||
const result = runPublisher(
|
||||
[
|
||||
"--input",
|
||||
input,
|
||||
"--bucket",
|
||||
"pi-artifacts",
|
||||
"--endpoint",
|
||||
"http://r2.test",
|
||||
"--source-commit",
|
||||
"mixed-api-commit",
|
||||
],
|
||||
{
|
||||
env: {
|
||||
...process.env,
|
||||
FAKE_R2_DIR: bucketDir,
|
||||
PATH: `${binDir}${delimiter}${process.env.PATH ?? ""}`,
|
||||
},
|
||||
},
|
||||
);
|
||||
assert.equal(result.status, 0, `${result.stdout}\n${result.stderr}`);
|
||||
const publication = JSON.parse(readFileSync(join(input, "publication.json"), "utf8"));
|
||||
const index = JSON.parse(readFileSync(indexPath, "utf8"));
|
||||
assert.equal(index.defaultRevision, publication.revision);
|
||||
assert.deepEqual(index.catalogs[0], legacyEntry);
|
||||
assert.equal(index.catalogs.length, 2);
|
||||
assert.equal(index.catalogs[1].minimumPiVersion, "0.85.0");
|
||||
assert.equal(index.catalogs[1].revision, publication.revision);
|
||||
assert.equal(index.catalogs[1].sourceCommit, "mixed-api-commit");
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue