mirror of
https://github.com/openclaw/openclaw.git
synced 2026-10-03 01:29:56 +00:00
fix(auth): use activated credentials for automatic chats (#144975)
Related: #136257 ## What Problem This Solves Fixes an issue where activating a verified replacement sign-in left existing and fresh chats using the old credential, even after a restart. An operator report on September 11 also found misleading model-access feedback, a “Replace key” action that could add an account, and missing command-line activation after unattended setup. ## Why This Change Was Made The shared credential selector now resolves the activated account for normal replies, `/btw` side questions, and worker inference, preserving explicit account choices and different-model behavior. Duplicate caller logic is removed. Activation also compares normalized runtime settings consistently, so omitted optional model fields do not cause a false connection-change error after saving. The existing setup owner also serves the new `openclaw models auth activate` command; both activation prompts default to Yes after verification. Saved model access is reported separately from application by the running Gateway, and the provider-wide key action is labeled accurately. ## User Impact - Automatic chats and `/btw` side questions use the activated account; explicit account pins retain precedence. - Unattended replacement setup prints a command that tests and activates the saved sign-in. - Enter accepts a successfully verified connection. - Saved-but-unapplied settings include recovery guidance instead of a failed-save message. ## Evidence - Real Gateway and recording-provider requests: old credential before; replacement afterward in existing and fresh chats. Explicit pins and a different model retain their expected account selection. - Real Telegram `/btw`: the activated replacement handles an existing automatic chat, while an explicit old-account pin still uses the old account. The same flow first exposed a false activation error for sparse saved model settings; the corrected flow succeeds. - Independent public CLI/browser validation confirms the printed activation command, Enter-to-activate, and the key editor. - Real Telegram Test Server: “Show all” saves the choice in both cases; disabled reload reports saved/unconfirmed, while enabled reload reports visible models. - The initial fixes passed 377 focused tests across nine files. The caller correction passed 309 tests across five files, including `/btw`, normal replies, worker inference, and person-linked account pins. The configured-account regression fails on the pinned base. A further 17 activation/acknowledgement tests pass, including sparse saved settings and rejection of real concurrent changes. - All 17 sanitized operator-config shapes reach Gateway readiness. The Tailscale-dependent fixture uses the supported per-run exposure override on the Linux test host; Tailscale Serve itself is not claimed. - Synthetic provider credentials only. No database, migration, configuration-key, or protocol changes in this PR. | Before | After | | --- | --- | |  |  | Captures are cropped to the changed control and exclude account identifiers. Additional catalog check: sign-in completes through the real xAI plugin against a simulated provider, but its existing endpoint metadata blocks live OAuth catalog discovery. That plugin is unchanged here. New-model discovery, first-account onboarding, and real vendor entitlement are not claimed by this PR. Co-authored-by: Ayaan Zaidi <hi@obviy.us>
This commit is contained in:
parent
a976d4134b
commit
63239351ed
30 changed files with 336 additions and 56 deletions
|
|
@ -63,8 +63,10 @@ restart, without caching a verification result. This adds no database schema or
|
|||
migration; older runtimes do not enforce the inactive state. Before downgrading,
|
||||
remove saved inactive replacements or restore the state from before setup.
|
||||
|
||||
Noninteractive setup saves replacement credentials for later activation in Model
|
||||
Setup and leaves the current connection unchanged. Reusing an existing credential
|
||||
Noninteractive setup saves replacement credentials and prints a
|
||||
`openclaw models auth activate <profileId> --agent <id>` command to test and activate
|
||||
the saved sign-in. Model Setup offers the same operation. Interactive setup defaults
|
||||
to activation after a successful test. Reusing an existing credential
|
||||
and first-run noninteractive setup retain their existing behavior.
|
||||
|
||||
## Agent copy portability
|
||||
|
|
|
|||
|
|
@ -314,6 +314,7 @@ openclaw models auth list [--provider <id>] [--json]
|
|||
openclaw models auth login --provider <id> [--agent <agentId>]
|
||||
openclaw models auth login --provider openai --profile-id openai:work
|
||||
openclaw models auth login-github-copilot
|
||||
openclaw models auth activate <profileId> [--agent <id>]
|
||||
openclaw models auth logout <profileId> [--yes]
|
||||
openclaw models auth paste-api-key --provider <id>
|
||||
openclaw models auth setup-token --provider <id>
|
||||
|
|
@ -341,11 +342,13 @@ fallback applies to auth changes, not to `models list`.
|
|||
|
||||
For the shared-main agent, `--force` clears the provider's shared credentials and main-agent local overrides, including their order and health state. For another agent it clears only that agent's local profiles, leaving shared credentials unchanged. A busy auth store stops the command before login starts; close other OpenClaw commands using the same state directory and retry. SQLite lock diagnostics can name either the shared state database or an agent database, so checking only the legacy auth file for open handles does not rule out contention.
|
||||
|
||||
`models auth activate <profileId>` tests a saved sign-in and selects its verified model and account for the chosen agent. Use the exact command printed after unattended replacement setup, or find the saved id with `models auth list --json`. This command confirms activation without another prompt; a failed test leaves the current connection unchanged.
|
||||
|
||||
`models auth logout <profileId>` removes one saved auth profile from the selected agent auth store. Use the profile id shown by `models auth list`. It also drops that profile from `auth.profiles` and from every `auth.order` list in your config, so no stale reference is left behind, and it deletes an `auth.order.<provider>` entry that would otherwise be emptied (an authored empty order means "select no profiles" and would disable the provider). It prompts for confirmation on a TTY; pass `--yes` for scripts and agents. Provider key references are cleared before the credential is removed. Model defaults and connection settings stay unchanged. Logout refuses when the profile is not in the store.
|
||||
|
||||
`models auth login-github-copilot` is a shortcut for `models auth login --provider github-copilot --method device` (GitHub device flow); it accepts `--yes` to overwrite an existing profile without prompting.
|
||||
|
||||
Use either `openclaw models auth --agent <id> <subcommand>` or `openclaw models auth <subcommand> --agent <id>` to target a specific configured agent store. Both forms are supported by `add`, `list`, `login`, `logout`, `paste-api-key`, `setup-token`, `paste-token`, `login-github-copilot`, and `order get`/`set`/`clear`.
|
||||
Use either `openclaw models auth --agent <id> <subcommand>` or `openclaw models auth <subcommand> --agent <id>` to target a specific configured agent store. Both forms are supported by `add`, `list`, `login`, `activate`, `logout`, `paste-api-key`, `setup-token`, `paste-token`, `login-github-copilot`, and `order get`/`set`/`clear`.
|
||||
|
||||
For OpenAI models, `--provider openai` defaults to ChatGPT/Codex account login. Use `--method api-key` only when you want to add an OpenAI API-key profile, usually as a backup for Codex subscription limits. Run `openclaw doctor --fix` to migrate older legacy OpenAI Codex prefix auth/profile state to `openai`.
|
||||
|
||||
|
|
|
|||
|
|
@ -39,9 +39,12 @@ async function select(params: {
|
|||
sessionEntry: SessionEntry;
|
||||
configuredProfileId?: string;
|
||||
modelId?: string;
|
||||
cfg?: OpenClawConfig;
|
||||
agentId?: string;
|
||||
}) {
|
||||
return await resolveSessionAuthSelection({
|
||||
cfg: {} as OpenClawConfig,
|
||||
cfg: params.cfg ?? {},
|
||||
agentId: params.agentId,
|
||||
provider: "openai",
|
||||
modelId: params.modelId ?? "gpt-5.6-sol",
|
||||
...(params.configuredProfileId ? { configuredProfileId: params.configuredProfileId } : {}),
|
||||
|
|
@ -54,6 +57,43 @@ async function select(params: {
|
|||
}
|
||||
|
||||
describe("session auth selection prepared facts", () => {
|
||||
it.each([
|
||||
{ source: "auto", selectedModel: "gpt-4.1", expected: TEST_SECONDARY_PROFILE_ID },
|
||||
{ source: "user", selectedModel: "gpt-4.1", expected: TEST_PRIMARY_PROFILE_ID },
|
||||
{ source: "auto", selectedModel: "gpt-4.1-mini", expected: TEST_PRIMARY_PROFILE_ID },
|
||||
] as const)(
|
||||
"selects $expected for $source sessions using $selectedModel after activation",
|
||||
async ({ source, selectedModel, expected }) => {
|
||||
await withAuthState(async (state) => {
|
||||
configureProfiles();
|
||||
const sessionEntry: SessionEntry = {
|
||||
sessionId: "existing-session",
|
||||
updatedAt: 1,
|
||||
compactionCount: 0,
|
||||
authProfileOverride: TEST_PRIMARY_PROFILE_ID,
|
||||
authProfileOverrideSource: source,
|
||||
authProfileOverrideCompactionCount: 0,
|
||||
};
|
||||
await expect(
|
||||
select({
|
||||
agentDir: state.agentDir(),
|
||||
agentId: "main",
|
||||
cfg: {
|
||||
agents: {
|
||||
entries: { main: { model: `openai/gpt-4.1@${TEST_SECONDARY_PROFILE_ID}` } },
|
||||
},
|
||||
},
|
||||
modelId: selectedModel,
|
||||
sessionEntry,
|
||||
}),
|
||||
).resolves.toMatchObject({
|
||||
profileId: expected,
|
||||
source: source === "user" || expected === TEST_SECONDARY_PROFILE_ID ? "user" : "auto",
|
||||
});
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
it("returns prepared facts for a user pin", async () => {
|
||||
await withAuthState(async (state) => {
|
||||
configureProfiles();
|
||||
|
|
|
|||
|
|
@ -61,6 +61,7 @@ vi.mock("./usage.js", () => ({
|
|||
vi.mock("../../plugins/provider-model-routes.js", () => ({
|
||||
// Synthetic route IDs in this fixture are already canonical.
|
||||
createProviderModelCatalogIdNormalizer: () => (modelId: string) => modelId,
|
||||
resolveProviderModelCatalogId: ({ modelId }: { modelId: string }) => modelId,
|
||||
resolveProviderModelPolicySurface: () => null,
|
||||
resolveProviderModelRoutes: authStoreMocks.resolveProviderModelRoutes,
|
||||
}));
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import { shouldPreserveUnavailableSessionAuthProfileOverride } from "../../sessi
|
|||
import { createLazyImportLoader } from "../../shared/lazy-promise.js";
|
||||
import { isUserModelAuthProfileId } from "../../state/user-model-account-id.js";
|
||||
import { resolveUserProfileAuthLink } from "../../state/user-model-accounts.js";
|
||||
import { resolveAgentEffectiveModelPrimary } from "../agent-scope.js";
|
||||
import {
|
||||
isConfiguredAwsSdkAuthProfileForProvider,
|
||||
isStoredCredentialCompatibleWithAuthProvider,
|
||||
|
|
@ -21,6 +22,8 @@ import {
|
|||
import { isProfileInCooldown } from "../auth-profiles/usage.js";
|
||||
import { resolveModelProviderAuthConfig } from "../model-auth-provider-route.js";
|
||||
import { splitTrailingAuthProfile } from "../model-ref-profile.js";
|
||||
import { resolveDefaultModelForAgent } from "../model-selection.js";
|
||||
import { resolveModelCatalogIdentityKey } from "../openai-model-routes.js";
|
||||
import { listOpenAIAuthProfileProvidersForAgentRuntime } from "../openai-routing.js";
|
||||
import { resolveProviderModelRouteAuthRequirement } from "../provider-model-route-auth.js";
|
||||
import { ensureAuthProfileStore } from "./store-runtime.js";
|
||||
|
|
@ -547,6 +550,7 @@ export async function resolveSessionAuthSelection(params: {
|
|||
cfg: OpenClawConfig;
|
||||
provider: string;
|
||||
modelId: string;
|
||||
agentId?: string;
|
||||
configuredProfileId?: string;
|
||||
harnessRuntime?: string;
|
||||
agentDir: string;
|
||||
|
|
@ -582,7 +586,20 @@ export async function resolveSessionAuthSelection(params: {
|
|||
// Person-linked pins carry user strength and outrank the agent's static @profile.
|
||||
const rotatedPinnedProfileId =
|
||||
rotatedSource === "user" || rotatedSource === "user-link" ? rotatedProfileId : undefined;
|
||||
const configuredProfileId = params.configuredProfileId?.trim() || undefined;
|
||||
const configuredProfile = params.agentId
|
||||
? splitTrailingAuthProfile(resolveAgentEffectiveModelPrimary(params.cfg, params.agentId) ?? "")
|
||||
.profile
|
||||
: undefined;
|
||||
const defaultModel = configuredProfile
|
||||
? resolveDefaultModelForAgent({ cfg: params.cfg, agentId: params.agentId })
|
||||
: undefined;
|
||||
const configuredProfileId =
|
||||
params.configuredProfileId?.trim() ||
|
||||
(defaultModel &&
|
||||
resolveModelCatalogIdentityKey({ provider: params.provider, id: modelId }) ===
|
||||
resolveModelCatalogIdentityKey({ provider: defaultModel.provider, id: defaultModel.model })
|
||||
? configuredProfile
|
||||
: undefined);
|
||||
const profileId = rotatedPinnedProfileId ?? configuredProfileId ?? rotatedProfileId;
|
||||
if (!profileId) {
|
||||
return undefined;
|
||||
|
|
|
|||
|
|
@ -1026,6 +1026,9 @@ describe("runBtwSideQuestion", () => {
|
|||
agentId: "work",
|
||||
allowGatewaySubagentBinding: true,
|
||||
});
|
||||
expect(resolveSessionAuthSelectionMock).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ agentId: "work" }),
|
||||
);
|
||||
if (sideQuestion) {
|
||||
expect(sideQuestion).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ agentId: "work", sessionKey: "global" }),
|
||||
|
|
|
|||
|
|
@ -529,6 +529,7 @@ async function resolveRuntimeModel(params: {
|
|||
cfg,
|
||||
provider: runtimeProvider,
|
||||
modelId: runtimeModelId,
|
||||
agentId: params.agentId,
|
||||
harnessRuntime: params.harnessId,
|
||||
agentDir,
|
||||
sessionEntry: params.sessionEntry,
|
||||
|
|
|
|||
|
|
@ -441,6 +441,7 @@ export async function prepareReplyRunAdmission(context: PreparedReplyRunContext)
|
|||
cfg,
|
||||
provider,
|
||||
modelId: model,
|
||||
agentId,
|
||||
...(agentHarnessPolicy ? { harnessRuntime: agentHarnessPolicy.runtime } : {}),
|
||||
agentDir,
|
||||
sessionEntry: authSessionEntry,
|
||||
|
|
|
|||
|
|
@ -3144,6 +3144,9 @@ describe("runPreparedReply media-only handling", () => {
|
|||
const call = requireLastRunReplyAgentCall();
|
||||
expect(call?.followupRun.run.authProfileId).toBe("profile-after-wait");
|
||||
expect(vi.mocked(resolveSessionAuthSelection)).toHaveBeenCalledTimes(1);
|
||||
expect(resolveSessionAuthSelection).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ agentId: "default" }),
|
||||
);
|
||||
});
|
||||
|
||||
it("re-resolves same-session ownership after session-id rotation during async prep", async () => {
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ const mocks = vi.hoisted(() => ({
|
|||
modelsAuthListCommand: vi.fn().mockResolvedValue(undefined),
|
||||
modelsAuthLoginCommand: vi.fn().mockResolvedValue(undefined),
|
||||
modelsAuthLogoutCommand: vi.fn().mockResolvedValue(undefined),
|
||||
modelsAuthActivateCommand: vi.fn().mockResolvedValue(undefined),
|
||||
modelsAuthOrderClearCommand: vi.fn().mockResolvedValue(undefined),
|
||||
modelsAuthOrderGetCommand: vi.fn().mockResolvedValue(undefined),
|
||||
modelsAuthOrderSetCommand: vi.fn().mockResolvedValue(undefined),
|
||||
|
|
@ -77,6 +78,9 @@ vi.mock("../commands/models/accounts.js", () => ({
|
|||
modelsAccountsUseCommand: mocks.modelsAccountsUseCommand,
|
||||
modelsAccountsClearDefaultCommand: mocks.modelsAccountsClearDefaultCommand,
|
||||
}));
|
||||
vi.mock("../commands/models/auth-activate.js", () => ({
|
||||
modelsAuthActivateCommand: mocks.modelsAuthActivateCommand,
|
||||
}));
|
||||
vi.mock("../commands/models/auth-logout.js", () => ({
|
||||
modelsAuthLogoutCommand: mocks.modelsAuthLogoutCommand,
|
||||
}));
|
||||
|
|
@ -125,6 +129,7 @@ describe("models cli", () => {
|
|||
modelsAuthListCommand.mockClear();
|
||||
modelsAuthLoginCommand.mockClear();
|
||||
modelsAuthLogoutCommand.mockClear();
|
||||
mocks.modelsAuthActivateCommand.mockClear();
|
||||
modelsAuthOrderClearCommand.mockClear();
|
||||
modelsAuthOrderGetCommand.mockClear();
|
||||
modelsAuthOrderSetCommand.mockClear();
|
||||
|
|
@ -383,6 +388,12 @@ describe("models cli", () => {
|
|||
command: modelsAuthAddCommand,
|
||||
expected: { agent: "poe" },
|
||||
},
|
||||
{
|
||||
label: "activate",
|
||||
args: ["models", "auth", "--agent", "poe", "activate", "openai:saved"],
|
||||
command: mocks.modelsAuthActivateCommand,
|
||||
expected: { agent: "poe", profileId: "openai:saved" },
|
||||
},
|
||||
{
|
||||
label: "list",
|
||||
args: ["models", "auth", "--agent", "poe", "list", "--provider", "openai"],
|
||||
|
|
@ -438,6 +449,12 @@ describe("models cli", () => {
|
|||
command: modelsAuthAddCommand,
|
||||
expected: { agent: "poe" },
|
||||
},
|
||||
{
|
||||
label: "activate",
|
||||
args: ["models", "auth", "activate", "openai:saved", "--agent", "poe"],
|
||||
command: mocks.modelsAuthActivateCommand,
|
||||
expected: { agent: "poe", profileId: "openai:saved" },
|
||||
},
|
||||
{
|
||||
label: "list",
|
||||
args: ["models", "auth", "list", "--provider", "openai", "--agent", "poe"],
|
||||
|
|
|
|||
|
|
@ -361,6 +361,19 @@ export function registerModelsCli(program: Command) {
|
|||
});
|
||||
});
|
||||
|
||||
auth
|
||||
.command("activate")
|
||||
.description("Test a saved sign-in and use it for this agent")
|
||||
.argument("<profileId>", "Saved sign-in id from models auth list")
|
||||
.option("--agent <id>", "Agent id (default: the only configured agent)")
|
||||
.action(async (profileId: string, opts, command) => {
|
||||
await withModelsRuntime(async ({ defaultRuntime, resolveModelAgentOption }) => {
|
||||
const agent = resolveModelAgentOption(command, opts);
|
||||
const { modelsAuthActivateCommand } = await import("../commands/models/auth-activate.js");
|
||||
await modelsAuthActivateCommand({ profileId, agent }, defaultRuntime);
|
||||
});
|
||||
});
|
||||
|
||||
auth
|
||||
.command("logout")
|
||||
.description("Remove a saved auth profile (see `models auth list` for ids)")
|
||||
|
|
|
|||
|
|
@ -175,6 +175,7 @@ const JSON_NOT_APPLICABLE = {
|
|||
"models image-fallbacks add",
|
||||
"models image-fallbacks remove",
|
||||
"models image-fallbacks clear",
|
||||
"models auth activate",
|
||||
"models auth logout",
|
||||
"models auth order set",
|
||||
"models auth order clear",
|
||||
|
|
|
|||
55
src/commands/models/auth-activate.test.ts
Normal file
55
src/commands/models/auth-activate.test.ts
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
import { afterEach, expect, it, vi } from "vitest";
|
||||
import { createOpenClawTestState } from "../../test-utils/openclaw-test-state.js";
|
||||
import { modelsAuthActivateCommand } from "./auth-activate.js";
|
||||
|
||||
const mocks = vi.hoisted(() => ({ callGateway: vi.fn() }));
|
||||
vi.mock("../../gateway/call.js", () => ({ callGateway: mocks.callGateway }));
|
||||
vi.mock("../../system-agent/setup-inference.js", () => ({
|
||||
activateSetupInference: async () => ({
|
||||
ok: true,
|
||||
modelRef: "sample/chat",
|
||||
latencyMs: 1,
|
||||
lines: ["Connection verified"],
|
||||
}),
|
||||
}));
|
||||
vi.mock("./auth-refresh.js", () => ({
|
||||
refreshRunningGatewayAuthState: async () => "refreshed",
|
||||
}));
|
||||
afterEach(() => vi.clearAllMocks());
|
||||
|
||||
it.each([
|
||||
{ name: "applied account", appliedHash: "saved", profile: "replacement", activated: true },
|
||||
{ name: "reload disabled", appliedHash: "old", profile: "replacement", activated: false },
|
||||
{ name: "another account selected", appliedHash: "saved", profile: "other", activated: false },
|
||||
{ name: "lost acknowledgement", appliedHash: null, profile: "replacement", activated: false },
|
||||
])(
|
||||
"reports the saved sign-in accurately with $name",
|
||||
async ({ appliedHash, profile, activated }) => {
|
||||
const state = await createOpenClawTestState({ label: "activate-saved-sign-in" });
|
||||
try {
|
||||
await state.writeConfig({ agents: { entries: { main: { workspace: state.workspaceDir } } } });
|
||||
const runtime = { log: vi.fn(), error: vi.fn(), exit: vi.fn() };
|
||||
if (appliedHash === null) {
|
||||
mocks.callGateway.mockRejectedValue(new Error("connection closed"));
|
||||
} else {
|
||||
mocks.callGateway.mockResolvedValue({
|
||||
config: { agents: { entries: { main: { model: `sample/chat@${profile}` } } } },
|
||||
configRevisionHash: "saved",
|
||||
appliedConfigHash: appliedHash,
|
||||
});
|
||||
}
|
||||
await modelsAuthActivateCommand({ profileId: "replacement", agent: "main" }, runtime);
|
||||
const output = runtime.log.mock.calls.flat().join("\n");
|
||||
expect(output).toContain("Connection verified");
|
||||
if (activated) {
|
||||
expect(output).toContain("Saved sign-in activated");
|
||||
} else {
|
||||
expect(output).not.toContain("Saved sign-in activated");
|
||||
expect(output).toContain("verified and saved");
|
||||
expect(output).toContain("openclaw gateway restart");
|
||||
}
|
||||
} finally {
|
||||
await state.cleanup();
|
||||
}
|
||||
},
|
||||
);
|
||||
57
src/commands/models/auth-activate.ts
Normal file
57
src/commands/models/auth-activate.ts
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
import { resolveAgentEffectiveModelPrimary } from "../../agents/agent-scope.js";
|
||||
import { splitTrailingAuthProfile } from "../../agents/model-ref-profile.js";
|
||||
import type { OpenClawConfig } from "../../config/types.openclaw.js";
|
||||
import { callGateway } from "../../gateway/call.js";
|
||||
import type { RuntimeEnv } from "../../runtime.js";
|
||||
import { toSavedAuthSetupKind } from "../../system-agent/setup-inference-core.js";
|
||||
import { activateSetupInference } from "../../system-agent/setup-inference.js";
|
||||
import { refreshRunningGatewayAuthState } from "./auth-refresh.js";
|
||||
import { loadValidConfigSnapshotOrThrow, resolveModelsTargetAgent } from "./shared.js";
|
||||
|
||||
export async function modelsAuthActivateCommand(
|
||||
opts: { profileId: string; agent?: string },
|
||||
runtime: RuntimeEnv,
|
||||
): Promise<void> {
|
||||
const { runtimeConfig } = await loadValidConfigSnapshotOrThrow();
|
||||
const { agentId } = resolveModelsTargetAgent(runtimeConfig, opts.agent, { kind: "mutation" });
|
||||
const result = await activateSetupInference({
|
||||
kind: toSavedAuthSetupKind(opts.profileId.trim()),
|
||||
agentId,
|
||||
surface: "cli",
|
||||
activationConfirmed: true,
|
||||
runtime,
|
||||
});
|
||||
if (!result.ok) {
|
||||
throw new Error(result.error);
|
||||
}
|
||||
const refreshed = await refreshRunningGatewayAuthState(agentId, "update", runtime);
|
||||
for (const line of result.lines) {
|
||||
runtime.log(line);
|
||||
}
|
||||
let applied = false;
|
||||
if (refreshed === "refreshed") {
|
||||
try {
|
||||
const current = await callGateway<{
|
||||
config: OpenClawConfig;
|
||||
configRevisionHash: string;
|
||||
appliedConfigHash: string | null;
|
||||
}>({
|
||||
method: "config.get",
|
||||
params: {},
|
||||
timeoutMs: 3000,
|
||||
requireLocalBackendSharedAuth: true,
|
||||
});
|
||||
applied =
|
||||
current.configRevisionHash === current.appliedConfigHash &&
|
||||
splitTrailingAuthProfile(resolveAgentEffectiveModelPrimary(current.config, agentId) ?? "")
|
||||
.profile === opts.profileId.trim();
|
||||
} catch {
|
||||
// A lost acknowledgement cannot turn the completed save into a failed save.
|
||||
}
|
||||
}
|
||||
runtime.log(
|
||||
applied
|
||||
? `Saved sign-in activated for ${agentId}: ${opts.profileId}`
|
||||
: "Sign-in verified and saved. The running connection could not be confirmed. Run `openclaw gateway restart` to apply the saved settings.",
|
||||
);
|
||||
}
|
||||
|
|
@ -203,8 +203,13 @@ describe("provider model access consent", () => {
|
|||
if (status === "applied") {
|
||||
await expect(result).resolves.toBe("All Sample models are now visible.");
|
||||
} else {
|
||||
await expect(result).rejects.toThrow("did not apply");
|
||||
await expect(result).resolves.toContain("Model access saved.");
|
||||
expect(await result).toContain("openclaw gateway restart");
|
||||
}
|
||||
expect((await readSaved()).agents?.defaults?.modelPolicy?.allow).toEqual([
|
||||
"other/current",
|
||||
"sample/*",
|
||||
]);
|
||||
} finally {
|
||||
stop();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -162,11 +162,9 @@ export async function completeProviderModelAccess(params: {
|
|||
},
|
||||
attachRuntimeConfigWriteApplication({}, application),
|
||||
);
|
||||
if (application.claimed && (await application.result) !== "applied") {
|
||||
throw new Error("The running Gateway did not apply the saved model restrictions.");
|
||||
}
|
||||
const applied = application.claimed && (await application.result) === "applied";
|
||||
logConfigUpdated(params.runtime);
|
||||
const message = application.claimed
|
||||
const message = applied
|
||||
? `All ${prepared.providerLabel} models are now visible.`
|
||||
: "Model access saved. Application by the running Gateway is not confirmed. Run `openclaw gateway restart` to apply it.";
|
||||
params.runtime.log(`Credentials saved. ${message}`);
|
||||
|
|
|
|||
|
|
@ -9,6 +9,8 @@ import os from "node:os";
|
|||
import path from "node:path";
|
||||
import { isDeepStrictEqual } from "node:util";
|
||||
import type { ApiKeyCredential } from "../../../agents/auth-profiles/types.js";
|
||||
import { formatCliCommand } from "../../../cli/command-format.js";
|
||||
import { quoteCliArg } from "../../../cli/quote-cli-arg.js";
|
||||
import { resolveAgentModelPrimaryValue } from "../../../config/model-input.js";
|
||||
import type { OpenClawConfig } from "../../../config/types.openclaw.js";
|
||||
import { enablePluginWithCapabilityConsent } from "../../../plugins/enable.js";
|
||||
|
|
@ -313,6 +315,7 @@ export async function applyNonInteractivePluginProviderChoice(params: {
|
|||
};
|
||||
const stagingRoot = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-setup-credential-"));
|
||||
const stagingAgentDir = path.join(stagingRoot, "agents", "setup", "agent");
|
||||
let savedProfileId: string | undefined;
|
||||
try {
|
||||
await fs.mkdir(stagingAgentDir, { recursive: true });
|
||||
result = await withAuthProfileStoreAgentDir(stagingAgentDir, stagingRoot, async () => {
|
||||
|
|
@ -369,7 +372,7 @@ export async function applyNonInteractivePluginProviderChoice(params: {
|
|||
"Provider setup did not save a replacement credential. Your connection is unchanged.",
|
||||
);
|
||||
}
|
||||
await saveSetupCredential({
|
||||
const saved = await saveSetupCredential({
|
||||
profile,
|
||||
config: projectProviderResult(prepared.config),
|
||||
baseConfig: params.baseConfig,
|
||||
|
|
@ -378,7 +381,7 @@ export async function applyNonInteractivePluginProviderChoice(params: {
|
|||
authChoice: trustedManifestMatch?.choiceId ?? providerChoice.wizard?.choiceId,
|
||||
pluginId: providerChoice.provider.pluginId,
|
||||
});
|
||||
result = null;
|
||||
savedProfileId = saved.profile.profileId;
|
||||
}
|
||||
} finally {
|
||||
clearRuntimeAuthProfileStoreSnapshot(stagingAgentDir);
|
||||
|
|
@ -386,9 +389,9 @@ export async function applyNonInteractivePluginProviderChoice(params: {
|
|||
closeOpenClawAgentDatabases(stagingRoot);
|
||||
await fs.rm(stagingRoot, { recursive: true, force: true });
|
||||
}
|
||||
if (!result) {
|
||||
if (savedProfileId) {
|
||||
return reject(
|
||||
"Replacement credential saved but inactive. Your connection is unchanged. Open Model Setup to test and activate the saved sign-in.",
|
||||
`Replacement credential saved but inactive. Your connection is unchanged. Test and activate it with:\n${formatCliCommand(`openclaw models auth activate ${quoteCliArg(savedProfileId)} --agent ${quoteCliArg(params.target.agentId)}`)}`,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -117,6 +117,9 @@ it.each([true, false])(
|
|||
}),
|
||||
}),
|
||||
]);
|
||||
expect(runtime.error.mock.calls.flat().join("\n")).toContain("saved but inactive");
|
||||
const savedProfileId = Object.keys(store.profiles).find((id) => id !== "openai:default");
|
||||
expect(runtime.error.mock.calls.flat().join("\n")).toContain(
|
||||
`openclaw models auth activate ${savedProfileId} --agent main`,
|
||||
);
|
||||
},
|
||||
);
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@
|
|||
*/
|
||||
import type { ApiKeyCredential } from "../../../agents/auth-profiles/types.js";
|
||||
import { formatCliCommand } from "../../../cli/command-format.js";
|
||||
import { quoteCliArg } from "../../../cli/quote-cli-arg.js";
|
||||
import type { OpenClawConfig } from "../../../config/types.openclaw.js";
|
||||
import type { SecretInput } from "../../../config/types.secrets.js";
|
||||
import { formatErrorMessage } from "../../../infra/errors.js";
|
||||
|
|
@ -278,7 +279,7 @@ export async function applyNonInteractiveAuthChoice(params: {
|
|||
const { prepareCustomSetupCredentials } =
|
||||
await import("../../../system-agent/setup-inference-custom.js");
|
||||
const prepared = prepareCustomSetupCredentials(result);
|
||||
await saveSetupCredential({
|
||||
const saved = await saveSetupCredential({
|
||||
profile: prepared.profiles[0]!,
|
||||
config: prepared.config,
|
||||
baseConfig,
|
||||
|
|
@ -288,7 +289,7 @@ export async function applyNonInteractiveAuthChoice(params: {
|
|||
rejectOnboardingOption(
|
||||
opts,
|
||||
runtime,
|
||||
"Replacement credential saved but inactive. Your connection is unchanged. Open Model Setup to test and activate the saved sign-in.",
|
||||
`Replacement credential saved but inactive. Your connection is unchanged. Test and activate it with:\n${formatCliCommand(`openclaw models auth activate ${quoteCliArg(saved.profile.profileId)} --agent ${quoteCliArg(params.target.agentId)}`)}`,
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,11 +7,7 @@ import type {
|
|||
WorkerInferenceStartParams,
|
||||
WorkerInferenceTerminalOutcome,
|
||||
} from "../../../packages/gateway-protocol/src/schema/worker-inference.js";
|
||||
import {
|
||||
resolveAgentDir,
|
||||
resolveAgentEffectiveModelPrimary,
|
||||
resolveAgentWorkspaceDir,
|
||||
} from "../../agents/agent-scope.js";
|
||||
import { resolveAgentDir, resolveAgentWorkspaceDir } from "../../agents/agent-scope.js";
|
||||
import { resolveSessionAuthSelection } from "../../agents/auth-profiles/session-override.js";
|
||||
import { applyExtraParamsToAgent } from "../../agents/embedded-agent-runner/extra-params.js";
|
||||
import { resolveModelAsync } from "../../agents/embedded-agent-runner/model.js";
|
||||
|
|
@ -417,13 +413,6 @@ async function resolveApprovedModel(params: {
|
|||
runtimeLease.release();
|
||||
return undefined;
|
||||
}
|
||||
const configuredDefaultProfile =
|
||||
resolvedKey ===
|
||||
resolveModelCatalogIdentityKey({ provider: defaultModel.provider, id: defaultModel.model })
|
||||
? splitTrailingAuthProfile(
|
||||
resolveAgentEffectiveModelPrimary(lifecycleConfig, target.agentId) ?? "",
|
||||
).profile
|
||||
: undefined;
|
||||
const harnessPolicy = resolveAgentHarnessPolicy({
|
||||
provider: resolved.ref.provider,
|
||||
modelId: resolved.ref.model,
|
||||
|
|
@ -440,7 +429,7 @@ async function resolveApprovedModel(params: {
|
|||
cfg: lifecycleConfig,
|
||||
provider: resolved.ref.provider,
|
||||
modelId: resolved.ref.model,
|
||||
...(configuredDefaultProfile ? { configuredProfileId: configuredDefaultProfile } : {}),
|
||||
agentId: target.agentId,
|
||||
harnessRuntime: harnessPolicy.runtime,
|
||||
agentDir,
|
||||
sessionEntry: target.sessionEntry,
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import assert from "node:assert/strict";
|
||||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
|
|
@ -14,6 +15,7 @@ import { resolveApiKeyForProviderCore } from "../agents/model-auth.js";
|
|||
import { clearConfigCache, readConfigFileSnapshot } from "../config/config.js";
|
||||
import { resolveAgentModelPrimaryValue } from "../config/model-input.js";
|
||||
import type { OpenClawConfig } from "../config/types.openclaw.js";
|
||||
import { validateConfigObjectRaw } from "../config/validation-core.js";
|
||||
import type { ProviderAuthChoiceMetadata } from "../plugins/provider-auth-choices.js";
|
||||
import { persistProviderAuthProfilesAfterLogin } from "../plugins/provider-auth-persistence.js";
|
||||
import { createEmptyPluginRegistry } from "../plugins/registry-empty.js";
|
||||
|
|
@ -24,6 +26,7 @@ import { closeOpenClawStateDatabaseForTest } from "../state/openclaw-state-db.js
|
|||
import { resolveSystemAgentConfiguredRouteFromConfig } from "./inference-route.js";
|
||||
import { activateSetupInference } from "./setup-inference-activate.js";
|
||||
import type { ActivateSetupInferenceDeps } from "./setup-inference-core.js";
|
||||
import { saveSetupCredential } from "./setup-inference-credentials.js";
|
||||
import { detectSetupInference } from "./setup-inference-detect.js";
|
||||
import { createSystemAgentPluginMetadataTestSnapshot } from "./system-agent.test-helpers.js";
|
||||
|
||||
|
|
@ -197,7 +200,10 @@ async function fixture(
|
|||
};
|
||||
};
|
||||
}
|
||||
const activate = (kind: Parameters<typeof activateSetupInference>[0]["kind"] = "provider-auth") =>
|
||||
const activate = (
|
||||
kind: Parameters<typeof activateSetupInference>[0]["kind"] = "provider-auth",
|
||||
activationConfirmed?: true,
|
||||
) =>
|
||||
metadata.run(() =>
|
||||
activateSetupInference({
|
||||
kind,
|
||||
|
|
@ -206,7 +212,8 @@ async function fixture(
|
|||
nativeSessionCatalogsEnabled: false,
|
||||
surface: "cli",
|
||||
runtime: { log: vi.fn(), error: vi.fn(), exit: vi.fn() },
|
||||
prompter,
|
||||
prompter: activationConfirmed ? undefined : prompter,
|
||||
activationConfirmed,
|
||||
deps,
|
||||
}),
|
||||
);
|
||||
|
|
@ -348,12 +355,13 @@ describe("setup activation credentials and configuration", () => {
|
|||
});
|
||||
|
||||
it.each([
|
||||
{ explicitProfile: true, restartRequired: false },
|
||||
{ explicitProfile: false, restartRequired: false },
|
||||
{ explicitProfile: true, restartRequired: true },
|
||||
{ explicitProfile: true, restartRequired: false, activationConfirmed: undefined },
|
||||
{ explicitProfile: false, restartRequired: false, activationConfirmed: undefined },
|
||||
{ explicitProfile: true, restartRequired: true, activationConfirmed: undefined },
|
||||
{ explicitProfile: true, restartRequired: false, activationConfirmed: true as const },
|
||||
])(
|
||||
"keeps a working credential and rotation when a replacement is rejected (configured: $explicitProfile, restart: $restartRequired)",
|
||||
async ({ explicitProfile, restartRequired }) => {
|
||||
"keeps a working credential and rotation when a replacement is rejected (configured: $explicitProfile, restart: $restartRequired, confirmed: $activationConfirmed)",
|
||||
async ({ explicitProfile, restartRequired, activationConfirmed }) => {
|
||||
const setup = await fixture({ restartRequired });
|
||||
const originalProfileId = "openai:fixture";
|
||||
const originalCredential = { ...credential, key: "working-original-key" };
|
||||
|
|
@ -449,7 +457,7 @@ describe("setup activation credentials and configuration", () => {
|
|||
expect(declined, await setup.diagnostics(declined)).toMatchObject({ ok: false });
|
||||
expect(setup.prompter.confirm).toHaveBeenCalledWith({
|
||||
message: "Connection verified. Activate this saved sign-in?",
|
||||
initialValue: false,
|
||||
initialValue: true,
|
||||
});
|
||||
expect(await fs.readFile(setup.configPath, "utf8")).toBe(before);
|
||||
const inactive = loadAuthProfileStoreWithoutExternalProfiles(setup.agentDir);
|
||||
|
|
@ -457,8 +465,10 @@ describe("setup activation credentials and configuration", () => {
|
|||
expect(inactive.lastGood).toEqual(store.lastGood);
|
||||
expect(inactive.usageStats).toEqual(store.usageStats);
|
||||
|
||||
vi.mocked(setup.prompter.confirm).mockResolvedValue(true);
|
||||
const accepted = await setup.activate(retryKind);
|
||||
vi.mocked(setup.prompter.confirm).mockImplementation(
|
||||
async ({ initialValue }) => initialValue ?? false,
|
||||
);
|
||||
const accepted = await setup.activate(retryKind, activationConfirmed);
|
||||
expect(accepted, await setup.diagnostics(accepted)).toMatchObject({ ok: true });
|
||||
expect(setup.readProfile()).toEqual([savedId, credential]);
|
||||
expect(setup.login).toHaveBeenCalledOnce();
|
||||
|
|
@ -469,6 +479,60 @@ describe("setup activation credentials and configuration", () => {
|
|||
},
|
||||
);
|
||||
|
||||
it("activates saved sparse model settings without treating runtime defaults as a changed connection", async () => {
|
||||
const setup = await fixture();
|
||||
const configured: OpenClawConfig = {
|
||||
...setup.config,
|
||||
agents: {
|
||||
...setup.config.agents,
|
||||
defaults: { ...setup.config.agents?.defaults, model: `${modelRef}@openai:original` },
|
||||
},
|
||||
};
|
||||
await persistProviderAuthProfilesAfterLogin({
|
||||
config: configured,
|
||||
agentDir: setup.agentDir,
|
||||
profiles: [
|
||||
{ profileId: "openai:original", credential: { ...credential, key: "original-key" } },
|
||||
],
|
||||
});
|
||||
await fs.writeFile(setup.configPath, JSON.stringify(configured));
|
||||
clearConfigCache();
|
||||
const sparse = validateConfigObjectRaw({
|
||||
...configured,
|
||||
models: {
|
||||
providers: {
|
||||
openai: {
|
||||
baseUrl: "https://provider.example/v1",
|
||||
api: "openai-responses",
|
||||
models: [{ id: "gpt-4.1-mini", name: "Sparse saved model" }],
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
assert.ok(sparse.ok);
|
||||
const saved = await saveSetupCredential({
|
||||
profile: { profileId: "openai:replacement", credential },
|
||||
config: sparse.config,
|
||||
baseConfig: configured,
|
||||
agentDir: setup.agentDir,
|
||||
modelRef,
|
||||
authChoice: "fixture-login",
|
||||
pluginId: "openai",
|
||||
});
|
||||
|
||||
const result = await setup.activate(
|
||||
`saved-auth:${encodeURIComponent(saved.profile.profileId)}`,
|
||||
true,
|
||||
);
|
||||
|
||||
expect(result, await setup.diagnostics(result)).toMatchObject({ ok: true });
|
||||
expect(setup.readProfile()).toEqual([saved.profile.profileId, credential]);
|
||||
const snapshot = await readConfigFileSnapshot();
|
||||
expect(snapshot.sourceConfig.models?.providers?.openai?.models).toEqual([
|
||||
{ id: "gpt-4.1-mini", name: "Sparse saved model" },
|
||||
]);
|
||||
});
|
||||
|
||||
it("preserves an unrelated config edit when selecting the verified model", async () => {
|
||||
const setup = await fixture();
|
||||
const changed = `${JSON.stringify({ ...setup.config, messages: { ackReaction: "seen" } })}\n`;
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ import {
|
|||
GEMINI_CLI_DEFAULT_MODEL_REF,
|
||||
OPENAI_API_DEFAULT_MODEL_REF,
|
||||
} from "../commands/onboard-inference.js";
|
||||
import { materializeRuntimeConfig } from "../config/materialize.js";
|
||||
import { applyMergePatch, createMergePatch } from "../config/merge-patch.js";
|
||||
import { normalizeAgentModelRefForConfig } from "../config/model-input.js";
|
||||
import {
|
||||
|
|
@ -471,8 +472,14 @@ async function verifyAndActivateCandidate(
|
|||
loadAuthProfileStoreForRuntime: deps.loadAuthProfileStoreForRuntime,
|
||||
};
|
||||
const requestedAgentId = params.agentId ? routeAgentId : undefined;
|
||||
// Saved model rows stay sparse; compare the same runtime defaults before and after writing.
|
||||
const project = (config: OpenClawConfig, sourceConfig: OpenClawConfig) =>
|
||||
projectInferenceRoute(config, requestedAgentId, routeDeps, sourceConfig);
|
||||
projectInferenceRoute(
|
||||
materializeRuntimeConfig(config, { manifestRegistry: { plugins: [...metadata.plugins] } }),
|
||||
requestedAgentId,
|
||||
routeDeps,
|
||||
sourceConfig,
|
||||
);
|
||||
const resolveRoute = (config: OpenClawConfig, currentSnapshot = snapshot) =>
|
||||
resolveSystemAgentConfiguredRouteFromConfig(
|
||||
config,
|
||||
|
|
@ -532,12 +539,12 @@ async function verifyAndActivateCandidate(
|
|||
const savedCredential = staged.authProfileId
|
||||
? loadAuthProfileStoreWithoutExternalProfiles(ctx.agentDir).profiles[staged.authProfileId]
|
||||
: undefined;
|
||||
if (savedCredential?.setup?.replacement) {
|
||||
if (savedCredential?.setup?.replacement && !params.activationConfirmed) {
|
||||
if (
|
||||
!params.prompter ||
|
||||
!(await params.prompter.confirm({
|
||||
message: "Connection verified. Activate this saved sign-in?",
|
||||
initialValue: false,
|
||||
initialValue: true,
|
||||
}))
|
||||
) {
|
||||
return failure({
|
||||
|
|
|
|||
|
|
@ -203,6 +203,8 @@ export type ActivateSetupInferenceParams = {
|
|||
/** False when an enclosing persistent-operation boundary owns the setup audit. */
|
||||
recordSetupAudit?: boolean;
|
||||
runtime: RuntimeEnv;
|
||||
/** Explicit consent from the CLI command that tests and activates a saved sign-in. */
|
||||
activationConfirmed?: true;
|
||||
/** Interactive provider login transport, required for `provider-auth`. */
|
||||
prompter?: WizardPrompter;
|
||||
/** Cancels provider-owned browser callbacks and device-code polling. */
|
||||
|
|
|
|||
|
|
@ -278,7 +278,7 @@ export async function offerLiveModelVerification(params: {
|
|||
(params.opts.nonInteractive ||
|
||||
!(await params.prompter.confirm({
|
||||
message: "Connection verified. Activate this saved sign-in?",
|
||||
initialValue: false,
|
||||
initialValue: true,
|
||||
})))
|
||||
) {
|
||||
await params.prompter.note(
|
||||
|
|
|
|||
|
|
@ -77,7 +77,7 @@ suite.define(() => {
|
|||
const card = page.locator('[data-provider-id="fixture"]');
|
||||
await card.waitFor();
|
||||
await page.screenshot({ path: path.join(suite.artifactDir, "before-edit.png") });
|
||||
await card.getByRole("button", { name: "Replace key" }).click();
|
||||
await card.getByRole("button", { name: "Set API key" }).click();
|
||||
await card.getByLabel("API key").fill(key);
|
||||
await card.getByRole("button", { name: "Save", exact: true }).click();
|
||||
await expect.poll(() => card.textContent()).toContain("Secret saved.");
|
||||
|
|
|
|||
|
|
@ -275,7 +275,7 @@ suite.define(() => {
|
|||
await page.goto(`${suite.server.baseUrl}settings/model-providers`);
|
||||
const openaiCard = page.locator('[data-provider-id="openai"]');
|
||||
await expect.poll(async () => openaiCard.textContent()).toContain("Credentials for Main");
|
||||
await openaiCard.getByRole("button", { name: "Replace key" }).click();
|
||||
await openaiCard.getByRole("button", { name: "Set API key" }).click();
|
||||
if (recordVisuals) {
|
||||
await mkdir(path.join(suite.artifactDir, "model-providers"), { recursive: true });
|
||||
await page.screenshot({
|
||||
|
|
|
|||
|
|
@ -374,7 +374,7 @@ describeControlUiE2e("Control UI Models mocked Gateway E2E", () => {
|
|||
|
||||
const openrouterCard = page.locator(".model-providers__row", { hasText: "OpenRouter" });
|
||||
await openrouterCard.waitFor();
|
||||
await openrouterCard.getByRole("button", { name: "Replace key", exact: true }).waitFor();
|
||||
await openrouterCard.getByRole("button", { name: "Set API key", exact: true }).waitFor();
|
||||
expect(await openrouterCard.locator(".model-providers__profile").textContent()).toContain(
|
||||
"openrouter:default",
|
||||
);
|
||||
|
|
@ -657,7 +657,7 @@ describeControlUiE2e("Control UI Models mocked Gateway E2E", () => {
|
|||
await captureProviderProof("01-configured.png", openaiCard);
|
||||
}
|
||||
|
||||
await openaiCard.getByRole("button", { name: "Replace key" }).click();
|
||||
await openaiCard.getByRole("button", { name: "Set API key" }).click();
|
||||
await openaiCard.getByLabel("API key").fill(openaiInputValue);
|
||||
const keyWriteCount = (await gateway.getRequests("models.authSetApiKey")).length;
|
||||
await gateway.deferNext("models.authSetApiKey");
|
||||
|
|
|
|||
|
|
@ -354,7 +354,6 @@ const enSettings = {
|
|||
placeholder: "Enter provider API key",
|
||||
replacePlaceholder: "Secret saved. Enter a new key to replace it.",
|
||||
set: "Set API key",
|
||||
replace: "Replace key",
|
||||
remove: "Remove key",
|
||||
saved: "Secret saved.",
|
||||
removed: "Saved API keys removed.",
|
||||
|
|
|
|||
|
|
@ -480,7 +480,7 @@ describe("renderModelProviders", () => {
|
|||
expect(
|
||||
provider?.querySelector<HTMLInputElement>(".model-providers__inline-form input")?.disabled,
|
||||
).toBe(true);
|
||||
expect(button(provider!, "Replace key")?.disabled).toBe(true);
|
||||
expect(button(provider!, "Set API key")?.disabled).toBe(true);
|
||||
expect(button(provider!, "Remove key")?.disabled).toBe(true);
|
||||
expect(
|
||||
provider?.querySelector<HTMLButtonElement>(".model-providers__profile-logout")?.disabled,
|
||||
|
|
|
|||
|
|
@ -338,12 +338,7 @@ function renderProviderActions(card: ModelProviderCard, props: ModelProvidersVie
|
|||
title=${keyBlocked}
|
||||
@click=${() => props.onOpenKeyEditor(card.id)}
|
||||
>
|
||||
${
|
||||
card.hasConfigApiKey ||
|
||||
card.profiles.some((profile) => profile.type === "api_key")
|
||||
? t("modelProviders.apiKey.replace")
|
||||
: t("modelProviders.apiKey.set")
|
||||
}
|
||||
${t("modelProviders.apiKey.set")}
|
||||
</button>
|
||||
`
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue