fix(ui): clarify model setup flows (#116086)

This commit is contained in:
Vincent Koc
2026-07-30 09:47:57 +08:00
committed by GitHub
parent 21db50efc7
commit 12e5eb6a23
15 changed files with 1226 additions and 120 deletions
@@ -6,6 +6,8 @@ export type SetupInferenceManualProvider = {
id: string;
/** Canonical provider identity for clients with bundled brand artwork. */
brandId?: string;
/** Provider family shown above the specific credential method. */
groupLabel?: string;
label: string;
hint?: string;
icon?: string;
@@ -48,6 +50,7 @@ export function listSetupInferenceManualProviders(
choices.set(id, {
id,
brandId: choice.providerId,
...(choice.groupLabel?.trim() ? { groupLabel: choice.groupLabel.trim() } : {}),
label: choice.choiceLabel,
...(choice.choiceHint?.trim() ? { hint: choice.choiceHint.trim() } : {}),
...(choice.icon ? { icon: choice.icon } : {}),
@@ -55,7 +58,13 @@ export function listSetupInferenceManualProviders(
});
}
return [...choices.values()].toSorted(
(a, b) => a.label.localeCompare(b.label, "en") || a.id.localeCompare(b.id, "en"),
(a, b) =>
compareProviderAuthChoiceGroups(
{ id: a.brandId ?? a.id, label: a.groupLabel ?? a.label },
{ id: b.brandId ?? b.id, label: b.groupLabel ?? b.label },
) ||
a.label.localeCompare(b.label, "en") ||
a.id.localeCompare(b.id, "en"),
);
}
+41 -4
View File
@@ -1,4 +1,7 @@
import { normalizeOptionalAgentRuntimeId } from "../agents/agent-runtime-id.js";
import { resolveAgentEffectiveModelPrimary, resolveDefaultAgentId } from "../agents/agent-scope.js";
import { areRuntimeModelRefsEquivalent } from "../agents/model-runtime-aliases.js";
import { resolveModelRuntimePolicy } from "../agents/model-runtime-policy.js";
import { normalizeProviderId } from "../agents/model-selection.js";
import { detectInferenceBackends } from "../commands/onboard-inference.js";
import { formatErrorMessage } from "../infra/errors.js";
@@ -28,6 +31,31 @@ import {
} from "./setup-inference-core.js";
import { parseRef } from "./setup-inference-plan-helpers.js";
function resolveConfiguredCandidateKind(
config: Parameters<typeof resolveModelRuntimePolicy>[0]["config"],
modelRef: string | undefined,
): SetupInferenceCandidate["kind"] | undefined {
if (!modelRef) {
return undefined;
}
const ref = parseRef(modelRef);
const runtime = normalizeOptionalAgentRuntimeId(
resolveModelRuntimePolicy({
config,
provider: ref.provider,
modelId: ref.model,
agentId: resolveDefaultAgentId(config ?? {}),
}).policy?.id,
);
if (runtime === "codex") {
return "codex-cli";
}
if (runtime === "claude-cli") {
return "claude-cli";
}
return undefined;
}
/**
* Manual setup options only — no CLI probing, no credential discovery. Used
* when guarded onboarding declines the "look around" step: the option lists
@@ -113,7 +141,19 @@ export async function detectSetupInference(
"OpenCode CLI is installed, but its ACP harness requires separate setup and is not a reusable guided-setup inference route.",
});
}
const raw = detected.filter((candidate) => candidate.kind !== "gemini-cli");
const configuredModel = detected.find(
(candidate) => candidate.kind === "existing-model",
)?.modelRef;
const configuredCandidateKind = resolveConfiguredCandidateKind(cfg, configuredModel);
const raw = detected.filter(
(candidate) =>
candidate.kind !== "gemini-cli" &&
!(
candidate.kind === configuredCandidateKind &&
configuredModel &&
areRuntimeModelRefsEquivalent(candidate.modelRef, configuredModel, { config: cfg })
),
);
const { workspace } = await resolveSetupInferenceWorkspace({
configExists: snapshot.exists,
configValid: snapshot.valid,
@@ -174,9 +214,6 @@ export async function detectSetupInference(
resolveCandidatePresentation(candidate, authChoices),
),
);
const configuredModel = candidates.find(
(candidate) => candidate.kind === "existing-model",
)?.modelRef;
const discoveryChoices = authChoices.filter(
(choice) =>
choice.appGuidedDiscovery === true && supportsSetupTextInference(choice.onboardingScopes),
+115
View File
@@ -626,6 +626,7 @@ describe("detectSetupInference", () => {
choiceId: "zeta-api-key",
choiceLabel: "Zeta API key",
choiceHint: "Direct key",
groupLabel: "Zeta",
icon: "https://cdn.example.com/zeta.svg",
website: "https://zeta.example.com/keys",
optionKey: "zetaApiKey",
@@ -638,6 +639,7 @@ describe("detectSetupInference", () => {
methodId: "api-key",
choiceId: "alpha-api-key",
choiceLabel: "Alpha API key",
groupLabel: "Alpha",
appGuidedSecret: true,
},
{
@@ -656,6 +658,7 @@ describe("detectSetupInference", () => {
{
id: "alpha-api-key",
brandId: "alpha",
groupLabel: "Alpha",
label: "Alpha API key",
},
{
@@ -666,6 +669,7 @@ describe("detectSetupInference", () => {
{
id: "zeta-api-key",
brandId: "zeta",
groupLabel: "Zeta",
label: "Zeta API key",
hint: "Direct key",
icon: "https://cdn.example.com/zeta.svg",
@@ -809,6 +813,117 @@ describe("detectSetupInference", () => {
});
});
it("does not re-offer the configured Codex route as a setup candidate", async () => {
const { readConfigFileSnapshot } = await import("../config/config.js");
const config: OpenClawConfig = {
agents: {
defaults: { model: "openai/gpt-5.6-sol" },
entries: {
main: {
default: true,
models: {
"openai/gpt-5.6-sol": { agentRuntime: { id: "codex" } },
},
},
},
},
};
vi.mocked(readConfigFileSnapshot).mockResolvedValueOnce({
exists: true,
valid: true,
path: "/tmp/openclaw.json",
issues: [],
config,
sourceConfig: config,
runtimeConfig: config,
} as never);
vi.mocked(detectInferenceBackends).mockResolvedValueOnce([
{
kind: "existing-model",
modelRef: "openai/gpt-5.6-sol",
label: "Current model",
detail: "openai/gpt-5.6-sol — already configured",
credentials: true,
},
{
kind: "claude-cli",
modelRef: "claude-cli/claude-opus-5",
label: "Claude Code",
detail: "logged in",
credentials: true,
},
{
kind: "codex-cli",
modelRef: "openai/gpt-5.6-sol",
label: "Codex",
detail: "logged in",
credentials: true,
},
]);
const detection = await detectSetupInference({
resolveManifestProviderAuthChoices: () => [],
probeLocalCommand: vi.fn(async (command) => ({ command, found: false })),
});
expect(detection.candidates.map((candidate) => candidate.kind)).toEqual([
"existing-model",
"claude-cli",
]);
});
it("keeps a Codex candidate when it would switch the configured model", async () => {
const { readConfigFileSnapshot } = await import("../config/config.js");
const config: OpenClawConfig = {
agents: {
defaults: { model: "openai/gpt-5.5" },
entries: {
main: {
default: true,
models: {
"openai/gpt-5.5": { agentRuntime: { id: "codex" } },
},
},
},
},
};
vi.mocked(readConfigFileSnapshot).mockResolvedValueOnce({
exists: true,
valid: true,
path: "/tmp/openclaw.json",
issues: [],
config,
sourceConfig: config,
runtimeConfig: config,
} as never);
vi.mocked(detectInferenceBackends).mockResolvedValueOnce([
{
kind: "existing-model",
modelRef: "openai/gpt-5.5",
label: "Current model",
detail: "openai/gpt-5.5 — already configured",
credentials: true,
},
{
kind: "codex-cli",
modelRef: "openai/gpt-5.6-sol",
label: "Codex",
detail: "logged in",
credentials: true,
},
]);
const detection = await detectSetupInference({
resolveManifestProviderAuthChoices: () => [],
probeLocalCommand: vi.fn(async (command) => ({ command, found: false })),
});
expect(detection.candidates.map((candidate) => candidate.kind)).toEqual([
"existing-model",
"codex-cli",
]);
});
it("omits Gemini CLI because setup verification cannot hard-disable its tools", async () => {
vi.mocked(detectInferenceBackends).mockResolvedValueOnce([
{