Skip to content

Commit e735d94

Browse files
refactor(models): unify list row identity
Co-authored-by: 毛宇豪0668001457 <[email protected]>
1 parent f7bb38f commit e735d94

9 files changed

Lines changed: 433 additions & 226 deletions

src/commands/models/list.configured.test.ts

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,10 @@ describe("resolveConfiguredEntries", () => {
5050
const { entries } = resolveConfiguredEntries({
5151
agents: {
5252
defaults: {
53-
model: { primary: "kilocode/google/gemini-3-pro-preview" },
53+
model: {
54+
primary: "openai/gpt-5.5",
55+
fallbacks: ["kilocode/google/gemini-3-pro-preview"],
56+
},
5457
models: {
5558
"kilocode/google/gemini-3-pro-preview": { alias: "Kilo Gemini" },
5659
},
@@ -77,9 +80,12 @@ describe("resolveConfiguredEntries", () => {
7780
},
7881
});
7982

80-
expect(entries.map((entry) => entry.key)).toEqual(["kilocode/google/gemini-3.1-pro-preview"]);
81-
expect(entries[0]?.aliases).toEqual(["Kilo Gemini"]);
82-
expect(entries[0]?.tags).toEqual(new Set(["default", "configured"]));
83+
expect(entries.map((entry) => entry.key)).toEqual([
84+
"openai/gpt-5.5",
85+
"kilocode/google/gemini-3.1-pro-preview",
86+
]);
87+
expect(entries[1]?.aliases).toEqual(["Kilo Gemini"]);
88+
expect(entries[1]?.tags).toEqual(new Set(["fallback#1", "configured"]));
8389
});
8490
it("treats provider wildcard defaults as selectors, not configured model rows", () => {
8591
const { entries } = resolveConfiguredEntries({
@@ -126,14 +132,21 @@ describe("resolveConfiguredEntries", () => {
126132
const { entries } = resolveConfiguredEntries({
127133
agents: {
128134
defaults: {
129-
model: { primary: "azure-openai-responses/gpt-5.4" },
135+
model: {
136+
primary: "azure-openai-responses/gpt-5.4",
137+
fallbacks: ["openai/gpt-5.4"],
138+
},
130139
},
131140
},
132141
models: { providers: {} },
133142
});
134143

135-
expect(entries.map((entry) => entry.key)).toEqual(["azure-openai-responses/gpt-5.4"]);
144+
expect(entries.map((entry) => entry.key)).toEqual([
145+
"azure-openai-responses/gpt-5.4",
146+
"openai/gpt-5.4",
147+
]);
136148
expect(entries[0]?.tags).toEqual(new Set(["default"]));
149+
expect(entries[1]?.tags).toEqual(new Set(["fallback#1"]));
137150
});
138151

139152
it("recovers bundled source aliases when stale dist metadata omits them", () => {

src/commands/models/list.configured.ts

Lines changed: 63 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
/** Resolves configured model refs and tags for model-list rows. */
2+
import { DEFAULT_CONTEXT_TOKENS } from "../../agents/defaults.js";
3+
import { normalizeConfiguredProviderCatalogModelId } from "../../agents/model-ref-shared.js";
24
import {
35
buildModelAliasIndex,
46
resolveConfiguredModelRef,
@@ -10,16 +12,65 @@ import {
1012
} from "../../config/model-input.js";
1113
import type { OpenClawConfig } from "../../config/types.openclaw.js";
1214
import type { PluginMetadataSnapshot } from "../../plugins/plugin-metadata-snapshot.js";
15+
import type { ListRowModel } from "./list.model-row.js";
1316
import type { ConfiguredEntry } from "./list.types.js";
14-
import { createModelCatalogProviderAliasCanonicalizer } from "./provider-aliases.js";
15-
import { DEFAULT_MODEL, DEFAULT_PROVIDER, modelKey } from "./shared.js";
17+
import {
18+
createModelCatalogProviderAliasCanonicalizer,
19+
type ModelCatalogProviderAliasCanonicalizer,
20+
} from "./provider-aliases.js";
21+
import { DEFAULT_MODEL, DEFAULT_PROVIDER } from "./shared.js";
1622

1723
const DISPLAY_MODEL_PARSE_OPTIONS = { allowPluginNormalization: false } as const;
1824

25+
export type ConfiguredProviderCandidate = {
26+
model: ListRowModel;
27+
explicitApi: boolean;
28+
};
29+
30+
/** Builds the canonical configured-provider rows once for every list source. */
31+
export function resolveConfiguredProviderCandidates(
32+
cfg: OpenClawConfig,
33+
canonicalize: ModelCatalogProviderAliasCanonicalizer,
34+
metadataSnapshot?: Pick<PluginMetadataSnapshot, "manifestRegistry">,
35+
): Map<string, ConfiguredProviderCandidate> {
36+
const candidates = new Map<string, ConfiguredProviderCandidate>();
37+
for (const [rawProvider, providerConfig] of Object.entries(cfg.models?.providers ?? {})) {
38+
const provider = canonicalize.provider(rawProvider);
39+
for (const model of providerConfig.models ?? []) {
40+
const id = normalizeConfiguredProviderCatalogModelId(provider, model.id, {
41+
manifestPlugins: metadataSnapshot?.manifestRegistry.plugins,
42+
});
43+
const key = canonicalize.key(provider, id);
44+
// Config order decides alias/case collisions so every source sees one stable row.
45+
if (candidates.has(key)) {
46+
continue;
47+
}
48+
const input = model.input?.filter((item) => item === "text" || item === "image") ?? [];
49+
candidates.set(key, {
50+
model: {
51+
provider,
52+
id,
53+
name: model.name ?? model.id,
54+
baseUrl: model.baseUrl ?? providerConfig.baseUrl,
55+
input: input.length > 0 ? input : ["text"],
56+
contextWindow: model.contextWindow ?? DEFAULT_CONTEXT_TOKENS,
57+
contextTokens: model.contextTokens,
58+
},
59+
explicitApi: providerConfig.api !== undefined || model.api !== undefined,
60+
});
61+
}
62+
}
63+
return candidates;
64+
}
65+
1966
/** Returns canonical configured model entries with default/fallback/image/configured tags. */
2067
export function resolveConfiguredEntries(
2168
cfg: OpenClawConfig,
2269
metadataSnapshot?: Pick<PluginMetadataSnapshot, "manifestRegistry">,
70+
canonicalizeProviderAlias = createModelCatalogProviderAliasCanonicalizer({
71+
cfg,
72+
metadataSnapshot,
73+
}),
2374
) {
2475
const resolvedDefault = resolveConfiguredModelRef({
2576
cfg,
@@ -32,39 +83,29 @@ export function resolveConfiguredEntries(
3283
defaultProvider: DEFAULT_PROVIDER,
3384
...DISPLAY_MODEL_PARSE_OPTIONS,
3485
});
35-
const order: string[] = [];
86+
const order: Array<{ key: string; ref: { provider: string; model: string } }> = [];
3687
const tagsByKey = new Map<string, Set<string>>();
3788
const aliasesByKey = new Map<string, string[]>();
38-
const canonicalizeProviderAlias = createModelCatalogProviderAliasCanonicalizer({
39-
cfg,
40-
metadataSnapshot,
41-
});
42-
4389
for (const [key, aliases] of aliasIndex.byKey.entries()) {
44-
aliasesByKey.set(key, aliases);
90+
const canonicalKey = canonicalizeProviderAlias.keyFromString(key);
91+
aliasesByKey.set(canonicalKey, [
92+
...new Set([...(aliasesByKey.get(canonicalKey) ?? []), ...aliases]),
93+
]);
4594
}
4695

4796
const addEntry = (ref: { provider: string; model: string }, tag: string) => {
4897
const canonicalRef = canonicalizeProviderAlias.ref(ref);
49-
const key = modelKey(canonicalRef.provider, canonicalRef.model);
50-
const originalKey = modelKey(ref.provider, ref.model);
51-
if (originalKey !== key) {
52-
// Preserve aliases attached to pre-canonical provider keys so display rows
53-
// still show user-facing aliases after catalog provider canonicalization.
54-
const aliases = aliasesByKey.get(originalKey);
55-
if (aliases) {
56-
aliasesByKey.set(key, [...new Set([...(aliasesByKey.get(key) ?? []), ...aliases])]);
57-
}
58-
}
98+
const key = canonicalizeProviderAlias.key(canonicalRef.provider, canonicalRef.model);
5999
if (!tagsByKey.has(key)) {
60100
tagsByKey.set(key, new Set());
61-
order.push(key);
101+
order.push({ key, ref: canonicalRef });
62102
}
63103
tagsByKey.get(key)?.add(tag);
64104
};
65105

66106
const addResolvedModelRef = (raw: string, tag: string) => {
67107
const resolved = resolveModelRefFromString({
108+
cfg,
68109
raw,
69110
defaultProvider: DEFAULT_PROVIDER,
70111
aliasIndex,
@@ -110,13 +151,10 @@ export function resolveConfiguredEntries(
110151
addEntry(resolved.ref, "configured");
111152
}
112153

113-
const entries: ConfiguredEntry[] = order.map((key) => {
114-
const slash = key.indexOf("/");
115-
const provider = slash === -1 ? key : key.slice(0, slash);
116-
const model = slash === -1 ? "" : key.slice(slash + 1);
154+
const entries: ConfiguredEntry[] = order.map(({ key, ref }) => {
117155
return {
118156
key,
119-
ref: { provider, model },
157+
ref,
120158
tags: tagsByKey.get(key) ?? new Set(),
121159
aliases: aliasesByKey.get(key) ?? [],
122160
} satisfies ConfiguredEntry;

0 commit comments

Comments
 (0)