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:
Daniel Marta
2026-05-23 13:44:57 +02:00
committed by GitHub
parent 55a0c9b1f4
commit 4ec85762ab
7 changed files with 138 additions and 3 deletions
+1
View File
@@ -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.
+4
View File
@@ -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
+17
View File
@@ -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"
+8
View File
@@ -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.
+5
View File
@@ -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);
});
});
+35 -3
View File
@@ -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);