Select the best available API credential for each provider request, avoiding rate-limited or expired keys.
Agents/Providers — Token Management
API key rotation, auth health monitoring & cooldown tracking
The token management system handles the lifecycle of API credentials across all providers. It stores multiple auth profiles per provider, monitors their health status, implements fair rotation with cooldown tracking, and synchronizes credentials from external CLI tools on a configurable TTL.
Key Rotation Health Monitoring Cooldown Tracking File Locking
📚 What is this & why does it matter?
When you have multiple API keys for the same provider (personal key, team key, trial key), this system decides which one to use for each request. It tracks which keys are healthy, which hit rate limits, and when to retry failed keys — all without manual intervention.
💡 Real-world analogy
Imagine a delivery company with multiple drivers (API keys). A dispatcher (the auth resolver) assigns packages to drivers based on availability. If a driver gets a flat tire (rate limit), they’re put on a cooldown — no new packages until the tire is fixed. Meanwhile, the dispatcher routes packages to other drivers. If all drivers are down, the dispatcher checks who will be back soonest and queues the next package for that time. The dispatcher also periodically checks in with a staffing agency (external CLI sync) to see if new drivers have been added.
Contents
Overview
Auth store file (JSON with file-locking), provider ID from model resolution, API error responses (401/403/429).
Which auth profile to select (recency + random rotation), when to enter/exit cooldown, whether to allow a cooldown probe.
Selected API credentials for the request, updated cooldown state in the auth store, health status metadata for reporting.
flowchart TD
subgraph Input["Request Phase"]
REQ["Agent request\n(provider + model)"]
STORE["auth-profiles.ts\nLoad auth store\n(file-locked)"]
end
subgraph Resolution["Profile Resolution"]
ORDER["resolveAuthProfileOrder()"]
FILTER["Filter expired &\ncooldown profiles"]
SORT["Sort by recency\n+ random for fairness"]
end
subgraph Health["Health Check"]
HEALTH["AuthProfileHealth\nok | expiring | expired\nmissing | static"]
COOLDOWN["Cooldown tracker\nnow + TTL expiry"]
SOONEST["getSoonestCooldownExpiry()"]
end
subgraph Output["Selected Credential"]
CRED["API key / token\nfor provider request"]
FAIL["markAuthProfileFailure()\nSet cooldown on error"]
end
REQ --> STORE
STORE --> ORDER
ORDER --> FILTER
FILTER --> SORT
SORT --> HEALTH
HEALTH --> CRED
CRED -->|"401/403/429"| FAIL
FAIL --> COOLDOWN
COOLDOWN --> SOONEST
SOONEST -->|"retry after expiry"| FILTER
The flow begins when the agent runtime needs credentials for a provider request. The auth store is loaded with file locking to prevent corruption from concurrent access. resolveAuthProfileOrder() applies configured ordering, filters out expired and cooling-down profiles, then sorts by recency with random jitter for fair rotation across equivalent profiles.
Decision Points
| Decision | What controls it | Why it matters |
|---|---|---|
| Profile selection order | Config ordering + recency + random jitter | Ensures fair distribution across keys to avoid burning one key’s rate limit |
| Enter cooldown | API error status codes: 401, 403, 429 | Prevents repeated requests with a known-bad credential |
| Cooldown TTL | now + ttl (varies by error type) | Rate limits need shorter cooldown than auth failures |
| Allow cooldown probe | shouldAllowCooldownProbeForReason() | Lets recently-failed profiles retry once with short timeout to detect recovery |
| External CLI sync | EXTERNAL_CLI_SYNC_TTL_MS (15 min) | Picks up new credentials from CLI tools without restart |
| Lock acquisition | AUTH_STORE_LOCK_OPTIONS.retries (10) | Prevents data corruption from concurrent agent processes |
How It Works
Token management operates in three phases: resolution (selecting the best profile), monitoring (tracking health status), and recovery (cooldown management after failures).
Phase 1: Auth Profile Resolution
The resolveAuthProfileOrder() function implements the core selection algorithm. It loads the auth store, applies any configured ordering preferences, filters out unusable profiles, and returns the best available credential.
// auth-profiles.ts — simplified resolution flow
export async function resolveAuthProfileOrder(
providerId: string,
configOrder?: string[],
): Promise<ResolvedAuthProfile | null> {
// 1. Load auth store with file locking
const store = await loadAuthStore({
retries: AUTH_STORE_LOCK_OPTIONS.retries, // 10
minTimeout: 100, // ms
maxTimeout: 10_000, // ms
factor: 2, // exponential backoff
stale: 30_000, // 30s staleness
});
// 2. Get all profiles for this provider
const profiles = store.profiles.filter(p => p.providerId === providerId);
// 3. Apply config ordering (if provided)
const ordered = configOrder
? reorderByConfig(profiles, configOrder)
: profiles;
// 4. Filter expired and in-cooldown profiles
const now = Date.now();
const available = ordered.filter(p => {
if (p.expiresAt && p.expiresAt < now) return false; // expired
if (p.cooldownExpiry && p.cooldownExpiry > now) return false; // in cooldown
return true;
});
// 5. Sort by recency with random jitter for fair rotation
available.sort((a, b) => {
const recencyDiff = (b.lastUsedAt ?? 0) - (a.lastUsedAt ?? 0);
return recencyDiff + (Math.random() - 0.5) * 1000;
});
return available[0] ?? null;
}
Phase 2: Health Monitoring
The AuthProfileHealth type tracks the health status of each stored credential. The health check runs before every profile selection and after every API response.
| Status | Meaning | Transition trigger |
|---|---|---|
ok | Profile is valid and ready | Successful API call or cooldown expiry |
expiring | Profile will expire soon | expiresAt - now < warningThreshold |
expired | Profile has expired | expiresAt < now |
missing | No profiles found for provider | Empty profile list after filtering |
static | Profile has no expiry (permanent key) | Profile loaded without expiresAt field |
Phase 3: Cooldown & Recovery
When an API request fails with an auth or rate-limit error, markAuthProfileFailure() places the profile into cooldown. The cooldown expiry is calculated as now + ttl where the TTL varies by error type.
Auth Profile State Machine
stateDiagram-v2
[*] --> Valid : Profile loaded
Valid --> InUse : Selected for request
InUse --> Valid : API success
InUse --> Cooldown : API error (401/403/429)
Cooldown --> Probe : shouldAllowCooldownProbe()
Probe --> Valid : Probe succeeds
Probe --> Cooldown : Probe fails
Cooldown --> Valid : Cooldown expiry passes
Valid --> Expiring : Near expiry threshold
Expiring --> Expired : expiresAt reached
Expired --> Valid : Manual refresh / CLI sync
Expired --> [*] : Removed from store
note right of Cooldown
cooldownExpiry = now + ttl
ttl varies by error type:
401/403 → longer TTL
429 → shorter TTL
end note
note right of Probe
Single retry with short timeout
Only allowed for specific error reasons
end note
The state machine above shows the full lifecycle of an auth profile. Most profiles spend their time in the Valid ↔ InUse cycle. The Cooldown state is entered on API failure and exited either by timer expiry or by a successful cooldown probe. The probe mechanism is controlled by shouldAllowCooldownProbeForReason() which only allows probes for transient errors (rate limits), not permanent failures (invalid key).
getSoonestCooldownExpiry() to find the earliest available retry time. The caller can then decide whether to wait or fail immediately with timing information.
Annotated Code Walkthrough
Walk through the cooldown marking and recovery flow.
When an API call fails, markAuthProfileFailure() is called with the error details:
// auth-profiles.ts — marking a failure
export async function markAuthProfileFailure(
profileId: string,
error: ProviderError,
): Promise<void> {
const store = await loadAuthStore(AUTH_STORE_LOCK_OPTIONS);
const profile = store.profiles.find(p => p.id === profileId);
if (!profile) return;
const ttl = getCooldownTTL(error.status);
profile.cooldownExpiry = Date.now() + ttl;
profile.lastError = {
status: error.status,
message: error.message,
timestamp: Date.now(),
};
await saveAuthStore(store);
}
function getCooldownTTL(status: number): number {
switch (status) {
case 429: return 30_000; // Rate limit: 30s cooldown
case 401: return 300_000; // Unauthorized: 5min cooldown
case 403: return 600_000; // Forbidden: 10min cooldown
default: return 60_000; // Other: 1min cooldown
}
}
The getSoonestCooldownExpiry() function scans all profiles to find the earliest recovery time:
// auth-profiles.ts — finding nearest available retry
export function getSoonestCooldownExpiry(
profiles: AuthProfile[],
): number | null {
const now = Date.now();
let soonest: number | null = null;
for (const p of profiles) {
if (!p.cooldownExpiry || p.cooldownExpiry <= now) {
return null; // At least one profile is already available
}
if (soonest === null || p.cooldownExpiry < soonest) {
soonest = p.cooldownExpiry;
}
}
return soonest;
}
null immediately — indicating at least one profile is available right now without waiting.
External CLI credential synchronization runs on a 15-minute TTL to pick up new keys without restarting the agent:
// auth-profiles.ts — external CLI sync
const EXTERNAL_CLI_SYNC_TTL_MS = 900_000; // 15 minutes
let lastExternalSync = 0;
async function maybeSyncExternalCredentials(
store: AuthStore,
): Promise<boolean> {
const now = Date.now();
if (now - lastExternalSync < EXTERNAL_CLI_SYNC_TTL_MS) {
return false; // TTL not yet expired
}
lastExternalSync = now;
const externalProfiles = await loadFromExternalCLI();
let changed = false;
for (const ext of externalProfiles) {
const existing = store.profiles.find(
p => p.providerId === ext.providerId && p.keyHash === ext.keyHash,
);
if (!existing) {
store.profiles.push(ext);
changed = true;
}
}
return changed;
}
File Locking Details
The auth store is a JSON file on disk, and multiple agent processes may attempt to read or write it concurrently. The file-locking mechanism uses exponential backoff with configurable retry parameters:
| Parameter | Value | Purpose |
|---|---|---|
retries | 10 | Maximum lock acquisition attempts |
factor | 2 | Exponential backoff multiplier |
minTimeout | 100 ms | Initial retry delay |
maxTimeout | 10,000 ms | Maximum retry delay cap |
stale | 30,000 ms | Lock is considered stale after 30s (auto-cleanup) |
stale parameter (30s) ensures that orphaned locks are automatically broken, preventing permanent deadlocks. However, if a legitimate write takes longer than 30s (extremely rare), the stale detection may cause data corruption.
Edge Cases & Error Handling
| Scenario | Behavior | Code Location |
|---|---|---|
| All profiles in cooldown | getSoonestCooldownExpiry() returns nearest retry timestamp | auth-profiles.ts |
| Concurrent store writes | File lock with 10 retries and exponential backoff | auth-profiles.ts |
| Stale lock file from crash | Auto-broken after 30s staleness threshold | AUTH_STORE_LOCK_OPTIONS.stale |
| No profiles for provider | Health status set to missing; fallback to environment variable | auth-health.ts |
| External CLI not available | Sync silently skipped; existing profiles used | loadFromExternalCLI() |
| Profile expires mid-request | Completion runs normally; next request sees expired status | resolveAuthProfileOrder() |
| Rate limit with retry-after header | Cooldown TTL uses retry-after value if present (clamped to max) | markAuthProfileFailure() |
Configuration
| Knob / Parameter | Default | Description |
|---|---|---|
EXTERNAL_CLI_SYNC_TTL_MS | 900000 (15 min) | How often to check external CLI tools for new credentials |
AUTH_STORE_LOCK_OPTIONS.retries | 10 | Maximum lock acquisition attempts with exponential backoff |
AUTH_STORE_LOCK_OPTIONS.stale | 30000 (30s) | Threshold after which orphaned lock files are auto-broken |
| Cooldown TTL (429) | 30000 (30s) | Cooldown duration after rate limit error |
| Cooldown TTL (401) | 300000 (5 min) | Cooldown duration after unauthorized error |
| Cooldown TTL (403) | 600000 (10 min) | Cooldown duration after forbidden error |
| Config order | undefined | Optional user-configured profile priority ordering |
Related Topics
See also:
- Providers overview — the parent sub-module page
- Adapter Pattern — how provider wrappers consume the selected credentials
- Model Selection — model fallback chains interact with auth cooldowns
- Streaming pipeline — where auth errors trigger cooldown marking
Directory Map
Source files for token management
pi-embedded-runner/
├── auth-profiles.ts ← Auth store management, profile resolution, file locking
├── auth-health.ts ← AuthProfileHealth status types and health checks
├── model.ts ← Model registry (consumes auth profiles for provider auth)
└── run/
├── setup.ts ← Passes resolved auth to stream wrappers
└── attempt.ts ← Triggers markAuthProfileFailure() on API errors