Skip to content

Commit 4ff944c

Browse files
committed
fix(ci): stabilize model picker and release checks
1 parent 171675b commit 4ff944c

6 files changed

Lines changed: 159 additions & 15 deletions

File tree

src/agents/cli-backends.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -213,6 +213,25 @@ export function listCliRuntimeModelBackendBindings(
213213
);
214214
}
215215

216+
export function listCliRuntimeProviderIds(
217+
params: {
218+
config?: OpenClawConfig;
219+
env?: NodeJS.ProcessEnv;
220+
includeSetupRegistry?: boolean;
221+
} = {},
222+
): string[] {
223+
// Only CLI backends with a canonical modelProvider are runtime aliases that
224+
// should be hidden from model-provider pickers. Standalone CLI backends own
225+
// direct refs such as acme-cli/model and must remain selectable.
226+
return [
227+
...new Set(
228+
listCliRuntimeModelBackendBindings(params)
229+
.map((binding) => normalizeBackendKey(binding.runtime))
230+
.filter(Boolean),
231+
),
232+
].toSorted();
233+
}
234+
216235
export function resolveCliRuntimeModelBackendBinding(params: {
217236
provider: string | undefined;
218237
runtime: string | undefined;

src/agents/model-picker-visibility.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,26 @@
1+
import type { OpenClawConfig } from "../config/types.openclaw.js";
2+
import { listCliRuntimeProviderIds } from "./cli-backends.js";
13
import { isCliRuntimeProvider } from "./model-runtime-aliases.js";
24
import { normalizeProviderId } from "./provider-id.js";
35

46
const RETIRED_MODEL_PICKER_PROVIDERS = new Set(["codex", "codex-cli"]);
57

8+
export function createModelPickerVisibleProviderPredicate(
9+
params: { config?: OpenClawConfig; env?: NodeJS.ProcessEnv; includeSetupRegistry?: boolean } = {},
10+
): (provider: string) => boolean {
11+
const cliRuntimeProviders = new Set(
12+
listCliRuntimeProviderIds({
13+
config: params.config,
14+
env: params.env,
15+
includeSetupRegistry: params.includeSetupRegistry ?? false,
16+
}),
17+
);
18+
return (provider: string): boolean => {
19+
const normalized = normalizeProviderId(provider);
20+
return !RETIRED_MODEL_PICKER_PROVIDERS.has(normalized) && !cliRuntimeProviders.has(normalized);
21+
};
22+
}
23+
624
export function isModelPickerVisibleProvider(provider: string): boolean {
725
const normalized = normalizeProviderId(provider);
826
return (

src/agents/model-runtime-aliases.test.ts

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
11
import { afterEach, beforeEach, describe, expect, it } from "vitest";
22
import type { OpenClawConfig } from "../config/types.openclaw.js";
33
import { testing as cliBackendsTesting } from "./cli-backends.js";
4-
import { resolveCliRuntimeExecutionProvider } from "./model-runtime-aliases.js";
4+
import { createModelPickerVisibleProviderPredicate } from "./model-picker-visibility.js";
5+
import {
6+
isCliRuntimeProvider,
7+
resolveCliRuntimeExecutionProvider,
8+
} from "./model-runtime-aliases.js";
59

610
function createAnthropicAuthConfig(params: {
711
order?: string[];
@@ -127,4 +131,29 @@ describe("resolveCliRuntimeExecutionProvider", () => {
127131
}),
128132
).toBeUndefined();
129133
});
134+
135+
it("keeps standalone CLI backend provider refs visible", () => {
136+
cliBackendsTesting.setDepsForTest({
137+
resolveRuntimeCliBackends: () => [
138+
{
139+
id: "claude-cli",
140+
modelProvider: "anthropic",
141+
pluginId: "anthropic",
142+
config: { command: "claude" },
143+
},
144+
{
145+
id: "acme-cli",
146+
pluginId: "acme",
147+
config: { command: "acme" },
148+
},
149+
],
150+
});
151+
152+
const isVisibleProvider = createModelPickerVisibleProviderPredicate();
153+
154+
expect(isCliRuntimeProvider("claude-cli")).toBe(true);
155+
expect(isVisibleProvider("claude-cli")).toBe(false);
156+
expect(isCliRuntimeProvider("acme-cli")).toBe(false);
157+
expect(isVisibleProvider("acme-cli")).toBe(true);
158+
});
130159
});

src/agents/model-runtime-aliases.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { normalizeOptionalLowercaseString } from "../shared/string-coerce.js";
33
import {
44
isCliRuntimeModelBackendForProvider,
55
listCliRuntimeModelBackendBindings,
6+
listCliRuntimeProviderIds,
67
resolveCliRuntimeModelBackendBinding,
78
} from "./cli-backends.js";
89
import { resolveModelRuntimePolicy } from "./model-runtime-policy.js";
@@ -17,12 +18,12 @@ export function isCliRuntimeProvider(
1718
params: { config?: OpenClawConfig; env?: NodeJS.ProcessEnv; includeSetupRegistry?: boolean } = {},
1819
): boolean {
1920
const normalized = normalizeProviderId(provider);
20-
return listCliRuntimeModelBackendBindings({
21+
return listCliRuntimeProviderIds({
2122
config: params.config,
2223
env: params.env,
2324
includeSetupRegistry:
2425
params.includeSetupRegistry ?? (params.config !== undefined || params.env !== undefined),
25-
}).some((binding) => binding.runtime === normalized);
26+
}).includes(normalized);
2627
}
2728

2829
export function isCliRuntimeAlias(runtime: string | undefined): boolean {

src/commands/model-picker.test.ts

Lines changed: 47 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
import { beforeEach, describe, expect, it, vi } from "vitest";
1+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
2+
import { testing as cliBackendsTesting } from "../agents/cli-backends.js";
23
import type { OpenClawConfig } from "../config/config.js";
34
import type { NormalizedModelCatalogRow } from "../model-catalog/index.js";
45
import {
@@ -141,7 +142,7 @@ const resolveProviderPluginChoice = vi.hoisted(() => vi.fn());
141142
const runProviderModelSelectedHook = vi.hoisted(() => vi.fn(async () => {}));
142143
const resolvePluginProviders = vi.hoisted(() => vi.fn(() => []));
143144
const runProviderPluginAuthMethod = vi.hoisted(() => vi.fn());
144-
vi.mock("./model-picker.runtime.js", () => ({
145+
vi.mock("../commands/model-picker.runtime.js", () => ({
145146
modelPickerRuntime: {
146147
get resolveProviderModelPickerContributions() {
147148
return providerModelPickerContributionRuntime.enabled
@@ -248,6 +249,46 @@ function providerCallProviders() {
248249
beforeEach(() => {
249250
delete process.env.OPENCLAW_LOCALE;
250251
vi.clearAllMocks();
252+
cliBackendsTesting.setDepsForTest({
253+
resolveRuntimeCliBackends: () => [
254+
{
255+
id: "claude-cli",
256+
modelProvider: "anthropic",
257+
pluginId: "anthropic",
258+
config: { command: "claude" },
259+
},
260+
{
261+
id: "google-gemini-cli",
262+
modelProvider: "google",
263+
pluginId: "google",
264+
config: { command: "gemini" },
265+
},
266+
],
267+
resolvePluginSetupRegistry: () => ({
268+
providers: [],
269+
cliBackends: [
270+
{
271+
pluginId: "anthropic",
272+
backend: {
273+
id: "claude-cli",
274+
modelProvider: "anthropic",
275+
config: { command: "claude" },
276+
},
277+
},
278+
{
279+
pluginId: "google",
280+
backend: {
281+
id: "google-gemini-cli",
282+
modelProvider: "google",
283+
config: { command: "gemini" },
284+
},
285+
},
286+
],
287+
configMigrations: [],
288+
autoEnableProbes: [],
289+
diagnostics: [],
290+
}),
291+
});
251292
loadStaticManifestCatalogRowsForList.mockReturnValue([]);
252293
listProfilesForProvider.mockReturnValue([]);
253294
resolveEnvApiKey.mockImplementation((_provider: string) => ({
@@ -267,6 +308,10 @@ beforeEach(() => {
267308
});
268309
});
269310

311+
afterEach(() => {
312+
cliBackendsTesting.resetDepsForTest();
313+
});
314+
270315
describe("promptDefaultModel", () => {
271316
it("adds runtime-route hints for canonical and legacy OpenAI Codex models", async () => {
272317
loadModelCatalog.mockResolvedValue([

src/flows/model-picker.ts

Lines changed: 42 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,7 @@ import { DEFAULT_MODEL, DEFAULT_PROVIDER } from "../agents/defaults.js";
22
import { resolveVisibleModelCatalog } from "../agents/model-catalog-visibility.js";
33
import { loadModelCatalog } from "../agents/model-catalog.js";
44
import type { ModelCatalogEntry } from "../agents/model-catalog.js";
5-
import {
6-
isModelPickerVisibleModelRef,
7-
isModelPickerVisibleProvider,
8-
} from "../agents/model-picker-visibility.js";
5+
import { createModelPickerVisibleProviderPredicate } from "../agents/model-picker-visibility.js";
96
import { createProviderAuthChecker } from "../agents/model-provider-auth.js";
107
import { formatLiteralProviderPrefixedModelRef } from "../agents/model-ref-shared.js";
118
import {
@@ -244,13 +241,14 @@ async function addModelSelectOption(params: {
244241
aliasIndex: ReturnType<typeof buildModelAliasIndex>;
245242
hasAuth: (provider: string) => Promise<boolean>;
246243
literalPrefixProviders: Set<string>;
244+
isVisibleProvider: (provider: string) => boolean;
247245
}) {
248246
const normalizedRef = normalizeModelRef(params.entry.provider, params.entry.id);
249247
const key = modelKey(normalizedRef.provider, normalizedRef.model);
250248
if (
251249
params.seen.has(key) ||
252250
HIDDEN_ROUTER_MODELS.has(key) ||
253-
!isModelPickerVisibleProvider(normalizedRef.provider)
251+
!params.isVisibleProvider(normalizedRef.provider)
254252
) {
255253
return;
256254
}
@@ -304,6 +302,7 @@ async function addModelKeySelectOption(params: {
304302
aliasIndex: ReturnType<typeof buildModelAliasIndex>;
305303
hasAuth: (provider: string) => Promise<boolean>;
306304
literalPrefixProviders?: Set<string>;
305+
isVisibleProvider: (provider: string) => boolean;
307306
fallbackHint: string;
308307
}) {
309308
const entry = splitModelKey(params.key);
@@ -318,6 +317,7 @@ async function addModelKeySelectOption(params: {
318317
aliasIndex: params.aliasIndex,
319318
hasAuth: params.hasAuth,
320319
literalPrefixProviders: params.literalPrefixProviders ?? EMPTY_LITERAL_PREFIX_PROVIDERS,
320+
isVisibleProvider: params.isVisibleProvider,
321321
});
322322
if (params.seen.size > before) {
323323
const option = params.options.at(-1);
@@ -415,8 +415,9 @@ async function maybeFilterModelsByProvider(params: {
415415
cfg: OpenClawConfig;
416416
workspaceDir?: string;
417417
env?: NodeJS.ProcessEnv;
418+
isVisibleProvider: (provider: string) => boolean;
418419
}): Promise<typeof params.models> {
419-
let next = params.models.filter((entry) => isModelPickerVisibleProvider(entry.provider));
420+
let next = params.models.filter((entry) => params.isVisibleProvider(entry.provider));
420421
const providerIds = sortUniqueStrings(next.map((entry) => entry.provider));
421422
const hasPreferredProvider = !!params.preferredProvider;
422423
const shouldPromptProvider =
@@ -738,13 +739,19 @@ export async function promptDefaultModel(
738739
});
739740
}
740741

742+
const isVisibleProvider = createModelPickerVisibleProviderPredicate({
743+
config: cfg,
744+
env: params.env,
745+
includeSetupRegistry: true,
746+
});
741747
const filteredModels = await maybeFilterModelsByProvider({
742748
models,
743749
preferredProvider,
744750
prompter: params.prompter,
745751
cfg,
746752
workspaceDir: params.workspaceDir,
747753
env: params.env,
754+
isVisibleProvider,
748755
});
749756
if (filteredModels.length === 0) {
750757
return promptManualModel({
@@ -807,6 +814,7 @@ export async function promptDefaultModel(
807814
aliasIndex,
808815
hasAuth,
809816
literalPrefixProviders,
817+
isVisibleProvider,
810818
});
811819
}
812820
if (configuredKey && !seen.has(configuredKey)) {
@@ -947,6 +955,11 @@ export async function promptModelAllowlist(params: {
947955
})
948956
: [];
949957
if (scopedFastKeys.length > 0) {
958+
const isVisibleProvider = createModelPickerVisibleProviderPredicate({
959+
config: cfg,
960+
env: params.env,
961+
includeSetupRegistry: true,
962+
});
950963
const scopeKeys = allowedKeys.length > 0 ? allowedKeys : scopedFastKeys;
951964
const scopeKeySet = new Set(scopeKeys);
952965
const initialKeys = normalizeModelKeys(initialSeeds.filter((key) => scopeKeySet.has(key)));
@@ -964,6 +977,7 @@ export async function promptModelAllowlist(params: {
964977
seen,
965978
aliasIndex,
966979
hasAuth,
980+
isVisibleProvider,
967981
fallbackHint:
968982
allowedKeys.length > 0 ? t("wizard.model.allowed") : t("wizard.model.configured"),
969983
});
@@ -1037,14 +1051,23 @@ export async function promptModelAllowlist(params: {
10371051
workspaceDir: params.workspaceDir,
10381052
env: params.env,
10391053
});
1054+
const isVisibleProvider = createModelPickerVisibleProviderPredicate({
1055+
config: cfg,
1056+
env: params.env,
1057+
includeSetupRegistry: true,
1058+
});
1059+
const isVisibleModelRef = (ref: string): boolean => {
1060+
const separatorIndex = ref.indexOf("/");
1061+
return separatorIndex <= 0 || isVisibleProvider(ref.slice(0, separatorIndex));
1062+
};
10401063

10411064
const options: WizardSelectOption[] = [];
10421065
const seen = new Set<string>();
10431066
const allowedCatalog = (
10441067
allowedKeySet
10451068
? catalog.filter((entry) => allowedKeySet.has(modelKey(entry.provider, entry.id)))
10461069
: catalog
1047-
).filter((entry) => isModelPickerVisibleProvider(entry.provider));
1070+
).filter((entry) => isVisibleProvider(entry.provider));
10481071
const filteredCatalog =
10491072
preferredProvider && allowedCatalog.some((entry) => matchesPreferredProvider?.(entry.provider))
10501073
? allowedCatalog.filter((entry) => matchesPreferredProvider?.(entry.provider))
@@ -1067,7 +1090,7 @@ export async function promptModelAllowlist(params: {
10671090
: initialSeeds;
10681091
const initialKeys = allowedKeySet
10691092
? initialSeeds.filter((key) => allowedKeySet.has(key))
1070-
: selectableInitialSeeds.filter(isModelPickerVisibleModelRef);
1093+
: selectableInitialSeeds.filter(isVisibleModelRef);
10711094

10721095
for (const entry of filteredCatalog) {
10731096
await addModelSelectOption({
@@ -1077,11 +1100,12 @@ export async function promptModelAllowlist(params: {
10771100
aliasIndex,
10781101
hasAuth,
10791102
literalPrefixProviders,
1103+
isVisibleProvider,
10801104
});
10811105
}
10821106

10831107
const supplementalKeys = (allowedKeySet ? allowedKeys : selectableInitialSeeds).filter(
1084-
isModelPickerVisibleModelRef,
1108+
isVisibleModelRef,
10851109
);
10861110
for (const key of supplementalKeys) {
10871111
if (seen.has(key)) {
@@ -1266,9 +1290,17 @@ export function applyModelFallbacksFromSelection(
12661290
scopeKeySet && !includesResolvedPrimary
12671291
? rawSelectedFallbacks.filter((key) => existingFallbackSet.has(key))
12681292
: rawSelectedFallbacks;
1293+
const isVisibleProvider = createModelPickerVisibleProviderPredicate({
1294+
config: cfg,
1295+
includeSetupRegistry: true,
1296+
});
1297+
const isVisibleModelRef = (ref: string): boolean => {
1298+
const separatorIndex = ref.indexOf("/");
1299+
return separatorIndex <= 0 || isVisibleProvider(ref.slice(0, separatorIndex));
1300+
};
12691301
const preserveExistingFallback = scopeKeySet
12701302
? (fallback: string) => !scopeKeySet.has(fallback)
1271-
: (fallback: string) => !isModelPickerVisibleModelRef(fallback);
1303+
: (fallback: string) => !isVisibleModelRef(fallback);
12721304
const fallbacks = mergeFallbackSelection({
12731305
existingFallbacks,
12741306
selectedFallbacks,

0 commit comments

Comments
 (0)