Skip to content
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -33,3 +33,5 @@ gemini-debug-*.log

# Finder (MacOS) folder config
.DS_Store
.atl/
.atl
19 changes: 19 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# OpenCode Agent Instructions

## Commands
- **Install dependencies:** `bun install`
- **Run tests:** `bun test` (runs all tests). To run a specific test: `bun test path/to/test.ts`
- **Build project:** `bun run build` (uses `tsup` to compile ESM).
- **Check node import compatibility:** `bun run smoke:node-import`

## Architecture & Conventions
- **Opencode Plugin**: This is an Opencode authentication plugin for Gemini.
- **Entrypoints**: `index.ts` is the main export. `src/plugin.ts` sets up the loader and `fetch` interception.
- **Multi-Account**: The plugin supports multiple Gemini accounts via `AccountPool` (`src/plugin/account-pool.ts`). Accounts are selected using a rotation strategy (cooldown-aware LRU with quota fallback).
- **Storage**: Auth state is maintained in-memory and dynamically added accounts persist to `~/.config/opencode/gemini-auth.json` via `src/plugin/config-store.ts`. (Note: During testing, it isolates to `gemini-auth.test.json`).
- **Quota Handling**: `gquota` tool dynamically resolves Google Cloud project IDs and aggregates metrics across multiple accounts.

## Workflow
- **Spec-Driven Development (SDD)**: This repo uses SDD. Look for specs and task artifacts in `openspec/` (or via Engram memory) before making architectural changes.
- **Strict TDD**: Write tests for any new features or bug fixes. `bun test` must pass before considering a task complete. Keep tests next to source files (e.g., `feature.test.ts`).
- **Imports**: Ensure all necessary functions are exported from `src/plugin/types.ts` if they need to be shared across modules.
57 changes: 57 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,63 @@ or via environment variables:
You can also set `OPENCODE_GEMINI_PROJECT_ID`, `GOOGLE_CLOUD_PROJECT`, or
`GOOGLE_CLOUD_PROJECT_ID` to supply the project ID via environment variables.

### Multi-Account Support

This plugin supports using multiple Google accounts simultaneously to distribute quota usage and avoid `429 Quota Exhausted` errors.

When an account exhausts its quota for a requested model, the plugin places that account on a temporary cooldown and seamlessly retries your request with the next healthiest account in the background.

#### Adding Accounts via UI (Recommended)

1. Open your terminal and run `opencode auth login`.
2. Choose **Google**.
3. Select **Add Gemini Account**.
4. Complete the OAuth flow in your browser.

Accounts added via the UI are automatically persisted across Opencode restarts to `~/.config/opencode/gemini-auth.json`.

#### Managing Accounts

You can view the status of all your configured accounts by selecting **Manage Gemini Accounts** in the `opencode auth login` menu. This allows you to check health scores, active cooldowns, and manually enable or disable specific accounts.

#### Configuring Accounts Manually (Fallback)

If you prefer to configure your accounts manually or deploy the plugin in a headless environment, you can define them directly in your `opencode.json` under `provider.google.options.accounts`:

```json
{
"provider": {
"google": {
"options": {
"accounts": [
{
"id": "work",
"email": "user@company.com",
"refreshToken": "your-refresh-token-1",
"projectId": "prod-project"
},
{
"id": "personal",
"email": "user@gmail.com",
"refreshToken": "your-refresh-token-2"
}
]
}
}
}
}
```

*Note: Refresh tokens can be obtained from the `~/.config/opencode/gemini-auth.json` file after logging in at least once, or extracted from Opencode's credential store.*

#### Rotation Strategy

The plugin uses a hybrid **Cooldown-aware LRU with Quota Fallback** strategy:
1. **Cooldowns:** Ignores any account that recently received a `429` error for the requested model.
2. **Quota preference:** Prefers accounts with >20% remaining quota (based on `/gquota` data).
3. **Health Scoring:** Weighs accounts based on their historical success rate, average latency, and cooldown frequency.
4. **LRU Tiebreaker:** Uses the least recently used account to ensure fair round-robin distribution among healthy accounts.

### Proxy

If your network requires an HTTP proxy for Google API calls, set
Expand Down
52 changes: 52 additions & 0 deletions openspec/changes/multi-account-rotation/tasks.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
# Tasks: multi-account-rotation

Chain strategy: stacked-to-main | PR 1 of 5

## Phase 1: Foundation (PR 1, ~300 lines)

- [x] T1: types.ts — Add GeminiAccount, AccountState, HealthScore, CooldownState, AccountPoolConfig, RotationStrategy
- [x] T2: health.ts — HealthTracker class, O(1) incremental scoring (success 40%, quota 30%, latency 15%, cooldown 15%)
- [x] T3: rotation.ts — selectBestAccount() filter→health-weight→quota→LRU
- [x] T4: health.test.ts — Score monotonicity tests
- [x] T5: rotation.test.ts — All-filter scenarios

## Phase 2: Core Pool (PR 2, ~350 lines)

- [ ] T6: account-pool.ts — AccountPool class (select, cooldown, reportSuccess/Failure, get/add/remove/toggle)
- [ ] T7: account-pool.ts — Per-account refresh lock via Map<accountId, Promise>
- [ ] T8: account-pool.ts — Pool-of-one wrapper for backwards compat
- [ ] T9: cache.ts — Re-key auth cache by account ID
- [ ] T10: account-pool.test.ts — Selection, cooldown isolation, refresh lock, pool-of-one

## Phase 3: Auth & Project Context (PR 3, ~300 lines)

- [ ] T11: token.ts — refreshAccessTokenForAccount()
- [ ] T12: token.test.ts — Per-account refresh lock tests
- [ ] T13: project/utils.ts — buildProjectCacheKeyForAccount()
- [ ] T14: project/context.ts — Cache keyed by account ID, ensureProjectContextForAccount()
- [ ] T15: project/context.test.ts — Cache isolation per account
- [ ] T16: provider.ts — Per-account project ID resolution
- [ ] T17: oauth-authorize.ts — createOAuthAuthorizeMethodForAccount()

## Phase 4: Plugin Integration (PR 4, ~300 lines)

- [ ] T18: plugin.ts — Loader detects accounts[], builds AccountPool, fetch calls pool.select()
- [ ] T19: plugin.ts — fetch calls pool.refreshAccount(), ensureProjectContextForAccount()
- [ ] T20: plugin.test.ts — Multi-account init, fetch dispatch, backwards compat

## Phase 5: Retry & Quota (PR 4 continued)

- [ ] T21: retry/index.ts — Cooldown keys include account ID namespace
- [ ] T22: retry/index.ts — Terminal 429 sets per-account cooldown, delegates to pool.select()
- [ ] T23: retry/index.ts — All-exhausted error message lists accounts with reasons
- [ ] T24: quota.ts — Per-account quota sections
- [ ] T25: quota.ts — Aggregate summary header
- [ ] T26: quota.test.ts — Per-account formatting, aggregate, single-account compat

## Phase 6: Account Management (PR 5, ~350 lines)

- [ ] T27: account-manager.ts — Add Gemini Account auth method
- [ ] T28: account-manager.ts — Manage Gemini Accounts (list, toggle)
- [ ] T29: account-manager.ts — Remove account flow
- [ ] T30: oauth-authorize.ts — Extend OAuth for multi-account context
- [ ] T31: account-manager.test.ts — Add/remove/toggle, OAuth integration, config persistence
33 changes: 33 additions & 0 deletions openspec/config.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# openspec/config.yaml
schema: spec-driven

context: |
Tech stack: TypeScript (ESNext, strict mode), Bun runtime, ESM modules
Architecture: OpenCode plugin for Gemini OAuth auth — PKCE flow, token refresh, request proxying to Gemini Code Assist API, quota management
Testing: Bun built-in test runner (bun:test), 11 test files, no coverage tool configured
Style: TypeScript with strict mode, verbatim module syntax, no external linter/formatter configured
Build: tsup bundles to ESM, target node20, inlines external dependencies
CI: GitHub Actions — release pipeline via bun install + tsup build + npm publish

rules:
proposal:
- Include rollback plan for risky changes
specs:
- Use Given/When/Then for scenarios
- Use RFC 2119 keywords (MUST, SHALL, SHOULD, MAY)
design:
- Include sequence diagrams for complex flows
- Document architecture decisions with rationale
tasks:
- Group by phase, use hierarchical numbering
- Keep tasks completable in one session
apply:
- Follow existing code patterns
tdd: true
test_command: "bun test"
verify:
test_command: "bun test"
build_command: "bun run build"
coverage_threshold: 0
archive:
- Warn before merging destructive deltas
Loading