fix: keep model metadata for models served under mapped variant ids

getRoutstr21Models matched routstr21 ids against the aggregated models
by raw `.id`, but the aggregation folds providers' mapped variant ids
into one entry per canonical id while keeping the provider's raw model
object — so its `.id` is the variant (e.g. z-ai-glm-5-3-flash) even
though it carries full metadata. The lookup missed and degraded those
entries to bare { id, name } stubs, dropping context_length,
architecture and reasoning (visible as missing context windows in pi's
models.json).

Resolve through the SDK's findModelForId (exact native id wins, then
mapped variant/alias — same as getModelProviders) and always expose the
requested canonical id so variant ids cannot leak into /models.
This commit is contained in:
redshift
2026-09-26 18:10:58 +05:30
parent 1fc85054b4
commit c7b8a2ae5b
2 changed files with 152 additions and 4 deletions
+140
View File
@@ -149,3 +149,143 @@ describe("createModelService.ensureProvidersBootstrapped", () => {
expect(baseUrlCalls).toEqual([]);
});
});
/**
* `getRoutstr21Models` must resolve each routstr21 id against the aggregated
* provider models even when the surviving entry is a provider's raw model
* under a mapped variant id (e.g. z-ai-glm-5-3-flash for glm-5.3-flash).
* Those entries carry the full metadata (context_length, architecture,
* reasoning) but their `.id` is the variant, so a plain id lookup used to
* miss and degrade the exposed entry to a bare `{ id, name }` stub — which is
* what integrations (pi's models.json) project as missing context windows.
*/
describe("createModelService.getRoutstr21Models", () => {
function makeStore() {
const state: Record<string, unknown> = {
baseUrlsList: [],
setBaseUrlsList: () => {},
disabledProviders: [],
setDisabledProviders: () => {},
manuallyDisabledProviders: [],
manuallyEnabledProviders: [],
};
return { getState: () => state } as unknown as SdkStore;
}
function makeModelManager(
routstr21Ids: string[],
cached: Record<string, unknown[]>,
) {
return {
fetchRoutstr21Models: async () => routstr21Ids,
getAllCachedModels: () => cached,
getBaseUrls: () => Object.keys(cached),
// Warm reads must stay cache-only; these only run when the cache is
// empty and should never be reached in these tests.
bootstrapProviders: async () => {
throw new Error("unexpected bootstrap");
},
syncReviewedProvidersFromNostr: async () => null,
fetchModels: async () => {
throw new Error("unexpected fetchModels");
},
} as never;
}
it("resolves a routstr21 id served only under a mapped variant id, with full metadata", async () => {
const variant = {
id: "z-ai-glm-5-3-flash",
alias_ids: ["glm-5-3-flash", "glm-5.3-flash"],
name: "GLM 5.3 Flash",
context_length: 1048576,
architecture: { input_modalities: ["text", "image"] },
sats_pricing: { completion: 1 },
};
const service = createModelService(
makeModelManager(["glm-5.3-flash"], { "https://p.example/": [variant] }) as never,
{} as never,
makeStore(),
);
const models = await service.getRoutstr21Models();
expect(models).toHaveLength(1);
expect(models[0]!.id).toBe("glm-5.3-flash");
expect(models[0]!.name).toBe("GLM 5.3 Flash");
expect(models[0]!.context_length).toBe(1048576);
expect((models[0] as Record<string, unknown>).architecture).toEqual({
input_modalities: ["text", "image"],
});
});
it("exposes the canonical id when the cheapest entry is a variant of a native model", async () => {
// Two providers serve the same model; the cheaper one only knows it as
// z-ai-glm-5-3. Aggregation folds them into one entry (the variant), and
// the routstr21 id must still resolve to it.
const native = {
id: "glm-5.3",
name: "Z.ai: GLM 5.3",
context_length: 1310720,
sats_pricing: { completion: 10 },
};
const variant = {
id: "z-ai-glm-5-3",
name: "GLM 5.3",
context_length: 1310720,
sats_pricing: { completion: 1 },
};
const service = createModelService(
makeModelManager(["glm-5.3"], {
"https://a.example/": [native],
"https://b.example/": [variant],
}) as never,
{} as never,
makeStore(),
);
const models = await service.getRoutstr21Models();
expect(models).toHaveLength(1);
expect(models[0]!.id).toBe("glm-5.3");
expect(models[0]!.context_length).toBe(1310720);
});
it("keeps full metadata for models served under their native id", async () => {
const native = {
id: "kimi-k3",
name: "Kimi K3",
context_length: 1000000,
sats_pricing: { completion: 1 },
};
const service = createModelService(
makeModelManager(["kimi-k3"], { "https://p.example/": [native] }) as never,
{} as never,
makeStore(),
);
const models = await service.getRoutstr21Models();
expect(models).toHaveLength(1);
expect(models[0]!.id).toBe("kimi-k3");
expect(models[0]!.name).toBe("Kimi K3");
expect(models[0]!.context_length).toBe(1000000);
});
it("falls back to a bare stub for models no provider serves", async () => {
const other = {
id: "kimi-k3",
name: "Kimi K3",
context_length: 1000000,
sats_pricing: { completion: 1 },
};
const service = createModelService(
makeModelManager(["no-such-model"], { "https://p.example/": [other] }) as never,
{} as never,
makeStore(),
);
const models = await service.getRoutstr21Models();
expect(models).toEqual([{ id: "no-such-model", name: "no-such-model" }]);
});
});
+12 -4
View File
@@ -165,11 +165,19 @@ export function createModelService(
);
}
const modelsById = new Map(discoveredModels.map((model) => [model.id, model]));
// Resolve each routstr21 id against the aggregated provider models. The
// aggregation folds providers' mapped variant ids/aliases into one entry
// per canonical id, but the surviving entry is the provider's raw model,
// whose `.id` may be the variant (e.g. z-ai-glm-5-3-flash) — while still
// carrying the full metadata (context_length, architecture, reasoning).
// A plain id map would miss and degrade the entry to a bare stub, so
// resolve through the SDK's model mappings (exact native id wins, same as
// getModelProviders) and always expose the requested canonical id.
return routstr21ModelIds.map((modelId) => {
const model = modelsById.get(modelId);
return model || { id: modelId, name: modelId };
const model = findModelForId(discoveredModels as Model[], modelId);
return model
? { ...model, id: modelId }
: { id: modelId, name: modelId };
});
};