mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-12 21:53:00 -06:00
feat(auth): support named model login profiles
* docs(auth): document named OAuth profile logins * feat(auth): support --profile-id in models auth login * docs: note named model login profiles --------- Co-authored-by: Peter Steinberger <steipete@gmail.com>
This commit is contained in:
@@ -12,6 +12,7 @@ Docs: https://docs.openclaw.ai
|
||||
- Docs: clarify README onboarding and Gateway startup paths, WhatsApp QR/408 recovery, cron output language prompts, skill advanced features, gateway upstream 403 troubleshooting, and plugin fallback override guidance. Thanks @deepujain, @Zacxxx, @Jah-yee, @neyric, @usimic, @Renu-Cybe, @BigUncle, and @SeashoreShi.
|
||||
- Docs: clarify context-pruning ratio bounds, local dashboard recovery, CLI env markers, remote onboarding token behavior, and Peekaboo Bridge permissions for subprocess agents. Thanks @ayesha-aziz123, @dishraters, @hougangdev, and @brandonlipman.
|
||||
- Docs: clarify browser CDP diagnostics, Plugin SDK allowlist imports, status-reaction timing defaults, queue steering behavior, limited-tool troubleshooting, cron HEARTBEAT handling, Telegram multi-agent groups, Bitwarden SecretRef setup, and EasyRunner deployments. Thanks @Quratulain-bilal, @mbelinky, @Mickey-, @vancece, @xenouzik, @posigit, @surlymochan, @janaka, and @choiking.
|
||||
- CLI/models: let `openclaw models auth login` store a single returned provider auth profile under a requested `--profile-id`, and document named Codex OAuth profile setup. (#49315) Thanks @DanielLSM.
|
||||
- Crabbox/Testbox: run clean sparse-checkout Testbox syncs from a temporary full checkout and route remote changed gates through Corepack pnpm.
|
||||
- Docs: clarify IPv4-only Gateway BYOH binding, trusted-proxy scope clearing, Android pairing approval, macOS Accessibility grants, Zalo profile env vars, password-store SecretRef setup, and Chinese memory navigation. Thanks @itskai-dev, @gwh7078, @longstoryscott, @MoeJaberr, and @yuaiccc.
|
||||
- Docs: consolidate GLM under Z.AI, add the Upstash Box install guide and Gateway exposure runbook, clarify MEDIA directives, Copilot and Voyage setup, config path quoting, real behavior proof, and memory-file write guidance. Thanks @BobDu, @alitariksahin, @Jefsky, @musaabhasan, @OmerZeyveli, @leno23, @WuKongAI-CMU, @luoyanglang, and @majin1102.
|
||||
|
||||
@@ -169,6 +169,7 @@ openclaw models fallbacks list
|
||||
openclaw models auth add
|
||||
openclaw models auth list [--provider <id>] [--json]
|
||||
openclaw models auth login --provider <id>
|
||||
openclaw models auth login --provider openai --profile-id openai:work
|
||||
openclaw models auth paste-api-key --provider <id>
|
||||
openclaw models auth setup-token --provider <id>
|
||||
openclaw models auth paste-token
|
||||
@@ -205,6 +206,9 @@ openclaw models auth list --provider openai
|
||||
|
||||
Notes:
|
||||
|
||||
- `login` accepts `--profile-id <id>` for providers that support named
|
||||
profiles during login. Use this to keep multiple logins for the same
|
||||
provider separate.
|
||||
- `paste-api-key` accepts API keys generated elsewhere, prompts for the key
|
||||
value, and writes it to the default profile id `<provider>:manual` unless you
|
||||
pass `--profile-id`. In automation, pipe the key on stdin, for example
|
||||
|
||||
@@ -193,6 +193,19 @@ key in the provider dashboard when you need provider-side invalidation.
|
||||
|
||||
## Controlling which credential is used
|
||||
|
||||
### During login (CLI)
|
||||
|
||||
Use `openclaw models auth login --provider <id> --profile-id <profileId>` for
|
||||
providers that support named auth profiles during login.
|
||||
|
||||
```bash
|
||||
openclaw models auth login --provider openai-codex --profile-id openai-codex:ritsuko
|
||||
openclaw models auth login --provider openai-codex --profile-id openai-codex:lain
|
||||
```
|
||||
|
||||
This is the easiest way to keep multiple OAuth logins for the same provider
|
||||
separate inside one agent.
|
||||
|
||||
### Per-session (chat command)
|
||||
|
||||
Use `/model <alias-or-id>@<profileId>` to pin a specific provider credential for the current session (example profile ids: `anthropic:default`, `anthropic:work`).
|
||||
@@ -215,6 +228,10 @@ stored profiles as `excluded_by_auth_order` instead of silently skipping them.
|
||||
When you debug cooldown issues, remember that rate-limit cooldowns can be tied
|
||||
to one model id rather than the whole provider profile.
|
||||
|
||||
If you change auth order or profile pinning for a chat that is already running,
|
||||
send `/new` or `/reset` in that chat to start a fresh session. Existing
|
||||
sessions can keep their current model/profile selection until reset.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### "No credentials found"
|
||||
|
||||
@@ -337,6 +337,14 @@ Choose your preferred auth method and follow the setup steps.
|
||||
openclaw models status --probe --probe-provider openai-codex
|
||||
```
|
||||
|
||||
Use `--profile-id` when you want multiple Codex OAuth logins in the same
|
||||
agent and later want to control them via auth ordering or `/model ...@<profileId>`:
|
||||
|
||||
```bash
|
||||
openclaw models auth login --provider openai-codex --profile-id openai-codex:ritsuko
|
||||
openclaw models auth login --provider openai-codex --profile-id openai-codex:lain
|
||||
```
|
||||
|
||||
`openai/*` is the model route for OpenAI agent turns through Codex. The
|
||||
`openai-codex` auth/profile provider id remains accepted for existing
|
||||
profiles and CLI listing.
|
||||
|
||||
@@ -333,6 +333,10 @@ export function registerModelsCli(program: Command) {
|
||||
.option("--provider <id>", "Provider id registered by a plugin")
|
||||
.option("--method <id>", "Provider auth method id")
|
||||
.option("--device-code", "Use the provider device-code auth method", false)
|
||||
.option(
|
||||
"--profile-id <id>",
|
||||
"Auth profile id override for single-profile login methods",
|
||||
)
|
||||
.option("--set-default", "Apply the provider's default model recommendation", false)
|
||||
.action(async (opts, command) => {
|
||||
if (opts.deviceCode && typeof opts.method === "string" && opts.method !== "device-code") {
|
||||
@@ -347,6 +351,7 @@ export function registerModelsCli(program: Command) {
|
||||
{
|
||||
provider: opts.provider as string | undefined,
|
||||
method: opts.deviceCode ? "device-code" : (opts.method as string | undefined),
|
||||
profileId: opts.profileId as string | undefined,
|
||||
setDefault: Boolean(opts.setDefault),
|
||||
agent,
|
||||
},
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { resolveLoginProfiles } from "./auth.js";
|
||||
|
||||
describe("resolveLoginProfiles", () => {
|
||||
it("returns original profiles when --profile-id is not provided", () => {
|
||||
const profiles = [
|
||||
{
|
||||
profileId: "openai-codex:default",
|
||||
credential: {
|
||||
type: "oauth" as const,
|
||||
provider: "openai-codex",
|
||||
access: "a",
|
||||
refresh: "r",
|
||||
expires: Date.now() + 60_000,
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const resolved = resolveLoginProfiles({
|
||||
result: { profiles },
|
||||
});
|
||||
|
||||
expect(resolved).toEqual(profiles);
|
||||
});
|
||||
|
||||
it("overrides profile id when exactly one profile is returned", () => {
|
||||
const resolved = resolveLoginProfiles({
|
||||
requestedProfileId: "openai-codex:work",
|
||||
result: {
|
||||
profiles: [
|
||||
{
|
||||
profileId: "openai-codex:default",
|
||||
credential: {
|
||||
type: "oauth" as const,
|
||||
provider: "openai-codex",
|
||||
access: "a",
|
||||
refresh: "r",
|
||||
expires: Date.now() + 60_000,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
expect(resolved).toHaveLength(1);
|
||||
expect(resolved[0]?.profileId).toBe("openai-codex:work");
|
||||
});
|
||||
|
||||
it("throws when --profile-id is used with multi-profile auth responses", () => {
|
||||
expect(() =>
|
||||
resolveLoginProfiles({
|
||||
requestedProfileId: "provider:manual",
|
||||
result: {
|
||||
profiles: [
|
||||
{
|
||||
profileId: "provider:one",
|
||||
credential: { type: "api_key" as const, provider: "provider", key: "k1" },
|
||||
},
|
||||
{
|
||||
profileId: "provider:two",
|
||||
credential: { type: "api_key" as const, provider: "provider", key: "k2" },
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
).toThrow(/--profile-id requires exactly one returned auth profile/i);
|
||||
});
|
||||
});
|
||||
@@ -379,6 +379,7 @@ async function pickProviderTokenMethod(params: {
|
||||
|
||||
async function persistProviderAuthResult(params: {
|
||||
result: ProviderAuthResult;
|
||||
profiles?: ProviderAuthResult["profiles"];
|
||||
agentDir: string;
|
||||
runtime: RuntimeEnv;
|
||||
prompter: ReturnType<typeof createClackPrompter>;
|
||||
@@ -387,7 +388,9 @@ async function persistProviderAuthResult(params: {
|
||||
const defaultModel = params.result.defaultModel
|
||||
? normalizeAgentModelRefForConfig(params.result.defaultModel)
|
||||
: undefined;
|
||||
for (const profile of params.result.profiles) {
|
||||
const profiles = params.profiles ?? params.result.profiles;
|
||||
|
||||
for (const profile of profiles) {
|
||||
await upsertAuthProfileWithLockOrThrow({
|
||||
profileId: profile.profileId,
|
||||
credential: profile.credential,
|
||||
@@ -408,7 +411,7 @@ async function persistProviderAuthResult(params: {
|
||||
replaceDefaultModels: params.result.replaceDefaultModels,
|
||||
});
|
||||
}
|
||||
for (const profile of params.result.profiles) {
|
||||
for (const profile of profiles) {
|
||||
next = applyAuthProfileConfig(next, {
|
||||
profileId: profile.profileId,
|
||||
provider: profile.credential.provider,
|
||||
@@ -436,7 +439,7 @@ async function persistProviderAuthResult(params: {
|
||||
}
|
||||
|
||||
logConfigUpdated(params.runtime);
|
||||
for (const profile of params.result.profiles) {
|
||||
for (const profile of profiles) {
|
||||
params.runtime.log(
|
||||
`Auth profile: ${profile.profileId} (${profile.credential.provider}/${credentialMode(profile.credential)})`,
|
||||
);
|
||||
@@ -461,6 +464,7 @@ async function runProviderAuthMethod(params: {
|
||||
method: ProviderAuthMethod;
|
||||
runtime: RuntimeEnv;
|
||||
prompter: ReturnType<typeof createClackPrompter>;
|
||||
profileId?: string;
|
||||
setDefault?: boolean;
|
||||
}) {
|
||||
const selectedProviderId = normalizeProviderId(params.provider.id);
|
||||
@@ -492,8 +496,14 @@ async function runProviderAuthMethod(params: {
|
||||
}
|
||||
}
|
||||
|
||||
const profiles = resolveLoginProfiles({
|
||||
result,
|
||||
requestedProfileId: params.profileId,
|
||||
});
|
||||
|
||||
await persistProviderAuthResult({
|
||||
result,
|
||||
profiles,
|
||||
agentDir: params.agentDir,
|
||||
runtime: params.runtime,
|
||||
prompter: params.prompter,
|
||||
@@ -795,6 +805,7 @@ export async function modelsAuthAddCommand(opts: { agent?: string }, runtime: Ru
|
||||
type LoginOptions = {
|
||||
provider?: string;
|
||||
method?: string;
|
||||
profileId?: string;
|
||||
setDefault?: boolean;
|
||||
yes?: boolean;
|
||||
agent?: string;
|
||||
@@ -837,6 +848,25 @@ function credentialMode(credential: AuthProfileCredential): "api_key" | "oauth"
|
||||
return "oauth";
|
||||
}
|
||||
|
||||
export function resolveLoginProfiles(params: {
|
||||
result: ProviderAuthResult;
|
||||
requestedProfileId?: string;
|
||||
}): ProviderAuthResult["profiles"] {
|
||||
const requestedProfileId = params.requestedProfileId?.trim();
|
||||
if (!requestedProfileId) {
|
||||
return params.result.profiles;
|
||||
}
|
||||
|
||||
if (params.result.profiles.length !== 1) {
|
||||
throw new Error(
|
||||
"--profile-id requires exactly one returned auth profile from the selected auth method.",
|
||||
);
|
||||
}
|
||||
|
||||
const [profile] = params.result.profiles;
|
||||
return [{ ...profile, profileId: requestedProfileId }];
|
||||
}
|
||||
|
||||
function maybeLogOpenAICodexNativeSearchTip(runtime: RuntimeEnv, providerId: string) {
|
||||
if (providerId !== "openai-codex") {
|
||||
return;
|
||||
@@ -845,6 +875,7 @@ function maybeLogOpenAICodexNativeSearchTip(runtime: RuntimeEnv, providerId: str
|
||||
"Tip: Codex-capable models can use native Codex web search. Enable it with openclaw configure --section web (recommended mode: cached). Docs: https://docs.openclaw.ai/tools/web",
|
||||
);
|
||||
}
|
||||
|
||||
export async function modelsAuthLoginCommand(opts: LoginOptions, runtime: RuntimeEnv) {
|
||||
if (!process.stdin.isTTY) {
|
||||
throw new Error(
|
||||
@@ -903,6 +934,7 @@ export async function modelsAuthLoginCommand(opts: LoginOptions, runtime: Runtim
|
||||
method: chosenMethod,
|
||||
runtime,
|
||||
prompter,
|
||||
profileId: opts.profileId,
|
||||
setDefault: opts.setDefault,
|
||||
});
|
||||
maybeLogOpenAICodexNativeSearchTip(runtime, selectedProvider.id);
|
||||
|
||||
Reference in New Issue
Block a user