mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-27 12:56:01 -06:00
fix(ui): clarify model setup flows (#116086)
This commit is contained in:
@@ -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"),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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([
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user