mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-12 21:53:00 -06:00
fix: keep provider ranking in startup catalogs (#115425)
* fix(models): rank static registry catalogs * fix(models): preserve unranked catalog order
This commit is contained in:
committed by
GitHub
parent
ec56892b74
commit
661efb67a3
@@ -8,6 +8,7 @@ import type { ModelCatalogEntry } from "./model-catalog.types.js";
|
||||
export function assignProviderModelOrder(
|
||||
entries: readonly ModelCatalogEntry[],
|
||||
existingEntries: readonly ModelCatalogEntry[] = [],
|
||||
options: { appendUnknown?: boolean } = {},
|
||||
): ModelCatalogEntry[] {
|
||||
const orderByModel = new Map<string, number>();
|
||||
const nextOrderByProvider = new Map<string, number>();
|
||||
@@ -30,6 +31,9 @@ export function assignProviderModelOrder(
|
||||
if (existingOrder !== undefined) {
|
||||
return { ...entry, providerOrder: existingOrder };
|
||||
}
|
||||
if (options.appendUnknown === false) {
|
||||
return entry;
|
||||
}
|
||||
const providerOrder = nextOrderByProvider.get(provider) ?? 0;
|
||||
nextOrderByProvider.set(provider, providerOrder + 1);
|
||||
orderByModel.set(key, providerOrder);
|
||||
|
||||
@@ -112,6 +112,18 @@ describe("prepared model catalog builder", () => {
|
||||
expect(snapshot.routeVariants).toEqual(snapshot.entries);
|
||||
});
|
||||
|
||||
it("keeps unranked registry rows in deterministic model-id order", async () => {
|
||||
const snapshot = await build({
|
||||
entries: [
|
||||
{ id: "model-b", name: "Model B", provider: "demo" },
|
||||
{ id: "model-a", name: "Model A", provider: "demo" },
|
||||
],
|
||||
});
|
||||
|
||||
expect(snapshot.entries.map((entry) => entry.id)).toEqual(["model-a", "model-b"]);
|
||||
expect(snapshot.entries.every((entry) => entry.providerOrder === undefined)).toBe(true);
|
||||
});
|
||||
|
||||
it("keeps account-denied runtime models out of the prepared catalog", async () => {
|
||||
const config: OpenClawConfig = { plugins: { enabled: false } };
|
||||
const runtimeManifest = providerManifestSnapshot({
|
||||
@@ -153,6 +165,30 @@ describe("prepared model catalog builder", () => {
|
||||
]);
|
||||
});
|
||||
|
||||
it("ranks runtime registry rows from the manifest without augmentation", async () => {
|
||||
const snapshot = await build({
|
||||
entries: [
|
||||
{ id: "gpt-5.4", name: "GPT-5.4", provider: "openai" },
|
||||
{ id: "gpt-5.6-luna", name: "GPT-5.6 Luna", provider: "openai" },
|
||||
{ id: "gpt-5.6-sol", name: "GPT-5.6 Sol", provider: "openai" },
|
||||
{ id: "gpt-5.6-terra", name: "GPT-5.6 Terra", provider: "openai" },
|
||||
],
|
||||
metadataSnapshot: providerManifestSnapshot({
|
||||
provider: "openai",
|
||||
discovery: "runtime",
|
||||
modelIds: ["gpt-5.6-sol", "gpt-5.6-terra", "gpt-5.6-luna", "gpt-5.4"],
|
||||
}),
|
||||
includeProviderPluginAugmentation: false,
|
||||
});
|
||||
|
||||
expect(snapshot.entries.map((entry) => entry.id)).toEqual([
|
||||
"gpt-5.6-sol",
|
||||
"gpt-5.6-terra",
|
||||
"gpt-5.6-luna",
|
||||
"gpt-5.4",
|
||||
]);
|
||||
});
|
||||
|
||||
it("canonicalizes manifest-owned provider aliases in registry rows", async () => {
|
||||
const snapshot = await build({
|
||||
entries: [
|
||||
@@ -230,6 +266,47 @@ describe("prepared model catalog builder", () => {
|
||||
]);
|
||||
});
|
||||
|
||||
it("keeps manifest rank for configured runtime models absent from the registry", async () => {
|
||||
mocks.augmentModelCatalogWithProviderPlugins.mockResolvedValueOnce([
|
||||
{ id: "gpt-5.4", name: "GPT-5.4", provider: "openai" },
|
||||
{ id: "gpt-5.6-sol", name: "GPT-5.6 Sol", provider: "openai" },
|
||||
]);
|
||||
|
||||
const snapshot = await build({
|
||||
config: {
|
||||
plugins: { enabled: false },
|
||||
models: {
|
||||
providers: {
|
||||
openai: {
|
||||
api: "openai-responses",
|
||||
baseUrl: "https://api.openai.com/v1",
|
||||
models: [
|
||||
{
|
||||
id: "gpt-5.6-sol",
|
||||
name: "Configured GPT-5.6 Sol",
|
||||
contextWindow: 1_050_000,
|
||||
maxTokens: 128_000,
|
||||
reasoning: true,
|
||||
input: ["text", "image"],
|
||||
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
entries: [{ id: "gpt-5.4", name: "GPT-5.4", provider: "openai" }],
|
||||
metadataSnapshot: providerManifestSnapshot({
|
||||
provider: "openai",
|
||||
discovery: "runtime",
|
||||
modelIds: ["gpt-5.6-sol", "gpt-5.6-terra", "gpt-5.6-luna", "gpt-5.4"],
|
||||
}),
|
||||
readOnly: false,
|
||||
});
|
||||
|
||||
expect(snapshot.entries.map((entry) => entry.id)).toEqual(["gpt-5.6-sol", "gpt-5.4"]);
|
||||
});
|
||||
|
||||
it("preserves explicitly configured runtime-provider models", async () => {
|
||||
mocks.augmentModelCatalogWithProviderPlugins.mockResolvedValueOnce([
|
||||
{
|
||||
@@ -276,8 +353,8 @@ describe("prepared model catalog builder", () => {
|
||||
});
|
||||
|
||||
expect(snapshot.entries.map((entry) => `${entry.provider}/${entry.id}`)).toEqual([
|
||||
"openai/gpt-5.4",
|
||||
"openai/gpt-5.5",
|
||||
"openai/gpt-5.4",
|
||||
]);
|
||||
expect(snapshot.routeVariants).toEqual(
|
||||
expect.arrayContaining([
|
||||
|
||||
+17
-10
@@ -471,6 +471,11 @@ export async function buildPreparedModelCatalogSnapshot(
|
||||
const { buildShouldSuppressBuiltInModel } = await loadModelSuppression();
|
||||
logStage("catalog-deps-ready");
|
||||
const entries = params.modelRegistry.getAll() as DiscoveredModel[];
|
||||
const declaredManifestModels = loadManifestModelCatalog({
|
||||
config: cfg,
|
||||
env,
|
||||
metadataSnapshot: manifestMetadataSnapshot,
|
||||
});
|
||||
logStage("registry-read", `entries=${entries.length}`);
|
||||
|
||||
const shouldSuppressBuiltInModel = buildShouldSuppressBuiltInModel({ config: cfg });
|
||||
@@ -525,8 +530,15 @@ export async function buildPreparedModelCatalogSnapshot(
|
||||
compat,
|
||||
} satisfies ModelCatalogEntry;
|
||||
models.push(model);
|
||||
mergeCatalogRouteVariants(routeVariants, [model]);
|
||||
}
|
||||
// Gateway startup may publish registry rows without runtime augmentation.
|
||||
// Rank them here so both static startup and later live enrichment preserve
|
||||
// provider-owned order instead of falling back to model-id sorting.
|
||||
const orderedRegistryModels = assignProviderModelOrder(models, declaredManifestModels, {
|
||||
appendUnknown: false,
|
||||
});
|
||||
models.splice(0, models.length, ...orderedRegistryModels);
|
||||
mergeCatalogRouteVariants(routeVariants, orderedRegistryModels);
|
||||
const supplementalManifestPlan = planEffectiveModelCatalogRows({
|
||||
registry: {
|
||||
plugins: resolveEligibleManifestCatalogPlugins(manifestMetadataSnapshot, cfg),
|
||||
@@ -544,11 +556,6 @@ export async function buildPreparedModelCatalogSnapshot(
|
||||
);
|
||||
// Runtime declarations describe possible models, not account entitlement.
|
||||
// Only live registry or refreshed rows may publish those provider models.
|
||||
const declaredManifestModels = loadManifestModelCatalog({
|
||||
config: cfg,
|
||||
env,
|
||||
metadataSnapshot: manifestMetadataSnapshot,
|
||||
});
|
||||
const manifestModels = declaredManifestModels.filter((entry) =>
|
||||
supplementalManifestKeys.has(catalogEntryDedupeKey(entry.provider, entry.id)),
|
||||
);
|
||||
@@ -636,10 +643,10 @@ export async function buildPreparedModelCatalogSnapshot(
|
||||
}
|
||||
// Manifest ranks are provider-owned policy. Live discovery enriches
|
||||
// those rows and appends unknown models without replacing the ranking.
|
||||
const orderedSupplemental = assignProviderModelOrder(
|
||||
normalizedSupplemental,
|
||||
declaredManifestModels,
|
||||
);
|
||||
const orderedSupplemental = assignProviderModelOrder(normalizedSupplemental, [
|
||||
...declaredManifestModels,
|
||||
...models,
|
||||
]);
|
||||
mergeCatalogRouteVariants(routeVariants, orderedSupplemental);
|
||||
mergeCatalogEntries(models, orderedSupplemental);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user