This document describes the changes required in this CLIProxyAPI fork so it behaves exactly as expected by the current VibeProxy menubar app.
Goals:
- Support multiple authenticated accounts per provider (Claude, Codex, Gemini, Qwen, etc.).
- Let VibeProxy choose the active account per provider via files under
~/.cli-proxy-api. - Preserve backward compatibility with existing single-account setups.
Each authenticated account must be stored as its own JSON file:
- Directory:
~/.cli-proxy-api/ - Filename pattern:
provider-accountId.jsonprovider: lowercase provider key (claude,codex,gemini,qwen, etc.).accountId: stable identifier for the account (see below).
Each per-account JSON must contain at least:
type: string- Provider identifier used elsewhere (e.g.
"claude","codex","gemini","qwen").
- Provider identifier used elsewhere (e.g.
accountId: string- Required for new files.
- Unique/stable ID for this account.
accountNickname: string (optional)- For UI display; VibeProxy modifies this.
email: string (optional)- Used by VibeProxy for status text.
expired: ISO8601 datetime with fractional seconds (optional)- Used to compute
isExpiredin VibeProxy.
- Used to compute
createdAt: ISO8601 datetime with fractional seconds (optional)- Used for ordering / display.
- Provider-specific tokens/refresh tokens/etc.
Behavioral requirements:
- On successful auth:
- DO NOT rely solely on a single global
provider.jsonbeing overwritten. - Create or update a specific
provider-accountId.jsonfile for that account.
- DO NOT rely solely on a single global
- Support multiple
provider-*.jsonfiles co-existing per provider.
VibeProxy’s AuthStatus.swift reconstructs accounts based on JSON plus filename; CLIProxyAPI must align.
Rules:
-
Preferred format for new files:
provider-accountId.json- Write
accountIdinto the JSON body.
-
When loading:
- Strip
.json→baseName. - If JSON has
accountId, use it. - Else, infer:
- If
baseNamestarts with"{provider}-", use the suffix asaccountId. - Otherwise, use
baseNameitself (for legacy UUID-style or old files).
- If
- Strip
-
Always include
typein JSON so VibeProxy can group by provider.
This matches the extraction logic already used by VibeProxy.
VibeProxy selects accounts by writing a control file:
- Path:
~/.cli-proxy-api/active-accounts.json - Format: flat object mapping provider → selected identifier.
Example:
{
"claude": "claude-abc123",
"codex": "work@example.com",
"gemini": "gemini-sandbox",
"qwen": "another@example.com"
}VibeProxy writes:
- A value derived from the chosen
accountId, possibly including or derived from:provider-accountId- bare
accountId - email-like strings (see below)
CLIProxyAPI must:
-
On each request (or on suitable cache/refresh):
- Enumerate all
~/.cli-proxy-api/*.jsonfiles. - Parse each into an internal
Accountmodel. - Group accounts by their
type(provider).
- Enumerate all
-
Load
active-accounts.jsonif it exists. -
For the provider handling the current request, resolve the active account:
Matching strategy (in order):
- Exact match against
account.accountId. - If the configured value looks like
provider-*:- Strip
provider-and match suffix toaccountId.
- Strip
- Compare against email when present.
- Compare against filename-derived id (same rules VibeProxy uses):
- Base name without
.json - Optionally without
provider-prefix.
- Base name without
- Exact match against
-
If a matching account is found and not expired:
- Use that account’s credentials for the upstream call.
-
If no valid match:
- Fallback to:
- First non-expired account for that provider, or
- Existing single-account behavior.
- Never hard-fail solely due to a bad
active-accounts.json.
- Fallback to:
-
If
active-accounts.jsonis missing/malformed:- Ignore and use default account selection.
Per provider:
- All
provider-*.jsonfiles represent distinct accounts. - VibeProxy:
- Scans
~/.cli-proxy-api/*.json. - Reads
type,email,expired,accountId, etc. - Lets the user pick an account; writes choice into
active-accounts.json. - Updates
accountNicknameand may delete account files.
- Scans
- CLIProxyAPI:
- Must treat these files as canonical.
- Must respect
active-accounts.jsonfor which account to use. - Must tolerate UI edits:
accountNicknamechanges.- Account file deletion (treat missing file as account removed).
No new HTTP endpoints are required for VibeProxy; everything is via filesystem contract.
VibeProxy uses:
expiredfield →isExpired = expired < now.
CLIProxyAPI should:
- Populate or update
expiredwhen tokens are issued/refreshed/invalidated. - Avoid automatically selecting expired accounts when valid ones exist.
- On fallback:
- Prefer non-expired accounts.
- If
expiredis absent:- Treat account as non-expired.
VibeProxy writes accountNickname into the same per-account JSON file.
CLIProxyAPI must:
- Preserve
accountNicknameand unknown fields when updating files:- Read existing JSON → merge new values → write back.
- Never depend on
accountNicknameas an identifier. - Never wipe metadata like
accountNickname,createdAtunless intentionally migrating.
Requirements:
- Existing installs with a single credentials file must keep working.
Recommended behavior:
- On startup / auth loading:
- Support legacy files (e.g.
claude.jsonor single JSON). - Treat them as one account for that provider.
- Optionally expose them in the same
Accountmodel (accountIdfrom filename).
- Support legacy files (e.g.
- When adding new accounts:
- Use the new per-account pattern and include
accountIdin JSON.
- Use the new per-account pattern and include
- When updating logic:
- Prefer non-breaking migrations (no forced renames unless explicitly implemented).
When applying changes in this fork, ensure at least:
-
Internal model:
- Introduce
Account/ProviderAccountstructure:type,accountId,email,accountNickname,expired,createdAt, token fields.
- Add helpers:
- Scan
~/.cli-proxy-api. - Parse and group accounts by provider.
- Derive
accountIdfrom filename when missing.
- Scan
- Introduce
-
Auth flows:
- On successful OAuth/auth:
- Create/maintain
provider-accountId.json. - Include
typeandaccountId. - Do not assume a single global file per provider.
- Create/maintain
- On successful OAuth/auth:
-
Active account resolution:
- Implement loader for
active-accounts.json. - For each request:
- Resolve active account per provider using matching rules above.
- Fallback gracefully if no match/expired/invalid.
- Implement loader for
-
Request routing:
- Wherever CLIProxyAPI selects credentials/tokens:
- Use the resolved active
Accountfor that provider.
- Use the resolved active
- Ensure all provider integrations (Claude, etc.) honor this.
- Wherever CLIProxyAPI selects credentials/tokens:
-
File I/O robustness:
- Ignore malformed JSON files instead of crashing.
- Ignore malformed
active-accounts.jsonand continue with defaults. - Preserve unknown fields on write.
-
Testing scenarios:
- Single-account, no
active-accounts.json→ unchanged behavior. - Two+ accounts for same provider:
- Changing
active-accounts.jsonswitches which credentials are used.
- Changing
- Expired active account:
- Fallback to valid account (if any).
- Account JSON deleted externally:
- No crash; no longer selectable/used.
-
accountNicknameedited externally:- No effect on routing; only UI.
- Single-account, no
Summary: this spec reconstructs the contract implied by VibeProxy’s AuthManager and menu integration so your CLIProxyAPI fork can be updated independently while remaining fully compatible with the existing multi-account UI and selection behavior.