mirror of
https://github.com/Routstr/routstrd.git
synced 2026-10-05 12:28:23 +00:00
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:
@@ -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
@@ -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 };
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user