From c7b8a2ae5bd1f729a2c458ef6fcfaf7e2a52cf37 Mon Sep 17 00:00:00 2001 From: redshift <213178690+1ftredsh@users.noreply.github.com> Date: Sat, 26 Sep 2026 18:10:58 +0530 Subject: [PATCH] fix: keep model metadata for models served under mapped variant ids MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- src/daemon/models.test.ts | 140 ++++++++++++++++++++++++++++++++++++++ src/daemon/models.ts | 16 +++-- 2 files changed, 152 insertions(+), 4 deletions(-) diff --git a/src/daemon/models.test.ts b/src/daemon/models.test.ts index 8fce420..b372406 100644 --- a/src/daemon/models.test.ts +++ b/src/daemon/models.test.ts @@ -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 = { + baseUrlsList: [], + setBaseUrlsList: () => {}, + disabledProviders: [], + setDisabledProviders: () => {}, + manuallyDisabledProviders: [], + manuallyEnabledProviders: [], + }; + return { getState: () => state } as unknown as SdkStore; + } + + function makeModelManager( + routstr21Ids: string[], + cached: Record, + ) { + 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).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" }]); + }); +}); diff --git a/src/daemon/models.ts b/src/daemon/models.ts index 4fb8c66..fd52ff6 100644 --- a/src/daemon/models.ts +++ b/src/daemon/models.ts @@ -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 }; }); };