fix(coding-agent): serve catalogs without version redirect (#11690)

Update the model catalog protocol to pi.dev after a change to stop
pi.dev redirecting released clients to `?pi-version=` URLs and serve
their catalog directly. The released CLI is unaffected.
This commit is contained in:
Vegard Stikbakke 2026-11-02 14:42:17 +01:00 • committed by GitHub
parent 9823079ad9
commit 62752fd4c3
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 13 additions and 18 deletions

View file

@ -61,8 +61,6 @@ function startCatalogServer(requests: string[]): Promise<Server> {
const catalogRequest = parseModelCatalogRequest(url, request.headers["user-agent"]);
if (!provider) {
response.writeHead(404).end();
} else if (catalogRequest.kind === "redirect") {
response.writeHead(307, { location: catalogRequest.location, "cache-control": "no-store" }).end();
} else if (catalogRequest.kind === "invalid") {
response.writeHead(400).end(catalogRequest.error);
} else {
@ -120,7 +118,7 @@ describe("model catalog protocol with the current client", () => {
expect([...refresh.errors]).toEqual([]);
const catalogUrl = "/api/models/providers/openrouter?types=chat%2Cimage%2Cclassifier";
expect(requests).toEqual([catalogUrl, `${catalogUrl}&pi-version=${VERSION}`]);
expect(requests).toEqual([catalogUrl]);
const expectedModel =
selectModelCatalog(index, VERSION)?.revision === legacyRevision ? legacyModel : mixedApiModel;

View file

@ -55,25 +55,23 @@ test("selects the newest catalog a Pi version supports", () => {
assert.equal(selectModelCatalog(index, undefined)?.revision, mixedApiRevision);
});
test("redirects Pi user agents to an explicit catalog version", () => {
test("serves Pi user agents the catalog for their version", () => {
// Regression test for #9099: released clients identify themselves only by User-Agent.
assert.deepEqual(
parseModelCatalogRequest(
"https://pi.dev/api/models/providers/openrouter?types=chat%2Cimage",
"pi/0.84.4 (linux; node/v22.0.0; x64)",
),
{
kind: "redirect",
location: "https://pi.dev/api/models/providers/openrouter?types=chat%2Cimage&pi-version=0.84.4",
},
{ kind: "catalog", piVersion: "0.84.4", representation: "typed" },
);
assert.deepEqual(parseModelCatalogRequest("https://pi.dev/api/models", "pi/0.84.4"), {
kind: "redirect",
location: "https://pi.dev/api/models?pi-version=0.84.4",
kind: "catalog",
piVersion: "0.84.4",
representation: "legacy",
});
});
test("serves explicit and unversioned catalog requests without redirecting", () => {
test("serves explicit and unversioned catalog requests", () => {
assert.deepEqual(
parseModelCatalogRequest("https://pi.dev/api/models?pi-version=0.83.0", "pi/0.85.1 (linux; node/v22.0.0; x64)"),
{ kind: "catalog", piVersion: "0.83.0", representation: "legacy" },

View file

@ -53,7 +53,6 @@ export type ModelCatalogIndex = {
export type ModelCatalogRequest =
| { kind: "catalog"; piVersion: string | undefined; representation: ModelCatalogRepresentation }
| { kind: "redirect"; location: string }
| { kind: "invalid"; error: string };
export function getModelCatalogArtifactKey(revision: string, artifact: ModelCatalogArtifact): string {
@ -97,10 +96,11 @@ export function parseModelCatalogRepresentation(
/**
* Decide how to answer a catalog request from its URL and User-Agent.
*
* Released clients do not send `pi-version`. Responses are cached by URL, so
* instead of varying the response on the User-Agent, clients that identify
* as Pi are redirected to the equivalent URL with an explicit `pi-version`.
* Requests without a Pi User-Agent receive the default catalog.
* Released clients do not send `pi-version`, so clients that identify as Pi
* are served the catalog for the version in their User-Agent. The response
* therefore varies on the User-Agent, and the server must say so with `Vary`.
* An explicit `pi-version` takes precedence. Requests without a Pi User-Agent
* receive the default catalog.
*/
export function parseModelCatalogRequest(url: string | URL, userAgent: string | null | undefined): ModelCatalogRequest {
const requestUrl = new URL(url);
@ -113,8 +113,7 @@ export function parseModelCatalogRequest(url: string | URL, userAgent: string |
if (piVersion === null) {
const userAgentVersion = PI_USER_AGENT_RE.exec(userAgent ?? "")?.[1];
if (userAgentVersion !== undefined && isValidModelCatalogPiVersion(userAgentVersion)) {
requestUrl.searchParams.set("pi-version", userAgentVersion);
return { kind: "redirect", location: requestUrl.toString() };
return { kind: "catalog", piVersion: userAgentVersion, representation };
}
return { kind: "catalog", piVersion: undefined, representation };
}