Auth
Store and manage LLM provider credentials securely — API keys or browser-based OAuth
praisonai auth stores provider credentials locally so praisonai run works without exporting env vars every session. Two methods are supported: a long-lived API key, or a short-lived OAuth token signed in via your browser.
Quick Start
Picker
# Omit the provider to pick from the model catalogue (~30+ LLMs)
praisonai auth login
# → Choose your LLM provider: openai | anthropic | google | ollama | ... | custom
praisonai run "What is 2+2?"The picker is built from ModelCatalogue.list_providers(): openai, anthropic, google, ollama first, every other catalogue provider (Groq, OpenRouter, Mistral, DeepSeek, xAI, …) next, and custom last.
API Key
# Interactive — key is hidden at the prompt
praisonai auth login openai
# Then run immediately
praisonai run "What is 2+2?"OAuth (Browser)
# Device-code flow; sign in; tokens stored + auto-refreshed
praisonai auth login github --method oauth --client-id <your-github-oauth-app-client-id>
praisonai run "What is 2+2?"Headless / SSH
# Prints a URL + code instead of opening a browser
praisonai auth login github --method oauth --client-id <your-github-oauth-app-client-id> --no-browser
# To sign in, visit https://github.com/login/device and enter code: ABCD-EFGH
praisonai run "What is 2+2?"Commands
| Command | Description |
|---|---|
auth login [<provider>] | Store credentials. Omit <provider> to open the catalogue-driven picker. --method auto (default) picks OAuth if available, else API key |
auth list | Show stored and active env-var providers with method, source, and expiry |
auth status [provider] | Format check; --validate does a live call (uses a freshly-refreshed OAuth token) |
auth logout <provider> | Remove one provider |
auth logout --all | Remove all stored credentials |
Login flags
| Flag | Default | Purpose | ||
|---|---|---|---|---|
| `–method auto\ | apikey\ | oauth` | auto | Force a flow, or let the CLI pick |
--key sk-… | (prompt) | API-key only; skip prompt | ||
--key-stdin | off | Read API key from stdin | ||
--no-browser | off | OAuth only; print URL + code instead of opening a browser | ||
--base-url URL | (default) | Custom provider endpoint | ||
--model NAME | (none) | Default model recorded with the credential | ||
--skip-validation | off | API-key only; skip the live check | ||
--client-id ID | (from registry) | OAuth client id. Overrides the built-in registry entry. Required for registry providers until a first-party PraisonAI app is provisioned | ||
--token-url URL | (from registry) | OAuth token endpoint. Set to enable OAuth for a provider outside the registry (e.g. self-hosted gateway) | ||
--device-authorization-url URL | (from registry) | RFC 8628 device-code endpoint | ||
--authorization-url URL | (from registry) | RFC 7636 PKCE authorization endpoint | ||
--scope SCOPE | (from registry) | Scope(s) to request; overrides the registry default |
Which --method should I use?
Provider Picker
Omit the provider argument and auth login opens a catalogue-driven picker so you never have to memorise a provider id.
praisonai auth loginChoose your LLM provider:
1) openai
2) anthropic
3) google
4) ollama
5) groq
6) openrouter
7) mistral
...
N) Other/custom
Select provider [1]:The list comes from ModelCatalogue.list_providers(): openai, anthropic, google, ollama first, every other catalogue provider appended, custom last. Picking custom prompts for a free-form provider id. The positional <provider> argument still works unchanged — keep passing it in scripts and CI.
OAuth Login (browser-based)
Two flows are negotiated automatically per provider:
- Device code (RFC 8628) — prints a short code + URL, polls until you approve. Default on headless servers.
- Authorization code + PKCE (RFC 7636) — opens your default browser and listens on
127.0.0.1for the redirect. Default on desktops.
The OAuth callback handler and PKCE helpers are shared with the MCP OAuth integration.
Token storage & refresh
| Item | Value |
|---|---|
| File | ~/.praisonai/credentials.json (unchanged from API-key flow) |
| Legacy fallback | ~/.praison/credentials.json is still read if the canonical file doesn’t exist yet; migrated onto the canonical file on the next write. No auth logout/re-login needed. |
| Permissions | 0o600, atomic writes |
| Refresh | Transparent on next use, ~60 s before expires_at |
| Refresh failure | CLI returns no token rather than a stale one — re-run auth login |
Self-hosted / custom OAuth providers
praisonai auth login ships built-in endpoints for github, google, gemini, and azure. For a provider outside the registry (e.g. a self-hosted gateway), supply the endpoints directly with CLI flags:
praisonai auth login my-gateway --method oauth \
--client-id cli \
--device-authorization-url https://gateway.example.com/oauth/device \
--token-url https://gateway.example.com/oauth/token \
--scope "read write"The CLI infers the device flow when a --device-authorization-url is present, or falls back to authcode (PKCE) when you pass --authorization-url instead.
As an escape hatch, you can also register a provider config from Python before invoking the flow:
from praisonai_code.cli.configuration.oauth import OAUTH_PROVIDERS, OAuthProviderConfig
OAUTH_PROVIDERS["my-gateway"] = OAuthProviderConfig(
flow="device",
client_id="my-client-id",
token_url="https://gateway.example.com/oauth/token",
device_authorization_url="https://gateway.example.com/oauth/device",
scope="read write",
)Headless / SSH
praisonai auth login github --method oauth --client-id <your-github-oauth-app-client-id> --no-browser
# To sign in, visit https://github.com/login/device and enter code: ABCD-EFGHAPI Key Login
praisonai auth login openai
praisonai auth login openai --key sk-...
echo "sk-..." | praisonai auth login openai --key-stdin
# Optional extras
praisonai auth login openai --key sk-... --base-url https://api.openai.com/v1 --model gpt-4o
praisonai auth login openai --skip-validationReplacing an existing key
The store holds one credential per provider, so auth login never overwrites a different stored key silently.
Interactive
praisonai auth login openai
# Enter API key for openai: ****************
# API key validated: live call ok
# Replace existing openai key sk-o***1111? [Y/n]: n
# → exits with status 1; the old key is keptPress Enter (default yes) to replace the key, or type n to abort.
Scripted (–key / –key-stdin)
praisonai auth login openai --key sk-new-...2222
# API key validated: live call ok
# Replacing existing openai key sk-o***1111
# Stored credentials for openai
# Key stored: sk-n***2222No prompt — scripts behave as before. The Replacing existing … line records which redacted key was overwritten.
JSON
praisonai --json auth login openai --key sk-new-...2222{
"provider": "openai",
"status": "stored",
"key_redacted": "sk-n***2222",
"replaced_key_redacted": "sk-o***1111"
}replaced_key_redacted appears only when a different stored key was replaced.
List & status
praisonai auth list
praisonai auth status
praisonai auth status openai --validateLogout
praisonai auth logout openai
praisonai auth logout --allList & Status — new columns
praisonai auth list
# Provider Method Source Secret Expires Base URL Model
# openai apikey env (OPENAI_API_KEY) sk-1***efgh (n/a) (default) (none)
# anthropic apikey stored sk-a***wxyz (n/a) (default) (none)
# groq apikey env (GROQ_API_KEY) gsk_***abcd (n/a) (default) (none)
# github oauth stored at-9***xyz1 59m (default) (none)Source shows where the live credential comes from: stored (from ~/.praisonai/credentials.json) or env (VARNAME) when an active environment variable is present. An env key that shadows a stored one is flagged env (VARNAME) [live, overrides stored].
Expires shows (n/a) for API keys, 59m, 1h 23m, <1m, or expired for OAuth tokens, and (no expiry) when the provider didn’t include one.
Stored vs environment credentials
auth list and auth status surface both stored credentials (from ~/.praisonai/credentials.json) and active environment-variable keys for catalogue-known providers. The Source column disambiguates them:
| Source | Meaning |
|---|---|
stored | From ~/.praisonai/credentials.json (unchanged behaviour) |
env (VAR_NAME) | Live credential coming from an environment variable; the var name is shown for unambiguity |
env (in-memory) | The whole store was loaded from PRAISONAI_AUTH_CONTENT — see Zero-disk mode |
When PRAISONAI_AUTH_CONTENT is set, every stored-side entry reports env (in-memory) instead of stored:
praisonai auth list
# Provider Method Source Secret Expires Base URL Model
# openai apikey env (in-memory) sk-1***efgh (n/a) (default) (none)
# github oauth env (in-memory) gho_***xyz1 59m (default) (none)If both a stored and an env credential exist for one provider, the env value wins at run time (this is unchanged) — the Source line appends [live, overrides stored] so it’s visible:
praisonai auth list
# Provider Method Source Secret Expires Base URL Model
# openai apikey env (OPENAI_API_KEY) [live, overrides stored] sk-1***efgh (n/a) (default) (none)Both stored and env secrets are redacted the same way (sk-1***efgh).
Credential env-var aliases
Some providers declare more than one credential env-var spelling — Together’s TOGETHER_API_KEY / TOGETHERAI_API_KEY, Fireworks’ FIREWORKS_API_KEY / FIREWORKS_AI_API_KEY, Google’s GOOGLE_API_KEY / GEMINI_API_KEY. auth list and auth status recognise every alias — a key set under a secondary spelling is surfaced exactly like one set under the canonical first.
export TOGETHERAI_API_KEY=sk-1234abcdefgh
praisonai auth list
# Provider Method Source Secret Expires Base URL Model
# together apikey env (TOGETHERAI_API_KEY) sk-1***efgh (n/a) (default) (none)The Source column names whichever alias is live in the environment, so it always tells you exactly which variable the runtime is reading — even when a secondary spelling is the one you set. If no alias is set but the provider is otherwise referenced, the canonical first var name is shown for display only.
Credential Fields (ProviderCredential)
Each stored provider credential contains the following fields:
| Field | Type | Description |
|---|---|---|
api_key | str | Static API key. For OAuth flows, mirrors access_token for backward compatibility |
access_token | str | OAuth access token (empty for API-key credentials) |
refresh_token | str | OAuth refresh token used for silent renewal |
expires_at | int | Unix epoch seconds when the access token expires. 0 for API keys (no expiry) |
auth_method | str | "apikey" or "oauth" — indicates which flow was used to store this credential |
auth list and auth status show Method and Expires columns reflecting these fields.
Storage & Security
| Item | Value |
|---|---|
| File | ~/.praisonai/credentials.json |
| Legacy fallback | Read-only fallback: ~/.praison/credentials.json. Any write transparently migrates its entries onto the canonical file. |
| Permissions | 0o600 (auto-corrected on read if loose) |
| Writes | Atomic via temp file + os.replace |
| Display | Keys/tokens redacted as at-9***xyz1 everywhere except the file |
| OAuth internals | refresh_token, token_url, client_id are scrubbed before crossing into the LLM resolver — never sent to the provider |
| Zero-disk mode | Set PRAISONAI_AUTH_CONTENT to a JSON store — nothing is read from or written to credentials.json for the process lifetime. See below |
| Replace | auth login confirms (interactive) or reports (scripted) before overwriting a different stored key. See Replacing an existing key |
Zero-disk mode (PRAISONAI_AUTH_CONTENT)
Set PRAISONAI_AUTH_CONTENT to a JSON credential store and PraisonAI runs entirely from memory — nothing is ever read from or written to ~/.praisonai/credentials.json.
Use it for ephemeral containers (Docker/Kubernetes), CI runners, and secret-hygiene-sensitive deployments where OAuth and refresh tokens must not touch disk. A single secret injects the whole store — including OAuth entries — with zero on-disk footprint.
The value is a JSON object mapping provider name → credential object. Each credential object mirrors the on-disk ProviderCredential fields — api_key and auth_method for API keys, plus access_token, refresh_token, and expires_at for OAuth:
{
"openai": {
"api_key": "sk-...",
"auth_method": "apikey"
},
"github": {
"api_key": "gho_...",
"auth_method": "oauth",
"access_token": "gho_...",
"refresh_token": "ghr_...",
"expires_at": 1723545600
}
}Bash / CI
# Inject the store from a CI secret; nothing is written to disk
export PRAISONAI_AUTH_CONTENT='{"openai":{"api_key":"sk-...","auth_method":"apikey"}}'
praisonai run "What is 2+2?"Python
import json, os
from praisonaiagents import Agent
os.environ["PRAISONAI_AUTH_CONTENT"] = json.dumps({
"openai": {"api_key": "sk-...", "auth_method": "apikey"},
})
agent = Agent(name="CI Agent", instructions="Answer briefly")
agent.start("What is 2+2?")
# No ~/.praisonai/credentials.json is created — everything stays in memory.Invalid values fail fast rather than silently falling back to disk, protecting the zero-disk contract:
| Value | Behaviour |
|---|---|
| Unset | Falls back to disk (unchanged behaviour) |
| Empty / whitespace only | Raises ValueError — misconfiguration, no silent disk fallback |
| Not valid JSON | Raises ValueError with the underlying decode error |
| JSON scalar or array (not an object) | Raises ValueError — must map provider → credential |
| A provider value that isn’t a JSON object | Raises ValueError naming the offending provider(s) |
| Valid object with OAuth tokens | Loaded in memory; OAuth refresh works and stays in memory |
auth list and auth status report the Source column as env (in-memory) in this mode via the CredentialStore.is_in_memory property, so zero-disk runs are never silently invisible.
Supported Providers
praisonai auth list / praisonai auth status recognise every provider in PROVIDER_ENV_CATALOGUE — the same catalogue that drives first-run credential detection (full 14-row table) and praisonai setup.
| Provider | API key env var(s) | Default model | OAuth supported today? |
|---|---|---|---|
openai | OPENAI_API_KEY (+ OPENAI_BASE_URL if set) | gpt-4o-mini | Not yet — use API key |
anthropic | ANTHROPIC_API_KEY | anthropic/claude-3-5-sonnet-latest | Not yet — use API key |
google / gemini | GOOGLE_API_KEY / GEMINI_API_KEY | gemini/gemini-1.5-flash | ✅ — device flow, requires --client-id |
groq | GROQ_API_KEY | groq/llama-3.3-70b-versatile | Not yet — use API key |
cohere | COHERE_API_KEY | cohere/command-r | Not yet — use API key |
openrouter | OPENROUTER_API_KEY | openrouter/openai/gpt-4o-mini | Not yet — use API key |
ollama | OLLAMA_API_BASE / OLLAMA_HOST (endpoint hints, not credentials) | ollama/llama3.2 | n/a — local |
mistral | MISTRAL_API_KEY | mistral/mistral-large-latest | Not yet — use API key |
deepseek | DEEPSEEK_API_KEY | deepseek/deepseek-chat | Not yet — use API key |
xai | XAI_API_KEY | xai/grok-2-latest | Not yet — use API key |
together | TOGETHER_API_KEY or TOGETHERAI_API_KEY | together_ai/meta-llama/Llama-3.3-70B-Instruct-Turbo | Not yet — use API key |
perplexity | PERPLEXITYAI_API_KEY | perplexity/sonar | Not yet — use API key |
fireworks | FIREWORKS_API_KEY or FIREWORKS_AI_API_KEY | fireworks_ai/accounts/fireworks/models/llama-v3p3-70b-instruct | Not yet — use API key |
github | n/a | (your model) | ✅ — device flow, requires --client-id |
azure | n/a | (your model) | ✅ — device flow, requires --client-id |
| Self-hosted / custom | n/a | (your model) | ✅ — supply endpoints via --token-url / --device-authorization-url |
Plugin providers (via praisonai.providers entry point) | ` |
_API_KEY` (derived) | (your model) | Not yet — use API key |
Unknown providers accept API keys with length ≥ 10. OAuth is opt-in per provider; registry providers ship endpoints but no client_id — pass yours with --client-id.