Skip to content

feat: multi-account auth management (add/list/remove)#33

Merged
lubobill1990 merged 28 commits into
masterfrom
feat/17-auth-management
Apr 26, 2026
Merged

feat: multi-account auth management (add/list/remove)#33
lubobill1990 merged 28 commits into
masterfrom
feat/17-auth-management

Conversation

@lubobill1990
Copy link
Copy Markdown
Collaborator

Summary

  • Add auth add subcommand: Device Flow OAuth with auto-detected GitHub username as account name, --name override, appends to accounts.json
  • Add auth list subcommand: shows all accounts with name, type, and GitHub login
  • Add auth remove --name <name> subcommand: removes account by name
  • Legacy bare auth still works as before (backward compatible)
  • Extract reusable runDeviceFlow() from token.ts
  • Add ACCOUNTS_FILE_PATH and account file CRUD helpers to accounts-loader.ts

Test plan

  • copilot-api auth add --name test → device flow → account appended to ~/.local/share/copilot-api/accounts.json
  • copilot-api auth list → table of accounts with GitHub usernames
  • copilot-api auth remove --name test → account removed
  • copilot-api auth → legacy single-token flow still works
  • All 99 existing tests pass

🤖 Generated with Claude Code

lubobill1990 and others added 28 commits April 25, 2026 13:47
Introduce bun:sqlite-backed db module with WAL mode, foreign keys,
and a meta-table-driven migration runner. Initial schema (001) creates
accounts, model_pricing, model_pricing_versions, pricing_sync_log,
usage_events, usage_daily and supporting indexes.

- src/lib/db.ts: initDb/getDb/withTransaction + test-only reset helper
- src/lib/migrations/001_initial.sql: full schema per design doc 02
- src/lib/paths.ts: USAGE_DB_PATH under APP_DIR
- src/start.ts: --db-path flag, initDb after ensurePaths
- src/types.d.ts: declare *.sql text imports
- tests/db.test.ts: 8 tests covering init, idempotency, tx, WAL, indexes

Closes #1

Co-Authored-By: Claude Opus 4 <noreply@anthropic.com>
- src/lib/account-pool.ts: AccountPool with round-robin / least-busy / least-recent strategies, acquire/release, cooldown and failure tracking
- src/lib/accounts-loader.ts: load accounts.json or fall back to legacy single token; persistAccounts() upserts into accounts table
- src/lib/state.ts: pool + strategy on State (legacy githubToken/copilotToken kept as shims for not-yet-migrated callers; removed in #3)
- src/lib/token.ts: setupCopilotTokenFor(account) sets up per-account refresh interval
- src/start.ts: --accounts-file, --strategy flags; load + persist + parallel token init

Tests: AccountPool picker + cooldown + acquire/release; loader file/legacy/empty paths; persistAccounts idempotency.

Refs #2

Co-Authored-By: Claude Opus 4 <noreply@anthropic.com>
All upstream-calling helpers now take an ApiContext (Account + VSCode
version) instead of reading from the global state. Handlers acquire an
account from the pool inline (real load-balancing comes in Task #4).

- src/lib/api-config.ts: copilotBaseUrl/copilotHeaders/githubHeaders take ApiContext
- src/services/{copilot,github}/*.ts: all signatures accept ctx as first arg
- src/lib/state.ts: drop legacy githubToken/copilotToken fields
- src/lib/token.ts: setupCopilotTokenFor uses makeApiContext; setupGitHubToken returns the token instead of mutating state
- src/lib/utils.ts: makeApiContext / defaultApiContext helpers; cacheModels uses pool's first account
- src/routes/{chat-completions,messages,embeddings}/*.ts: pool.acquire/release around upstream call
- src/routes/usage,token/route.ts: use defaultApiContext / defaultAccount
- src/check-usage.ts: build a temporary ApiContext from the loaded GitHub token

Refs #3

Co-Authored-By: Claude Opus 4 <noreply@anthropic.com>
- src/lib/with-account.ts: 401 -> refresh token + retry; 5xx/network -> cooldown + retry; 4xx propagates. Cap min(pool size, 3).
- Handlers in chat-completions, messages, embeddings now go through withAccount.

Tests: success, 5xx retry/cooldown, 4xx no retry, 401 refresh + retry, retries cap.

Refs #4

Co-Authored-By: Claude Opus 4 <noreply@anthropic.com>
- src/lib/usage-normalizer.ts: NormalizedUsage shape + helpers for OpenAI / Anthropic / embeddings, plus streaming accumulators that watch for include_usage / message_start+message_delta and finalize to NormalizedUsage. UsageMissingError raised if OpenAI stream finishes without usage.

Tests: OpenAI/Anthropic/embeddings normalize, OpenAI accumulator capture + missing-usage error, Anthropic accumulator aggregation + sane zeros.

Refs #5

Co-Authored-By: Claude Opus 4 <noreply@anthropic.com>
- src/lib/usage-recorder.ts: recordUsage(input) inserts one usage_events row and atomically upserts the matching usage_daily row using SQLite's date(?/1000,'unixepoch','localtime') for the day boundary. Pricing snapshots are pulled from model_pricing at write time; missing rows leave snapshots NULL. premium_request_count = premium_multiplier (or 0). Errors are caught and logged so the response path is never broken. isInternal=true short-circuits the write entirely (for x-internal-pricing-sync calls).

Tests: single insert, increment, missing pricing, isInternal, swallowed errors.

Refs #6

Co-Authored-By: Claude Opus 4 <noreply@anthropic.com>
After a successful non-streaming OpenAI response, the handler builds a NormalizedUsage from response.usage and calls recordUsage with status='ok', the chosen account, durationMs, requestId. On thrown errors before the recorder, a status='error' event with zero tokens is written. The x-internal-pricing-sync: 1 header is honored via isInternal.

Streaming branch unchanged (Task #8 owns it).

Refs #7

Co-Authored-By: Claude Opus 4 <noreply@anthropic.com>
- create-chat-completions.ts forces stream_options.include_usage=true on streaming requests so the upstream emits a final usage frame.
- Streaming branch uses an OpenAI accumulator: every chunk is parsed, fed, then forwarded untouched. After the stream closes, recordUsage is called with status='ok' (or 'aborted' if c.req.raw.signal.aborted, or 'error' on iteration error). Missing usage frame logs a warn and records zero tokens with status='error'.

Refs #8

Co-Authored-By: Claude Opus 4 <noreply@anthropic.com>
Embeddings handler now wraps the upstream call with withAccount and records a usage_events row with endpoint='embeddings', upstreamFormat='openai'. On error, a status='error' row with zero usage is recorded.

Refs #9

Co-Authored-By: Claude Opus 4 <noreply@anthropic.com>
The /v1/messages route translates Anthropic→OpenAI before sending, so we use the OpenAI accumulator to capture usage. endpoint='messages', upstreamFormat='anthropic' tags the API surface.

Stream branch: feed every chunk into accumulator, finalize on close (ok/aborted/error). Non-stream: normalize response.usage. Errors before the upstream call write a status='error' row.

Refs #10

Co-Authored-By: Claude Opus 4 <noreply@anthropic.com>
- src/lib/usage-stats.ts: computeUsageStats(filters) builds the three SQL templates (historical/current/timeline) and returns totals + byAccount.byModel.endpoint_breakdown + daily + missing_pricing per design doc 03.
- src/routes/usage/route.ts: parses ?from/?to/?account/?model/?endpoint/?lens; returns { ...primaryAccountQuotaForBackcompat, quota: { byAccount, primary }, stats }.

Tests: historical from snapshots, current uses live pricing, missing_pricing list, endpoint filter.

Refs #11

Co-Authored-By: Claude Opus 4 <noreply@anthropic.com>
- src/lib/pricing-sources.ts: fetchAzureRetailPrices() pages through Azure Retail Prices API; fetchAnthropicPricingHtml() best-effort GET.
- src/lib/pricing-sync.ts: pickSyncModel, buildSyncRequest, callSyncLlm (x-internal-pricing-sync + json_object), priceChanged (0.5%), sanityFails (10x).

Tests: priceChanged, sanityFails, pickSyncModel.

Refs #12

Co-Authored-By: Claude Opus 4 <noreply@anthropic.com>
- src/lib/pricing-sync-runner.ts: runPricingSync({port, syncModel?, parsedOverride?}) glues task #12's parsed output into the DB:
  - Sanity gate rejects whole sync on any 10x change.
  - For each row, looks up current model_pricing_versions WHERE effective_to IS NULL, skips when priceChanged() is false.
  - On change: patches old version's effective_to and inserts a new row; UPSERTs model_pricing.
  - Writes pricing_sync_log + bumps meta.last_pricing_sync_ts inside the same transaction.
  - Status: ok / partial / rejected / failed.
- src/lib/pricing-sync.ts: tighten null/undefined handling in priceChanged/sanityFails so the runner can pass DB-shaped rows.

Tests: first sync inserts versions; identical second sync no-op; changed price patches + inserts; 10x change rejects + leaves DB unchanged.

Refs #13

Co-Authored-By: Claude Opus 4 <noreply@anthropic.com>
Extract promptClaudeCodeSetup helper to stay within max-lines-per-function.
Wire schedulePricingSync into start.ts with CLI flags:
--pricing-sync-model, --pricing-sync-interval-days, --pricing-sync-disabled

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
New `pricing-sync` CLI subcommand bootstraps a temporary server and runs
a one-off pricing sync against Azure/Anthropic sources via the LLM
extraction pipeline.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
)

Add pricing.models (current per-model rates) and pricing.lastSync
(latest sync log entry) to the /usage endpoint response, enabling
dashboard consumers to display cost configuration and sync health.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Remove response_format (not supported by all Copilot models)
- Pre-filter Azure rows to only model-relevant entries (reduces payload)
- Trim Anthropic HTML to pricing section (~10KB cap)
- Strip markdown fences from LLM response
- Log response body on error for easier debugging

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add `auth add`, `auth list`, and `auth remove` subcommands for interactive
multi-account management. `auth add` runs Device Flow OAuth and auto-detects
the GitHub username as account name. Accounts are stored in accounts.json.
Legacy `auth` (bare) still works as before for backward compatibility.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Tests for readAccountsFile, writeAccountsFile, addAccountEntry, and
removeAccountEntry covering round-trip, duplicate detection, removal,
and missing file scenarios.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add runtime-adaptive SQLite adapter (bun:sqlite for Bun, better-sqlite3 for Node.js)
- Support comma-separated multi-token --github-token with name:type:token format
- Rewrite README with full feature documentation

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add .gitattributes with eol=lf for cross-platform consistency
- Add GitHub Actions workflow to auto-publish to npm on release

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Single tokens with name:type:token format were treated as raw tokens
because parsing only triggered when comma was found.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Citty's underlying parser (mri) returns an array for repeated flags.
Normalize string|string[] to comma-separated before parsing.

Now both formats work:
  --github-token "a:individual:ghu_x,b:business:ghu_y"
  --github-token a:individual:ghu_x --github-token b:business:ghu_y

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add detectAccountInfo() that calls /user and /copilot_internal/user
- Simplify --github-token format to just token or name:token
- Account type (individual/business/enterprise) auto-detected from API
- Username auto-detected when no name provided
- Remove --account-type from auth add (always auto-detect)
- Deduplicate account names with suffix when needed

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Logs method, URL, and all headers of every incoming request to
~/.local/share/copilot-api/headers.log. Authorization values are
truncated for safety.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add --record-requests, --record-dir, --record-parts CLI flags for
opt-in HTTP request/response recording to per-request directories.
Remove unconditional header-only logging. Disable sourcemaps in build
to reduce npm package size. Bump to v0.9.0.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@lubobill1990 lubobill1990 merged commit 2899905 into master Apr 26, 2026
1 check failed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant